config.js 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741
  1. const _ = require('underscore-plus');
  2. const { Emitter } = require('event-kit');
  3. const {
  4. getValueAtKeyPath,
  5. setValueAtKeyPath,
  6. deleteValueAtKeyPath,
  7. pushKeyPath,
  8. splitKeyPath
  9. } = require('key-path-helpers');
  10. const Color = require('./color');
  11. const ScopedPropertyStore = require('scoped-property-store');
  12. const ScopeDescriptor = require('./scope-descriptor');
  13. const schemaEnforcers = {};
  14. // Essential: Used to access all of Pulsar's configuration details.
  15. //
  16. // An instance of this class is always available as the `atom.config` global.
  17. //
  18. // ## Getting and setting config settings.
  19. //
  20. // ```coffee
  21. // # Note that with no value set, ::get returns the setting's default value.
  22. // atom.config.get('my-package.myKey') # -> 'defaultValue'
  23. //
  24. // atom.config.set('my-package.myKey', 'value')
  25. // atom.config.get('my-package.myKey') # -> 'value'
  26. // ```
  27. //
  28. // You may want to watch for changes. Use {::observe} to catch changes to the setting.
  29. //
  30. // ```coffee
  31. // atom.config.set('my-package.myKey', 'value')
  32. // atom.config.observe 'my-package.myKey', (newValue) ->
  33. // # `observe` calls immediately and every time the value is changed
  34. // console.log 'My configuration changed:', newValue
  35. // ```
  36. //
  37. // If you want a notification only when the value changes, use {::onDidChange}.
  38. //
  39. // ```coffee
  40. // atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) ->
  41. // console.log 'My configuration changed:', newValue, oldValue
  42. // ```
  43. //
  44. // ### Value Coercion
  45. //
  46. // Config settings each have a type specified by way of a
  47. // [schema](json-schema.org). For example we might want an integer setting that only
  48. // allows integers greater than `0`:
  49. //
  50. // ```coffee
  51. // # When no value has been set, `::get` returns the setting's default value
  52. // atom.config.get('my-package.anInt') # -> 12
  53. //
  54. // # The string will be coerced to the integer 123
  55. // atom.config.set('my-package.anInt', '123')
  56. // atom.config.get('my-package.anInt') # -> 123
  57. //
  58. // # The string will be coerced to an integer, but it must be greater than 0, so is set to 1
  59. // atom.config.set('my-package.anInt', '-20')
  60. // atom.config.get('my-package.anInt') # -> 1
  61. // ```
  62. //
  63. // ## Defining settings for your package
  64. //
  65. // Define a schema under a `config` key in your package main.
  66. //
  67. // ```coffee
  68. // module.exports =
  69. // # Your config schema
  70. // config:
  71. // someInt:
  72. // type: 'integer'
  73. // default: 23
  74. // minimum: 1
  75. //
  76. // activate: (state) -> # ...
  77. // # ...
  78. // ```
  79. //
  80. // See [package docs](https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#package-word-count) for
  81. // more info.
  82. //
  83. // ## Config Schemas
  84. //
  85. // We use [json schema](http://json-schema.org) which allows you to define your value's
  86. // default, the type it should be, etc. A simple example:
  87. //
  88. // ```coffee
  89. // # We want to provide an `enableThing`, and a `thingVolume`
  90. // config:
  91. // enableThing:
  92. // type: 'boolean'
  93. // default: false
  94. // thingVolume:
  95. // type: 'integer'
  96. // default: 5
  97. // minimum: 1
  98. // maximum: 11
  99. // ```
  100. //
  101. // The type keyword allows for type coercion and validation. If a `thingVolume` is
  102. // set to a string `'10'`, it will be coerced into an integer.
  103. //
  104. // ```coffee
  105. // atom.config.set('my-package.thingVolume', '10')
  106. // atom.config.get('my-package.thingVolume') # -> 10
  107. //
  108. // # It respects the min / max
  109. // atom.config.set('my-package.thingVolume', '400')
  110. // atom.config.get('my-package.thingVolume') # -> 11
  111. //
  112. // # If it cannot be coerced, the value will not be set
  113. // atom.config.set('my-package.thingVolume', 'cats')
  114. // atom.config.get('my-package.thingVolume') # -> 11
  115. // ```
  116. //
  117. // ### Supported Types
  118. //
  119. // The `type` keyword can be a string with any one of the following. You can also
  120. // chain them by specifying multiple in an an array. For example
  121. //
  122. // ```coffee
  123. // config:
  124. // someSetting:
  125. // type: ['boolean', 'integer']
  126. // default: 5
  127. //
  128. // # Then
  129. // atom.config.set('my-package.someSetting', 'true')
  130. // atom.config.get('my-package.someSetting') # -> true
  131. //
  132. // atom.config.set('my-package.someSetting', '12')
  133. // atom.config.get('my-package.someSetting') # -> 12
  134. // ```
  135. //
  136. // #### string
  137. //
  138. // Values must be a string.
  139. //
  140. // ```coffee
  141. // config:
  142. // someSetting:
  143. // type: 'string'
  144. // default: 'hello'
  145. // ```
  146. //
  147. // #### integer
  148. //
  149. // Values will be coerced into integer. Supports the (optional) `minimum` and
  150. // `maximum` keys.
  151. //
  152. // ```coffee
  153. // config:
  154. // someSetting:
  155. // type: 'integer'
  156. // default: 5
  157. // minimum: 1
  158. // maximum: 11
  159. // ```
  160. //
  161. // #### number
  162. //
  163. // Values will be coerced into a number, including real numbers. Supports the
  164. // (optional) `minimum` and `maximum` keys.
  165. //
  166. // ```coffee
  167. // config:
  168. // someSetting:
  169. // type: 'number'
  170. // default: 5.3
  171. // minimum: 1.5
  172. // maximum: 11.5
  173. // ```
  174. //
  175. // #### boolean
  176. //
  177. // Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into
  178. // a boolean. Numbers, arrays, objects, and anything else will not be coerced.
  179. //
  180. // ```coffee
  181. // config:
  182. // someSetting:
  183. // type: 'boolean'
  184. // default: false
  185. // ```
  186. //
  187. // #### array
  188. //
  189. // Value must be an Array. The types of the values can be specified by a
  190. // subschema in the `items` key.
  191. //
  192. // ```coffee
  193. // config:
  194. // someSetting:
  195. // type: 'array'
  196. // default: [1, 2, 3]
  197. // items:
  198. // type: 'integer'
  199. // minimum: 1.5
  200. // maximum: 11.5
  201. // ```
  202. //
  203. // #### color
  204. //
  205. // Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha`
  206. // properties that all have numeric values. `red`, `green`, `blue` will be in
  207. // the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any
  208. // valid CSS color format such as `#abc`, `#abcdef`, `white`,
  209. // `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`.
  210. //
  211. // ```coffee
  212. // config:
  213. // someSetting:
  214. // type: 'color'
  215. // default: 'white'
  216. // ```
  217. //
  218. // #### object / Grouping other types
  219. //
  220. // A config setting with the type `object` allows grouping a set of config
  221. // settings. The group will be visually separated and has its own group headline.
  222. // The sub options must be listed under a `properties` key.
  223. //
  224. // ```coffee
  225. // config:
  226. // someSetting:
  227. // type: 'object'
  228. // properties:
  229. // myChildIntOption:
  230. // type: 'integer'
  231. // minimum: 1.5
  232. // maximum: 11.5
  233. // ```
  234. //
  235. // ### Other Supported Keys
  236. //
  237. // #### enum
  238. //
  239. // All types support an `enum` key, which lets you specify all the values the
  240. // setting can take. `enum` may be an array of allowed values (of the specified
  241. // type), or an array of objects with `value` and `description` properties, where
  242. // the `value` is an allowed value, and the `description` is a descriptive string
  243. // used in the settings view.
  244. //
  245. // In this example, the setting must be one of the 4 integers:
  246. //
  247. // ```coffee
  248. // config:
  249. // someSetting:
  250. // type: 'integer'
  251. // default: 4
  252. // enum: [2, 4, 6, 8]
  253. // ```
  254. //
  255. // In this example, the setting must be either 'foo' or 'bar', which are
  256. // presented using the provided descriptions in the settings pane:
  257. //
  258. // ```coffee
  259. // config:
  260. // someSetting:
  261. // type: 'string'
  262. // default: 'foo'
  263. // enum: [
  264. // {value: 'foo', description: 'Foo mode. You want this.'}
  265. // {value: 'bar', description: 'Bar mode. Nobody wants that!'}
  266. // ]
  267. // ```
  268. //
  269. // If you only have a few elements, you can display your enum as a list of
  270. // radio buttons in the settings view rather than a select list. To do so,
  271. // specify `radio: true` as a sibling property to the `enum` array.
  272. //
  273. // ```coffee
  274. // config:
  275. // someSetting:
  276. // type: 'string'
  277. // default: 'foo'
  278. // enum: [
  279. // {value: 'foo', description: 'Foo mode. You want this.'}
  280. // {value: 'bar', description: 'Bar mode. Nobody wants that!'}
  281. // ]
  282. // radio: true
  283. // ```
  284. //
  285. // Usage:
  286. //
  287. // ```coffee
  288. // atom.config.set('my-package.someSetting', '2')
  289. // atom.config.get('my-package.someSetting') # -> 2
  290. //
  291. // # will not set values outside of the enum values
  292. // atom.config.set('my-package.someSetting', '3')
  293. // atom.config.get('my-package.someSetting') # -> 2
  294. //
  295. // # If it cannot be coerced, the value will not be set
  296. // atom.config.set('my-package.someSetting', '4')
  297. // atom.config.get('my-package.someSetting') # -> 4
  298. // ```
  299. //
  300. // #### title and description
  301. //
  302. // The settings view will use the `title` and `description` keys to display your
  303. // config setting in a readable way. By default the settings view humanizes your
  304. // config key, so `someSetting` becomes `Some Setting`. In some cases, this is
  305. // confusing for users, and a more descriptive title is useful.
  306. //
  307. // Descriptions will be displayed below the title in the settings view.
  308. //
  309. // For a group of config settings the humanized key or the title and the
  310. // description are used for the group headline.
  311. //
  312. // ```coffee
  313. // config:
  314. // someSetting:
  315. // title: 'Setting Magnitude'
  316. // description: 'This will affect the blah and the other blah'
  317. // type: 'integer'
  318. // default: 4
  319. // ```
  320. //
  321. // __Note__: You should strive to be so clear in your naming of the setting that
  322. // you do not need to specify a title or description!
  323. //
  324. // Descriptions allow a subset of
  325. // [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/).
  326. // Specifically, you may use the following in configuration setting descriptions:
  327. //
  328. // * **bold** - `**bold**`
  329. // * *italics* - `*italics*`
  330. // * [links](https://pulsar-edit.dev) - `[links](https://pulsar-edit.dev)`
  331. // * `code spans` - `` `code spans` ``
  332. // * line breaks - `line breaks<br/>`
  333. // * ~~strikethrough~~ - `~~strikethrough~~`
  334. //
  335. // #### order
  336. //
  337. // The settings view orders your settings alphabetically. You can override this
  338. // ordering with the order key.
  339. //
  340. // ```coffee
  341. // config:
  342. // zSetting:
  343. // type: 'integer'
  344. // default: 4
  345. // order: 1
  346. // aSetting:
  347. // type: 'integer'
  348. // default: 4
  349. // order: 2
  350. // ```
  351. //
  352. // ## Manipulating values outside your configuration schema
  353. //
  354. // It is possible to manipulate(`get`, `set`, `observe` etc) values that do not
  355. // appear in your configuration schema. For example, if the config schema of the
  356. // package 'some-package' is
  357. //
  358. // ```coffee
  359. // config:
  360. // someSetting:
  361. // type: 'boolean'
  362. // default: false
  363. // ```
  364. //
  365. // You can still do the following
  366. //
  367. // ```coffee
  368. // let otherSetting = atom.config.get('some-package.otherSetting')
  369. // atom.config.set('some-package.stillAnotherSetting', otherSetting * 5)
  370. // ```
  371. //
  372. // In other words, if a function asks for a `key-path`, that path doesn't have to
  373. // be described in the config schema for the package or any package. However, as
  374. // highlighted in the best practices section, you are advised against doing the
  375. // above.
  376. //
  377. // ## Best practices
  378. //
  379. // * Don't depend on (or write to) configuration keys outside of your keypath.
  380. //
  381. class Config {
  382. static addSchemaEnforcer(typeName, enforcerFunction) {
  383. if (schemaEnforcers[typeName] == null) {
  384. schemaEnforcers[typeName] = [];
  385. }
  386. return schemaEnforcers[typeName].push(enforcerFunction);
  387. }
  388. static addSchemaEnforcers(filters) {
  389. for (let typeName in filters) {
  390. const functions = filters[typeName];
  391. for (let name in functions) {
  392. const enforcerFunction = functions[name];
  393. this.addSchemaEnforcer(typeName, enforcerFunction);
  394. }
  395. }
  396. }
  397. static executeSchemaEnforcers(keyPath, value, schema) {
  398. let error = null;
  399. let types = schema.type;
  400. if (!Array.isArray(types)) {
  401. types = [types];
  402. }
  403. for (let type of types) {
  404. try {
  405. const enforcerFunctions = schemaEnforcers[type].concat(
  406. schemaEnforcers['*']
  407. );
  408. for (let enforcer of enforcerFunctions) {
  409. // At some point in one's life, one must call upon an enforcer.
  410. value = enforcer.call(this, keyPath, value, schema);
  411. }
  412. error = null;
  413. break;
  414. } catch (e) {
  415. error = e;
  416. }
  417. }
  418. if (error != null) {
  419. throw error;
  420. }
  421. return value;
  422. }
  423. // Created during initialization, available as `atom.config`
  424. constructor(params = {}) {
  425. this.clear();
  426. this.initialize(params);
  427. }
  428. initialize({ saveCallback, mainSource, projectHomeSchema }) {
  429. if (saveCallback) {
  430. this.saveCallback = saveCallback;
  431. }
  432. if (mainSource) this.mainSource = mainSource;
  433. if (projectHomeSchema) {
  434. this.schema.properties.core.properties.projectHome = projectHomeSchema;
  435. this.defaultSettings.core.projectHome = projectHomeSchema.default;
  436. }
  437. }
  438. clear() {
  439. this.emitter = new Emitter();
  440. this.schema = {
  441. type: 'object',
  442. properties: {}
  443. };
  444. this.defaultSettings = {};
  445. this.settings = {};
  446. this.projectSettings = {};
  447. this.projectFile = null;
  448. this.scopedSettingsStore = new ScopedPropertyStore();
  449. this.settingsLoaded = false;
  450. this.transactDepth = 0;
  451. this.pendingOperations = [];
  452. this.legacyScopeAliases = new Map();
  453. this.requestSave = _.debounce(() => this.save(), 1);
  454. }
  455. /*
  456. Section: Config Subscription
  457. */
  458. // Essential: Add a listener for changes to a given key path. This is different
  459. // than {::onDidChange} in that it will immediately call your callback with the
  460. // current value of the config entry.
  461. //
  462. // ### Examples
  463. //
  464. // You might want to be notified when the themes change. We'll watch
  465. // `core.themes` for changes
  466. //
  467. // ```coffee
  468. // atom.config.observe 'core.themes', (value) ->
  469. // # do stuff with value
  470. // ```
  471. //
  472. // * `keyPath` {String} name of the key to observe
  473. // * `options` (optional) {Object}
  474. // * `scope` (optional) {ScopeDescriptor} describing a path from
  475. // the root of the syntax tree to a token. Get one by calling
  476. // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
  477. // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
  478. // for more information.
  479. // * `callback` {Function} to call when the value of the key changes.
  480. // * `value` the new value of the key
  481. //
  482. // Returns a {Disposable} with the following keys on which you can call
  483. // `.dispose()` to unsubscribe.
  484. observe(...args) {
  485. let callback, keyPath, options, scopeDescriptor;
  486. if (args.length === 2) {
  487. [keyPath, callback] = args;
  488. } else if (
  489. args.length === 3 &&
  490. (_.isString(args[0]) && _.isObject(args[1]))
  491. ) {
  492. [keyPath, options, callback] = args;
  493. scopeDescriptor = options.scope;
  494. } else {
  495. console.error(
  496. 'An unsupported form of Config::observe is being used. See https://atom.io/docs/api/latest/Config for details'
  497. );
  498. return;
  499. }
  500. if (scopeDescriptor != null) {
  501. return this.observeScopedKeyPath(scopeDescriptor, keyPath, callback);
  502. } else {
  503. return this.observeKeyPath(
  504. keyPath,
  505. options ?? {},
  506. callback
  507. );
  508. }
  509. }
  510. // Essential: Add a listener for changes to a given key path. If `keyPath` is
  511. // not specified, your callback will be called on changes to any key.
  512. //
  513. // * `keyPath` (optional) {String} name of the key to observe. Must be
  514. // specified if `scopeDescriptor` is specified.
  515. // * `options` (optional) {Object}
  516. // * `scope` (optional) {ScopeDescriptor} describing a path from
  517. // the root of the syntax tree to a token. Get one by calling
  518. // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
  519. // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
  520. // for more information.
  521. // * `callback` {Function} to call when the value of the key changes.
  522. // * `event` {Object}
  523. // * `newValue` the new value of the key
  524. // * `oldValue` the prior value of the key.
  525. //
  526. // Returns a {Disposable} with the following keys on which you can call
  527. // `.dispose()` to unsubscribe.
  528. onDidChange(...args) {
  529. let callback, keyPath, scopeDescriptor;
  530. if (args.length === 1) {
  531. [callback] = args;
  532. } else if (args.length === 2) {
  533. [keyPath, callback] = args;
  534. } else {
  535. let options;
  536. [keyPath, options, callback] = args;
  537. scopeDescriptor = options.scope;
  538. }
  539. if (scopeDescriptor != null) {
  540. return this.onDidChangeScopedKeyPath(scopeDescriptor, keyPath, callback);
  541. } else {
  542. return this.onDidChangeKeyPath(keyPath, callback);
  543. }
  544. }
  545. /*
  546. Section: Managing Settings
  547. */
  548. // Essential: Retrieves the setting for the given key.
  549. //
  550. // ### Examples
  551. //
  552. // You might want to know what themes are enabled, so check `core.themes`
  553. //
  554. // ```coffee
  555. // atom.config.get('core.themes')
  556. // ```
  557. //
  558. // With scope descriptors you can get settings within a specific editor
  559. // scope. For example, you might want to know `editor.tabLength` for ruby
  560. // files.
  561. //
  562. // ```coffee
  563. // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
  564. // ```
  565. //
  566. // This setting in ruby files might be different than the global tabLength setting
  567. //
  568. // ```coffee
  569. // atom.config.get('editor.tabLength') # => 4
  570. // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
  571. // ```
  572. //
  573. // You can get the language scope descriptor via
  574. // {TextEditor::getRootScopeDescriptor}. This will get the setting specifically
  575. // for the editor's language.
  576. //
  577. // ```coffee
  578. // atom.config.get('editor.tabLength', scope: @editor.getRootScopeDescriptor()) # => 2
  579. // ```
  580. //
  581. // Additionally, you can get the setting at the specific cursor position.
  582. //
  583. // ```coffee
  584. // scopeDescriptor = @editor.getLastCursor().getScopeDescriptor()
  585. // atom.config.get('editor.tabLength', scope: scopeDescriptor) # => 2
  586. // ```
  587. //
  588. // * `keyPath` The {String} name of the key to retrieve.
  589. // * `options` (optional) {Object}
  590. // * `sources` (optional) {Array} of {String} source names. If provided, only
  591. // values that were associated with these sources during {::set} will be used.
  592. // * `excludeSources` (optional) {Array} of {String} source names. If provided,
  593. // values that were associated with these sources during {::set} will not
  594. // be used.
  595. // * `scope` (optional) {ScopeDescriptor} describing a path from
  596. // the root of the syntax tree to a token. Get one by calling
  597. // {editor.getLastCursor().getScopeDescriptor()}
  598. // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
  599. // for more information.
  600. //
  601. // Returns the value from Pulsar's default settings, the user's configuration
  602. // file in the type specified by the configuration schema.
  603. get(...args) {
  604. let keyPath, options, scope;
  605. if (args.length > 1) {
  606. if (typeof args[0] === 'string' || args[0] == null) {
  607. [keyPath, options] = args;
  608. ({ scope } = options);
  609. }
  610. } else {
  611. [keyPath] = args;
  612. }
  613. if (scope != null) {
  614. const value = this.getRawScopedValue(scope, keyPath, options);
  615. return value != null ? value : this.getRawValue(keyPath, options);
  616. } else {
  617. return this.getRawValue(keyPath, options);
  618. }
  619. }
  620. // Extended: Get all of the values for the given key-path, along with their
  621. // associated scope selector.
  622. //
  623. // * `keyPath` The {String} name of the key to retrieve
  624. // * `options` (optional) {Object} see the `options` argument to {::get}
  625. //
  626. // Returns an {Array} of {Object}s with the following keys:
  627. // * `scopeDescriptor` The {ScopeDescriptor} with which the value is associated
  628. // * `value` The value for the key-path
  629. getAll(keyPath, options) {
  630. let globalValue, result, scope;
  631. if (options != null) {
  632. ({ scope } = options);
  633. }
  634. if (scope != null) {
  635. let legacyScopeDescriptor;
  636. const scopeDescriptor = ScopeDescriptor.fromObject(scope);
  637. result = this.scopedSettingsStore.getAll(
  638. scopeDescriptor.getScopeChain(),
  639. keyPath,
  640. options
  641. );
  642. legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(
  643. scopeDescriptor
  644. );
  645. if (legacyScopeDescriptor) {
  646. result.push(
  647. ...Array.from(
  648. this.scopedSettingsStore.getAll(
  649. legacyScopeDescriptor.getScopeChain(),
  650. keyPath,
  651. options
  652. ) || []
  653. )
  654. );
  655. }
  656. } else {
  657. result = [];
  658. }
  659. globalValue = this.getRawValue(keyPath, options);
  660. if (globalValue) {
  661. result.push({ scopeSelector: '*', value: globalValue });
  662. }
  663. return result;
  664. }
  665. // Essential: Sets the value for a configuration setting.
  666. //
  667. // This value is stored in Pulsar's internal configuration file.
  668. //
  669. // ### Examples
  670. //
  671. // You might want to change the themes programmatically:
  672. //
  673. // ```coffee
  674. // atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax'])
  675. // ```
  676. //
  677. // You can also set scoped settings. For example, you might want change the
  678. // `editor.tabLength` only for ruby files.
  679. //
  680. // ```coffee
  681. // atom.config.get('editor.tabLength') # => 4
  682. // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 4
  683. // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
  684. //
  685. // # Set ruby to 2
  686. // atom.config.set('editor.tabLength', 2, scopeSelector: '.source.ruby') # => true
  687. //
  688. // # Notice it's only set to 2 in the case of ruby
  689. // atom.config.get('editor.tabLength') # => 4
  690. // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
  691. // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
  692. // ```
  693. //
  694. // * `keyPath` The {String} name of the key.
  695. // * `value` The value of the setting. Passing `undefined` will revert the
  696. // setting to the default value.
  697. // * `options` (optional) {Object}
  698. // * `scopeSelector` (optional) {String}. eg. '.source.ruby'
  699. // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
  700. // for more information.
  701. // * `source` (optional) {String} The name of a file with which the setting
  702. // is associated. Defaults to the user's config file.
  703. //
  704. // Returns a {Boolean}
  705. // * `true` if the value was set.
  706. // * `false` if the value was not able to be coerced to the type specified in the setting's schema.
  707. set(...args) {
  708. let [keyPath, value, options = {}] = args;
  709. if (!this.settingsLoaded) {
  710. this.pendingOperations.push(() => this.set(keyPath, value, options));
  711. }
  712. // We should never use the scoped store to set global settings, since they are kept directly
  713. // in the config object.
  714. const scopeSelector =
  715. options.scopeSelector !== '*' ? options.scopeSelector : undefined;
  716. let source = options.source;
  717. const shouldSave = options.save != null ? options.save : true;
  718. if (source && !scopeSelector && source !== this.projectFile) {
  719. throw new Error(
  720. "::set with a 'source' and no 'scopeSelector' is not yet implemented!"
  721. );
  722. }
  723. if (!source) source = this.mainSource;
  724. if (value !== undefined) {
  725. try {
  726. value = this.makeValueConformToSchema(keyPath, value);
  727. } catch (e) {
  728. return false;
  729. }
  730. }
  731. if (scopeSelector != null) {
  732. this.setRawScopedValue(keyPath, value, source, scopeSelector);
  733. } else {
  734. this.setRawValue(keyPath, value, { source });
  735. }
  736. if (source === this.mainSource && shouldSave && this.settingsLoaded) {
  737. this.requestSave();
  738. }
  739. return true;
  740. }
  741. // Essential: Restore the setting at `keyPath` to its default value.
  742. //
  743. // * `keyPath` The {String} name of the key.
  744. // * `options` (optional) {Object}
  745. // * `scopeSelector` (optional) {String}. See {::set}
  746. // * `source` (optional) {String}. See {::set}
  747. unset(keyPath, options) {
  748. if (!this.settingsLoaded) {
  749. this.pendingOperations.push(() => this.unset(keyPath, options));
  750. }
  751. let { scopeSelector, source } = options != null ? options : {};
  752. if (source == null) {
  753. source = this.mainSource;
  754. }
  755. if (scopeSelector != null) {
  756. if (keyPath != null) {
  757. let settings = this.scopedSettingsStore.propertiesForSourceAndSelector(
  758. source,
  759. scopeSelector
  760. );
  761. if (getValueAtKeyPath(settings, keyPath) != null) {
  762. this.scopedSettingsStore.removePropertiesForSourceAndSelector(
  763. source,
  764. scopeSelector
  765. );
  766. setValueAtKeyPath(settings, keyPath, undefined);
  767. settings = withoutEmptyObjects(settings);
  768. if (settings != null) {
  769. this.set(null, settings, {
  770. scopeSelector,
  771. source,
  772. priority: this.priorityForSource(source)
  773. });
  774. }
  775. const configIsReady =
  776. source === this.mainSource && this.settingsLoaded;
  777. if (configIsReady) {
  778. return this.requestSave();
  779. }
  780. }
  781. } else {
  782. this.scopedSettingsStore.removePropertiesForSourceAndSelector(
  783. source,
  784. scopeSelector
  785. );
  786. return this.emitChangeEvent();
  787. }
  788. } else {
  789. for (scopeSelector in this.scopedSettingsStore.propertiesForSource(
  790. source
  791. )) {
  792. this.unset(keyPath, { scopeSelector, source });
  793. }
  794. if (keyPath != null && source === this.mainSource) {
  795. return this.set(
  796. keyPath,
  797. getValueAtKeyPath(this.defaultSettings, keyPath)
  798. );
  799. }
  800. }
  801. }
  802. // Extended: Get an {Array} of all of the `source` {String}s with which
  803. // settings have been added via {::set}.
  804. getSources() {
  805. return _.uniq(
  806. _.pluck(this.scopedSettingsStore.propertySets, 'source')
  807. ).sort();
  808. }
  809. // Extended: Retrieve the schema for a specific key path. The schema will tell
  810. // you what type the keyPath expects, and other metadata about the config
  811. // option.
  812. //
  813. // * `keyPath` The {String} name of the key.
  814. //
  815. // Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
  816. // Returns `null` when the keyPath has no schema specified, but is accessible
  817. // from the root schema.
  818. getSchema(keyPath) {
  819. const keys = splitKeyPath(keyPath);
  820. let { schema } = this;
  821. for (let key of keys) {
  822. let childSchema;
  823. if (schema.type === 'object') {
  824. childSchema =
  825. schema.properties != null ? schema.properties[key] : undefined;
  826. if (childSchema == null) {
  827. if (isPlainObject(schema.additionalProperties)) {
  828. childSchema = schema.additionalProperties;
  829. } else if (schema.additionalProperties === false) {
  830. return null;
  831. } else {
  832. return { type: 'any' };
  833. }
  834. }
  835. } else {
  836. return null;
  837. }
  838. schema = childSchema;
  839. }
  840. return schema;
  841. }
  842. getUserConfigPath() {
  843. return this.mainSource;
  844. }
  845. // Extended: Suppress calls to handler functions registered with {::onDidChange}
  846. // and {::observe} for the duration of `callback`. After `callback` executes,
  847. // handlers will be called once if the value for their key-path has changed.
  848. //
  849. // * `callback` {Function} to execute while suppressing calls to handlers.
  850. transact(callback) {
  851. this.beginTransaction();
  852. try {
  853. return callback();
  854. } finally {
  855. this.endTransaction();
  856. }
  857. }
  858. getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor) {
  859. return null;
  860. }
  861. /*
  862. Section: Internal methods used by core
  863. */
  864. // Private: Suppress calls to handler functions registered with {::onDidChange}
  865. // and {::observe} for the duration of the {Promise} returned by `callback`.
  866. // After the {Promise} is either resolved or rejected, handlers will be called
  867. // once if the value for their key-path has changed.
  868. //
  869. // * `callback` {Function} that returns a {Promise}, which will be executed
  870. // while suppressing calls to handlers.
  871. //
  872. // Returns a {Promise} that is either resolved or rejected according to the
  873. // `{Promise}` returned by `callback`. If `callback` throws an error, a
  874. // rejected {Promise} will be returned instead.
  875. transactAsync(callback) {
  876. let endTransaction;
  877. this.beginTransaction();
  878. try {
  879. endTransaction = fn => (...args) => {
  880. this.endTransaction();
  881. return fn(...args);
  882. };
  883. const result = callback();
  884. return new Promise((resolve, reject) => {
  885. return result
  886. .then(endTransaction(resolve))
  887. .catch(endTransaction(reject));
  888. });
  889. } catch (error) {
  890. this.endTransaction();
  891. return Promise.reject(error);
  892. }
  893. }
  894. beginTransaction() {
  895. this.transactDepth++;
  896. }
  897. endTransaction() {
  898. this.transactDepth--;
  899. this.emitChangeEvent();
  900. }
  901. pushAtKeyPath(keyPath, value) {
  902. const left = this.get(keyPath);
  903. const arrayValue = left == null ? [] : left;
  904. const result = arrayValue.push(value);
  905. this.set(keyPath, arrayValue);
  906. return result;
  907. }
  908. unshiftAtKeyPath(keyPath, value) {
  909. const left = this.get(keyPath);
  910. const arrayValue = left == null ? [] : left;
  911. const result = arrayValue.unshift(value);
  912. this.set(keyPath, arrayValue);
  913. return result;
  914. }
  915. removeAtKeyPath(keyPath, value) {
  916. const left = this.get(keyPath);
  917. const arrayValue = left == null ? [] : left;
  918. const result = _.remove(arrayValue, value);
  919. this.set(keyPath, arrayValue);
  920. return result;
  921. }
  922. setSchema(keyPath, schema) {
  923. if (!isPlainObject(schema)) {
  924. throw new Error(
  925. `Error loading schema for ${keyPath}: schemas can only be objects!`
  926. );
  927. }
  928. if (schema.type == null) {
  929. throw new Error(
  930. `Error loading schema for ${keyPath}: schema objects must have a type attribute`
  931. );
  932. }
  933. let rootSchema = this.schema;
  934. if (keyPath) {
  935. for (let key of splitKeyPath(keyPath)) {
  936. rootSchema.type = 'object';
  937. if (rootSchema.properties == null) {
  938. rootSchema.properties = {};
  939. }
  940. const { properties } = rootSchema;
  941. if (properties[key] == null) {
  942. properties[key] = {};
  943. }
  944. rootSchema = properties[key];
  945. }
  946. }
  947. Object.assign(rootSchema, schema);
  948. this.transact(() => {
  949. this.setDefaults(keyPath, this.extractDefaultsFromSchema(schema));
  950. this.setScopedDefaultsFromSchema(keyPath, schema);
  951. this.resetSettingsForSchemaChange();
  952. });
  953. }
  954. save() {
  955. if (this.saveCallback) {
  956. let allSettings = { '*': this.settings };
  957. allSettings = Object.assign(
  958. allSettings,
  959. this.scopedSettingsStore.propertiesForSource(this.mainSource)
  960. );
  961. allSettings = sortObject(allSettings);
  962. this.saveCallback(allSettings);
  963. }
  964. }
  965. /*
  966. Section: Private methods managing global settings
  967. */
  968. resetUserSettings(newSettings, options = {}) {
  969. this._resetSettings(newSettings, options);
  970. }
  971. _resetSettings(newSettings, options = {}) {
  972. const source = options.source;
  973. newSettings = Object.assign({}, newSettings);
  974. if (newSettings.global != null) {
  975. newSettings['*'] = newSettings.global;
  976. delete newSettings.global;
  977. }
  978. if (newSettings['*'] != null) {
  979. const scopedSettings = newSettings;
  980. newSettings = newSettings['*'];
  981. delete scopedSettings['*'];
  982. this.resetScopedSettings(scopedSettings, { source });
  983. }
  984. return this.transact(() => {
  985. this._clearUnscopedSettingsForSource(source);
  986. this.settingsLoaded = true;
  987. for (let key in newSettings) {
  988. const value = newSettings[key];
  989. this.set(key, value, { save: false, source });
  990. }
  991. if (this.pendingOperations.length) {
  992. for (let op of this.pendingOperations) {
  993. op();
  994. }
  995. this.pendingOperations = [];
  996. }
  997. });
  998. }
  999. _clearUnscopedSettingsForSource(source) {
  1000. if (source === this.projectFile) {
  1001. this.projectSettings = {};
  1002. } else {
  1003. this.settings = {};
  1004. }
  1005. }
  1006. resetProjectSettings(newSettings, projectFile) {
  1007. // Sets the scope and source of all project settings to `path`.
  1008. newSettings = Object.assign({}, newSettings);
  1009. const oldProjectFile = this.projectFile;
  1010. this.projectFile = projectFile;
  1011. if (this.projectFile != null) {
  1012. this._resetSettings(newSettings, { source: this.projectFile });
  1013. } else {
  1014. this.scopedSettingsStore.removePropertiesForSource(oldProjectFile);
  1015. this.projectSettings = {};
  1016. }
  1017. }
  1018. clearProjectSettings() {
  1019. this.resetProjectSettings({}, null);
  1020. }
  1021. getRawValue(keyPath, options = {}) {
  1022. let { excludeSources, sources } = options;
  1023. let value;
  1024. // If `excludeSources` is missing or does not exclude the main source…
  1025. if (!excludeSources || !excludeSources.includes(this.mainSource)) {
  1026. value = getValueAtKeyPath(this.settings, keyPath);
  1027. // we should prefer the project specific setting as long as…
  1028. if (
  1029. this.projectFile != null &&
  1030. // `excludeSources` is missing or does not include the project-specific
  1031. // source, and…
  1032. (
  1033. !excludeSources || !excludeSources.includes(this.projectFile)
  1034. ) &&
  1035. // `sources` is missing or includes the project-specific source.
  1036. (
  1037. !sources || sources.includes(this.projectFile)
  1038. )
  1039. ) {
  1040. let projectValue = getValueAtKeyPath(this.projectSettings, keyPath);
  1041. if (projectValue === undefined) {
  1042. // There is no project-specific override for this key path. `value`
  1043. // stays as `value` and we pretend this never happened.
  1044. } else if (isPlainObject(value) && isPlainObject(projectValue)) {
  1045. // This key path returned an object, so we need to merge the contents
  1046. // of the two objects into a third composite object. First we clone
  1047. // the project object so as not to modify it…
  1048. projectValue = this.deepClone(projectValue);
  1049. // …then we copy over the regular value's properties, preferring the
  1050. // project-specific value wherever there is overlap.
  1051. this.deepDefaults(projectValue, value);
  1052. value = projectValue;
  1053. } else {
  1054. // This is a single value, so we prefer the project version.
  1055. value = projectValue;
  1056. }
  1057. }
  1058. }
  1059. let defaultValue;
  1060. if (!options.sources || options.sources.length === 0) {
  1061. defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath);
  1062. }
  1063. if (value != null) {
  1064. value = this.deepClone(value);
  1065. if (isPlainObject(value) && isPlainObject(defaultValue)) {
  1066. this.deepDefaults(value, defaultValue);
  1067. }
  1068. return value;
  1069. } else {
  1070. return this.deepClone(defaultValue);
  1071. }
  1072. }
  1073. setRawValue(keyPath, value, options = {}) {
  1074. const source = options.source ? options.source : undefined;
  1075. const settingsToChange =
  1076. source === this.projectFile ? 'projectSettings' : 'settings';
  1077. const defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath);
  1078. if (_.isEqual(defaultValue, value)) {
  1079. if (keyPath != null) {
  1080. deleteValueAtKeyPath(this[settingsToChange], keyPath);
  1081. } else {
  1082. this[settingsToChange] = null;
  1083. }
  1084. } else {
  1085. if (keyPath != null) {
  1086. setValueAtKeyPath(this[settingsToChange], keyPath, value);
  1087. } else {
  1088. this[settingsToChange] = value;
  1089. }
  1090. }
  1091. return this.emitChangeEvent();
  1092. }
  1093. observeKeyPath(keyPath, options, callback) {
  1094. callback(this.get(keyPath));
  1095. return this.onDidChangeKeyPath(keyPath, event => callback(event.newValue));
  1096. }
  1097. onDidChangeKeyPath(keyPath, callback) {
  1098. let oldValue = this.get(keyPath);
  1099. return this.emitter.on('did-change', () => {
  1100. const newValue = this.get(keyPath);
  1101. if (!_.isEqual(oldValue, newValue)) {
  1102. const event = { oldValue, newValue };
  1103. oldValue = newValue;
  1104. return callback(event);
  1105. }
  1106. });
  1107. }
  1108. isSubKeyPath(keyPath, subKeyPath) {
  1109. if (keyPath == null || subKeyPath == null) {
  1110. return false;
  1111. }
  1112. const pathSubTokens = splitKeyPath(subKeyPath);
  1113. const pathTokens = splitKeyPath(keyPath).slice(0, pathSubTokens.length);
  1114. return _.isEqual(pathTokens, pathSubTokens);
  1115. }
  1116. setRawDefault(keyPath, value) {
  1117. setValueAtKeyPath(this.defaultSettings, keyPath, value);
  1118. return this.emitChangeEvent();
  1119. }
  1120. setDefaults(keyPath, defaults) {
  1121. if (defaults != null && isPlainObject(defaults)) {
  1122. const keys = splitKeyPath(keyPath);
  1123. this.transact(() => {
  1124. const result = [];
  1125. for (let key in defaults) {
  1126. const childValue = defaults[key];
  1127. if (!defaults.hasOwnProperty(key)) {
  1128. continue;
  1129. }
  1130. result.push(
  1131. this.setDefaults(keys.concat([key]).join('.'), childValue)
  1132. );
  1133. }
  1134. return result;
  1135. });
  1136. } else {
  1137. try {
  1138. defaults = this.makeValueConformToSchema(keyPath, defaults);
  1139. this.setRawDefault(keyPath, defaults);
  1140. } catch (e) {
  1141. console.warn(
  1142. `'${keyPath}' could not set the default. Attempted default: ${JSON.stringify(
  1143. defaults
  1144. )}; Schema: ${JSON.stringify(this.getSchema(keyPath))}`
  1145. );
  1146. }
  1147. }
  1148. }
  1149. deepClone(object) {
  1150. if (object instanceof Color) {
  1151. return object.clone();
  1152. } else if (Array.isArray(object)) {
  1153. return object.map(value => this.deepClone(value));
  1154. } else if (isPlainObject(object)) {
  1155. return _.mapObject(object, (key, value) => [key, this.deepClone(value)]);
  1156. } else {
  1157. return object;
  1158. }
  1159. }
  1160. deepDefaults(target) {
  1161. let result = target;
  1162. let i = 0;
  1163. while (++i < arguments.length) {
  1164. const object = arguments[i];
  1165. if (isPlainObject(result) && isPlainObject(object)) {
  1166. for (let key of Object.keys(object)) {
  1167. result[key] = this.deepDefaults(result[key], object[key]);
  1168. }
  1169. } else {
  1170. if (result == null) {
  1171. result = this.deepClone(object);
  1172. }
  1173. }
  1174. }
  1175. return result;
  1176. }
  1177. // `schema` will look something like this
  1178. //
  1179. // ```coffee
  1180. // type: 'string'
  1181. // default: 'ok'
  1182. // scopes:
  1183. // '.source.js':
  1184. // default: 'omg'
  1185. // ```
  1186. setScopedDefaultsFromSchema(keyPath, schema) {
  1187. if (schema.scopes != null && isPlainObject(schema.scopes)) {
  1188. const scopedDefaults = {};
  1189. for (let scope in schema.scopes) {
  1190. const scopeSchema = schema.scopes[scope];
  1191. if (!scopeSchema.hasOwnProperty('default')) {
  1192. continue;
  1193. }
  1194. scopedDefaults[scope] = {};
  1195. setValueAtKeyPath(scopedDefaults[scope], keyPath, scopeSchema.default);
  1196. }
  1197. this.scopedSettingsStore.addProperties('schema-default', scopedDefaults);
  1198. }
  1199. if (
  1200. schema.type === 'object' &&
  1201. schema.properties != null &&
  1202. isPlainObject(schema.properties)
  1203. ) {
  1204. const keys = splitKeyPath(keyPath);
  1205. for (let key in schema.properties) {
  1206. const childValue = schema.properties[key];
  1207. if (!schema.properties.hasOwnProperty(key)) {
  1208. continue;
  1209. }
  1210. this.setScopedDefaultsFromSchema(
  1211. keys.concat([key]).join('.'),
  1212. childValue
  1213. );
  1214. }
  1215. }
  1216. }
  1217. extractDefaultsFromSchema(schema) {
  1218. if (schema.default != null) {
  1219. return schema.default;
  1220. } else if (
  1221. schema.type === 'object' &&
  1222. schema.properties != null &&
  1223. isPlainObject(schema.properties)
  1224. ) {
  1225. const defaults = {};
  1226. const properties = schema.properties || {};
  1227. for (let key in properties) {
  1228. const value = properties[key];
  1229. defaults[key] = this.extractDefaultsFromSchema(value);
  1230. }
  1231. return defaults;
  1232. }
  1233. }
  1234. makeValueConformToSchema(keyPath, value, options) {
  1235. if (options != null ? options.suppressException : undefined) {
  1236. try {
  1237. return this.makeValueConformToSchema(keyPath, value);
  1238. } catch (e) {
  1239. return undefined;
  1240. }
  1241. } else {
  1242. let schema;
  1243. if ((schema = this.getSchema(keyPath)) == null) {
  1244. if (schema === false) {
  1245. throw new Error(`Illegal key path ${keyPath}`);
  1246. }
  1247. }
  1248. return this.constructor.executeSchemaEnforcers(keyPath, value, schema);
  1249. }
  1250. }
  1251. // When the schema is changed / added, there may be values set in the config
  1252. // that do not conform to the schema. This will reset make them conform.
  1253. resetSettingsForSchemaChange(source) {
  1254. if (source == null) {
  1255. source = this.mainSource;
  1256. }
  1257. return this.transact(() => {
  1258. this.settings = this.makeValueConformToSchema(null, this.settings, {
  1259. suppressException: true
  1260. });
  1261. const selectorsAndSettings = this.scopedSettingsStore.propertiesForSource(
  1262. source
  1263. );
  1264. this.scopedSettingsStore.removePropertiesForSource(source);
  1265. for (let scopeSelector in selectorsAndSettings) {
  1266. let settings = selectorsAndSettings[scopeSelector];
  1267. settings = this.makeValueConformToSchema(null, settings, {
  1268. suppressException: true
  1269. });
  1270. this.setRawScopedValue(null, settings, source, scopeSelector);
  1271. }
  1272. });
  1273. }
  1274. /*
  1275. Section: Private Scoped Settings
  1276. */
  1277. priorityForSource(source) {
  1278. switch (source) {
  1279. case this.mainSource:
  1280. return 1000;
  1281. case this.projectFile:
  1282. return 2000;
  1283. default:
  1284. return 0;
  1285. }
  1286. }
  1287. emitChangeEvent() {
  1288. if (this.transactDepth <= 0) {
  1289. return this.emitter.emit('did-change');
  1290. }
  1291. }
  1292. resetScopedSettings(newScopedSettings, options = {}) {
  1293. const source = options.source == null ? this.mainSource : options.source;
  1294. const priority = this.priorityForSource(source);
  1295. this.scopedSettingsStore.removePropertiesForSource(source);
  1296. for (let scopeSelector in newScopedSettings) {
  1297. let settings = newScopedSettings[scopeSelector];
  1298. settings = this.makeValueConformToSchema(null, settings, {
  1299. suppressException: true
  1300. });
  1301. const validatedSettings = {};
  1302. validatedSettings[scopeSelector] = withoutEmptyObjects(settings);
  1303. if (validatedSettings[scopeSelector] != null) {
  1304. this.scopedSettingsStore.addProperties(source, validatedSettings, {
  1305. priority
  1306. });
  1307. }
  1308. }
  1309. return this.emitChangeEvent();
  1310. }
  1311. setRawScopedValue(keyPath, value, source, selector, options) {
  1312. if (keyPath != null) {
  1313. const newValue = {};
  1314. setValueAtKeyPath(newValue, keyPath, value);
  1315. value = newValue;
  1316. }
  1317. const settingsBySelector = {};
  1318. settingsBySelector[selector] = value;
  1319. this.scopedSettingsStore.addProperties(source, settingsBySelector, {
  1320. priority: this.priorityForSource(source)
  1321. });
  1322. return this.emitChangeEvent();
  1323. }
  1324. getRawScopedValue(scopeDescriptor, keyPath, options) {
  1325. scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor);
  1326. const result = this.scopedSettingsStore.getPropertyValue(
  1327. scopeDescriptor.getScopeChain(),
  1328. keyPath,
  1329. options
  1330. );
  1331. const legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(
  1332. scopeDescriptor
  1333. );
  1334. if (result != null) {
  1335. return result;
  1336. } else if (legacyScopeDescriptor) {
  1337. return this.scopedSettingsStore.getPropertyValue(
  1338. legacyScopeDescriptor.getScopeChain(),
  1339. keyPath,
  1340. options
  1341. );
  1342. }
  1343. }
  1344. observeScopedKeyPath(scope, keyPath, callback) {
  1345. callback(this.get(keyPath, { scope }));
  1346. return this.onDidChangeScopedKeyPath(scope, keyPath, event =>
  1347. callback(event.newValue)
  1348. );
  1349. }
  1350. onDidChangeScopedKeyPath(scope, keyPath, callback) {
  1351. let oldValue = this.get(keyPath, { scope });
  1352. return this.emitter.on('did-change', () => {
  1353. const newValue = this.get(keyPath, { scope });
  1354. if (!_.isEqual(oldValue, newValue)) {
  1355. const event = { oldValue, newValue };
  1356. oldValue = newValue;
  1357. callback(event);
  1358. }
  1359. });
  1360. }
  1361. }
  1362. // Base schema enforcers. These will coerce raw input into the specified type,
  1363. // and will throw an error when the value cannot be coerced. Throwing the error
  1364. // will indicate that the value should not be set.
  1365. //
  1366. // Enforcers are run from most specific to least. For a schema with type
  1367. // `integer`, all the enforcers for the `integer` type will be run first, in
  1368. // order of specification. Then the `*` enforcers will be run, in order of
  1369. // specification.
  1370. Config.addSchemaEnforcers({
  1371. any: {
  1372. coerce(keyPath, value, schema) {
  1373. return value;
  1374. }
  1375. },
  1376. integer: {
  1377. coerce(keyPath, value, schema) {
  1378. value = parseInt(value);
  1379. if (isNaN(value) || !isFinite(value)) {
  1380. throw new Error(
  1381. `Validation failed at ${keyPath}, ${JSON.stringify(
  1382. value
  1383. )} cannot be coerced into an int`
  1384. );
  1385. }
  1386. return value;
  1387. }
  1388. },
  1389. number: {
  1390. coerce(keyPath, value, schema) {
  1391. value = parseFloat(value);
  1392. if (isNaN(value) || !isFinite(value)) {
  1393. throw new Error(
  1394. `Validation failed at ${keyPath}, ${JSON.stringify(
  1395. value
  1396. )} cannot be coerced into a number`
  1397. );
  1398. }
  1399. return value;
  1400. }
  1401. },
  1402. boolean: {
  1403. coerce(keyPath, value, schema) {
  1404. switch (typeof value) {
  1405. case 'string':
  1406. if (value.toLowerCase() === 'true') {
  1407. return true;
  1408. } else if (value.toLowerCase() === 'false') {
  1409. return false;
  1410. } else {
  1411. throw new Error(
  1412. `Validation failed at ${keyPath}, ${JSON.stringify(
  1413. value
  1414. )} must be a boolean or the string 'true' or 'false'`
  1415. );
  1416. }
  1417. case 'boolean':
  1418. return value;
  1419. default:
  1420. throw new Error(
  1421. `Validation failed at ${keyPath}, ${JSON.stringify(
  1422. value
  1423. )} must be a boolean or the string 'true' or 'false'`
  1424. );
  1425. }
  1426. }
  1427. },
  1428. string: {
  1429. validate(keyPath, value, schema) {
  1430. if (typeof value !== 'string') {
  1431. throw new Error(
  1432. `Validation failed at ${keyPath}, ${JSON.stringify(
  1433. value
  1434. )} must be a string`
  1435. );
  1436. }
  1437. return value;
  1438. },
  1439. validateMaximumLength(keyPath, value, schema) {
  1440. if (
  1441. typeof schema.maximumLength === 'number' &&
  1442. value.length > schema.maximumLength
  1443. ) {
  1444. return value.slice(0, schema.maximumLength);
  1445. } else {
  1446. return value;
  1447. }
  1448. }
  1449. },
  1450. null: {
  1451. // null sort of isnt supported. It will just unset in this case
  1452. coerce(keyPath, value, schema) {
  1453. if (![undefined, null].includes(value)) {
  1454. throw new Error(
  1455. `Validation failed at ${keyPath}, ${JSON.stringify(
  1456. value
  1457. )} must be null`
  1458. );
  1459. }
  1460. return value;
  1461. }
  1462. },
  1463. object: {
  1464. coerce(keyPath, value, schema) {
  1465. if (!isPlainObject(value)) {
  1466. throw new Error(
  1467. `Validation failed at ${keyPath}, ${JSON.stringify(
  1468. value
  1469. )} must be an object`
  1470. );
  1471. }
  1472. if (schema.properties == null) {
  1473. return value;
  1474. }
  1475. let defaultChildSchema = null;
  1476. let allowsAdditionalProperties = true;
  1477. if (isPlainObject(schema.additionalProperties)) {
  1478. defaultChildSchema = schema.additionalProperties;
  1479. }
  1480. if (schema.additionalProperties === false) {
  1481. allowsAdditionalProperties = false;
  1482. }
  1483. const newValue = {};
  1484. for (let prop in value) {
  1485. const propValue = value[prop];
  1486. const childSchema =
  1487. schema.properties[prop] != null
  1488. ? schema.properties[prop]
  1489. : defaultChildSchema;
  1490. if (childSchema != null) {
  1491. try {
  1492. newValue[prop] = this.executeSchemaEnforcers(
  1493. pushKeyPath(keyPath, prop),
  1494. propValue,
  1495. childSchema
  1496. );
  1497. } catch (error) {
  1498. console.warn(`Error setting item in object: ${error.message}`);
  1499. }
  1500. } else if (allowsAdditionalProperties) {
  1501. // Just pass through un-schema'd values
  1502. newValue[prop] = propValue;
  1503. } else {
  1504. console.warn(`Illegal object key: ${keyPath}.${prop}`);
  1505. }
  1506. }
  1507. return newValue;
  1508. }
  1509. },
  1510. array: {
  1511. coerce(keyPath, value, schema) {
  1512. if (!Array.isArray(value)) {
  1513. throw new Error(
  1514. `Validation failed at ${keyPath}, ${JSON.stringify(
  1515. value
  1516. )} must be an array`
  1517. );
  1518. }
  1519. const itemSchema = schema.items;
  1520. if (itemSchema != null) {
  1521. const newValue = [];
  1522. for (let item of value) {
  1523. try {
  1524. newValue.push(
  1525. this.executeSchemaEnforcers(keyPath, item, itemSchema)
  1526. );
  1527. } catch (error) {
  1528. console.warn(`Error setting item in array: ${error.message}`);
  1529. }
  1530. }
  1531. return newValue;
  1532. } else {
  1533. return value;
  1534. }
  1535. }
  1536. },
  1537. color: {
  1538. coerce(keyPath, value, schema) {
  1539. const color = Color.parse(value);
  1540. if (color == null) {
  1541. throw new Error(
  1542. `Validation failed at ${keyPath}, ${JSON.stringify(
  1543. value
  1544. )} cannot be coerced into a color`
  1545. );
  1546. }
  1547. return color;
  1548. }
  1549. },
  1550. '*': {
  1551. coerceMinimumAndMaximum(keyPath, value, schema) {
  1552. if (typeof value !== 'number') {
  1553. return value;
  1554. }
  1555. if (schema.minimum != null && typeof schema.minimum === 'number') {
  1556. value = Math.max(value, schema.minimum);
  1557. }
  1558. if (schema.maximum != null && typeof schema.maximum === 'number') {
  1559. value = Math.min(value, schema.maximum);
  1560. }
  1561. return value;
  1562. },
  1563. validateEnum(keyPath, value, schema) {
  1564. let possibleValues = schema.enum;
  1565. if (Array.isArray(possibleValues)) {
  1566. possibleValues = possibleValues.map(value => {
  1567. if (value.hasOwnProperty('value')) {
  1568. return value.value;
  1569. } else {
  1570. return value;
  1571. }
  1572. });
  1573. }
  1574. if (
  1575. possibleValues == null ||
  1576. !Array.isArray(possibleValues) ||
  1577. !possibleValues.length
  1578. ) {
  1579. return value;
  1580. }
  1581. for (let possibleValue of possibleValues) {
  1582. // Using `isEqual` for possibility of placing enums on array and object schemas
  1583. if (_.isEqual(possibleValue, value)) {
  1584. return value;
  1585. }
  1586. }
  1587. throw new Error(
  1588. `Validation failed at ${keyPath}, ${JSON.stringify(
  1589. value
  1590. )} is not one of ${JSON.stringify(possibleValues)}`
  1591. );
  1592. }
  1593. }
  1594. });
  1595. let isPlainObject = value =>
  1596. _.isObject(value) &&
  1597. !Array.isArray(value) &&
  1598. !_.isFunction(value) &&
  1599. !_.isString(value) &&
  1600. !(value instanceof Color);
  1601. let sortObject = value => {
  1602. if (!isPlainObject(value)) {
  1603. return value;
  1604. }
  1605. const result = {};
  1606. for (let key of Object.keys(value).sort()) {
  1607. result[key] = sortObject(value[key]);
  1608. }
  1609. return result;
  1610. };
  1611. const withoutEmptyObjects = object => {
  1612. let resultObject;
  1613. if (isPlainObject(object)) {
  1614. for (let key in object) {
  1615. const value = object[key];
  1616. const newValue = withoutEmptyObjects(value);
  1617. if (newValue != null) {
  1618. if (resultObject == null) {
  1619. resultObject = {};
  1620. }
  1621. resultObject[key] = newValue;
  1622. }
  1623. }
  1624. } else {
  1625. resultObject = object;
  1626. }
  1627. return resultObject;
  1628. };
  1629. module.exports = Config;