settingregistry.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import Ajv from 'ajv';
  4. import * as json from 'comment-json';
  5. import { find } from '@phosphor/algorithm';
  6. import {
  7. JSONExt,
  8. JSONObject,
  9. JSONValue,
  10. ReadonlyJSONObject,
  11. Token
  12. } from '@phosphor/coreutils';
  13. import { IDisposable } from '@phosphor/disposable';
  14. import { ISignal, Signal } from '@phosphor/signaling';
  15. import { IDataConnector } from './interfaces';
  16. /**
  17. * The key in the schema for setting editor icon class hints.
  18. */
  19. export const ICON_CLASS_KEY = 'jupyter.lab.setting-icon-class';
  20. /**
  21. * The key in the schema for setting editor icon label hints.
  22. */
  23. export const ICON_LABEL_KEY = 'jupyter.lab.setting-icon-label';
  24. /**
  25. * An alias for the JSON deep copy function.
  26. */
  27. const copy = JSONExt.deepCopy;
  28. /**
  29. * An implementation of a schema validator.
  30. */
  31. export interface ISchemaValidator {
  32. /**
  33. * Validate a plugin's schema and user data; populate the `composite` data.
  34. *
  35. * @param plugin - The plugin being validated. Its `composite` data will be
  36. * populated by reference.
  37. *
  38. * @param populate - Whether plugin data should be populated, defaults to
  39. * `true`.
  40. *
  41. * @return A list of errors if either the schema or data fail to validate or
  42. * `null` if there are no errors.
  43. */
  44. validateData(
  45. plugin: ISettingRegistry.IPlugin,
  46. populate?: boolean
  47. ): ISchemaValidator.IError[] | null;
  48. }
  49. /**
  50. * A namespace for schema validator interfaces.
  51. */
  52. export namespace ISchemaValidator {
  53. /**
  54. * A schema validation error definition.
  55. */
  56. export interface IError {
  57. /**
  58. * The path in the data where the error occurred.
  59. */
  60. dataPath: string;
  61. /**
  62. * The keyword whose validation failed.
  63. */
  64. keyword: string;
  65. /**
  66. * The error message.
  67. */
  68. message: string;
  69. /**
  70. * Optional parameter metadata that might be included in an error.
  71. */
  72. params?: ReadonlyJSONObject;
  73. /**
  74. * The path in the schema where the error occurred.
  75. */
  76. schemaPath: string;
  77. }
  78. }
  79. /* tslint:disable */
  80. /**
  81. * The setting registry token.
  82. */
  83. export const ISettingRegistry = new Token<ISettingRegistry>(
  84. '@jupyterlab/coreutils:ISettingRegistry'
  85. );
  86. /* tslint:enable */
  87. /**
  88. * A namespace for setting registry interfaces.
  89. */
  90. export namespace ISettingRegistry {
  91. /**
  92. * The settings for a specific plugin.
  93. */
  94. export interface IPlugin extends JSONObject {
  95. /**
  96. * The name of the plugin.
  97. */
  98. id: string;
  99. /**
  100. * The collection of values for a specified setting.
  101. */
  102. data: ISettingBundle;
  103. /**
  104. * The raw user settings data as a string containing JSON with comments.
  105. */
  106. raw: string;
  107. /**
  108. * The JSON schema for the plugin.
  109. */
  110. schema: ISchema;
  111. }
  112. /**
  113. * A schema type that is a minimal subset of the formal JSON Schema along with
  114. * optional JupyterLab rendering hints.
  115. */
  116. export interface ISchema extends JSONObject {
  117. /**
  118. * The JupyterLab icon class hint for a plugin can be overridden by user
  119. * settings. It can also be root level and therefore "private".
  120. */
  121. 'jupyter.lab.setting-icon-class'?: string;
  122. /**
  123. * The JupyterLab icon label hint for a plugin can be overridden by user
  124. * settings. It can also be root level and therefore "private".
  125. */
  126. 'jupyter.lab.setting-icon-label'?: string;
  127. /**
  128. * The default value, if any.
  129. */
  130. default?: any;
  131. /**
  132. * The schema description.
  133. */
  134. description?: string;
  135. /**
  136. * The schema's child properties.
  137. */
  138. properties?: {
  139. /**
  140. * The JupyterLab icon class hint for a plugin can be overridden by user
  141. * settings. It can also be root level and therefore "private".
  142. */
  143. 'jupyter.lab.setting-icon-class'?: ISchema;
  144. /**
  145. * The JupyterLab icon label hint for a plugin can be overridden by user
  146. * settings. It can also be root level and therefore "private".
  147. */
  148. 'jupyter.lab.setting-icon-label'?: ISchema;
  149. /**
  150. * Arbitrary setting keys can be added.
  151. */
  152. [key: string]: ISchema;
  153. };
  154. /**
  155. * The title of the schema.
  156. */
  157. title?: string;
  158. /**
  159. * The type or types of the data.
  160. */
  161. type?: string | string[];
  162. }
  163. /**
  164. * The setting values for a plugin.
  165. */
  166. export interface ISettingBundle extends JSONObject {
  167. /**
  168. * A composite of the user setting values and the plugin schema defaults.
  169. *
  170. * #### Notes
  171. * The `composite` values will always be a superset of the `user` values.
  172. */
  173. composite: JSONObject;
  174. /**
  175. * The user setting values.
  176. */
  177. user: JSONObject;
  178. }
  179. /**
  180. * An interface for manipulating the settings of a specific plugin.
  181. */
  182. export interface ISettings extends IDisposable {
  183. /**
  184. * A signal that emits when the plugin's settings have changed.
  185. */
  186. readonly changed: ISignal<this, void>;
  187. /**
  188. * Get the composite of user settings and extension defaults.
  189. */
  190. readonly composite: JSONObject;
  191. /*
  192. * The plugin name.
  193. */
  194. readonly plugin: string;
  195. /**
  196. * The plugin settings raw text value.
  197. */
  198. readonly raw: string;
  199. /**
  200. * Get the plugin settings schema.
  201. */
  202. readonly schema: ISettingRegistry.ISchema;
  203. /**
  204. * Get the user settings.
  205. */
  206. readonly user: JSONObject;
  207. /**
  208. * Return the defaults in a commented JSON format.
  209. */
  210. annotatedDefaults(): string;
  211. /**
  212. * Calculate the default value of a setting by iterating through the schema.
  213. *
  214. * @param key - The name of the setting whose default value is calculated.
  215. *
  216. * @returns A calculated default JSON value for a specific setting.
  217. */
  218. default(key: string): JSONValue | undefined;
  219. /**
  220. * Get an individual setting.
  221. *
  222. * @param key - The name of the setting being retrieved.
  223. *
  224. * @returns The setting value.
  225. */
  226. get(key: string): { composite: JSONValue; user: JSONValue };
  227. /**
  228. * Remove a single setting.
  229. *
  230. * @param key - The name of the setting being removed.
  231. *
  232. * @returns A promise that resolves when the setting is removed.
  233. *
  234. * #### Notes
  235. * This function is asynchronous because it writes to the setting registry.
  236. */
  237. remove(key: string): Promise<void>;
  238. /**
  239. * Save all of the plugin's user settings at once.
  240. */
  241. save(raw: string): Promise<void>;
  242. /**
  243. * Set a single setting.
  244. *
  245. * @param key - The name of the setting being set.
  246. *
  247. * @param value - The value of the setting.
  248. *
  249. * @returns A promise that resolves when the setting has been saved.
  250. *
  251. * #### Notes
  252. * This function is asynchronous because it writes to the setting registry.
  253. */
  254. set(key: string, value: JSONValue): Promise<void>;
  255. /**
  256. * Validates raw settings with comments.
  257. *
  258. * @param raw - The JSON with comments string being validated.
  259. *
  260. * @returns A list of errors or `null` if valid.
  261. */
  262. validate(raw: string): ISchemaValidator.IError[] | null;
  263. }
  264. }
  265. /**
  266. * An implementation of a setting registry.
  267. */
  268. export interface ISettingRegistry extends SettingRegistry {}
  269. /**
  270. * The default implementation of a schema validator.
  271. */
  272. export class DefaultSchemaValidator implements ISchemaValidator {
  273. /**
  274. * Instantiate a schema validator.
  275. */
  276. constructor() {
  277. this._composer.addSchema(Private.SCHEMA, 'main');
  278. this._validator.addSchema(Private.SCHEMA, 'main');
  279. }
  280. /**
  281. * Validate a plugin's schema and user data; populate the `composite` data.
  282. *
  283. * @param plugin - The plugin being validated. Its `composite` data will be
  284. * populated by reference.
  285. *
  286. * @param populate - Whether plugin data should be populated, defaults to
  287. * `true`.
  288. *
  289. * @return A list of errors if either the schema or data fail to validate or
  290. * `null` if there are no errors.
  291. */
  292. validateData(
  293. plugin: ISettingRegistry.IPlugin,
  294. populate = true
  295. ): ISchemaValidator.IError[] | null {
  296. const validate = this._validator.getSchema(plugin.id);
  297. const compose = this._composer.getSchema(plugin.id);
  298. // If the schemas do not exist, add them to the validator and continue.
  299. if (!validate || !compose) {
  300. const errors = this._addSchema(plugin.id, plugin.schema);
  301. if (errors) {
  302. return errors;
  303. }
  304. return this.validateData(plugin);
  305. }
  306. // Parse the raw commented JSON into a user map.
  307. let user: JSONObject;
  308. try {
  309. const strip = true;
  310. user = json.parse(plugin.raw, null, strip) as JSONObject;
  311. } catch (error) {
  312. if (error instanceof SyntaxError) {
  313. return [
  314. {
  315. dataPath: '',
  316. keyword: 'syntax',
  317. schemaPath: '',
  318. message: error.message
  319. }
  320. ];
  321. }
  322. const { column, description } = error;
  323. const line = error.lineNumber;
  324. return [
  325. {
  326. dataPath: '',
  327. keyword: 'parse',
  328. schemaPath: '',
  329. message: `${description} (line ${line} column ${column})`
  330. }
  331. ];
  332. }
  333. if (!validate(user)) {
  334. return validate.errors as ISchemaValidator.IError[];
  335. }
  336. // Copy the user data before merging defaults into composite map.
  337. const composite = copy(user);
  338. if (!compose(composite)) {
  339. return compose.errors as ISchemaValidator.IError[];
  340. }
  341. if (populate) {
  342. plugin.data = { composite, user };
  343. }
  344. return null;
  345. }
  346. /**
  347. * Add a schema to the validator.
  348. *
  349. * @param plugin - The plugin ID.
  350. *
  351. * @param schema - The schema being added.
  352. *
  353. * @return A list of errors if the schema fails to validate or `null` if there
  354. * are no errors.
  355. *
  356. * #### Notes
  357. * It is safe to call this function multiple times with the same plugin name.
  358. */
  359. private _addSchema(
  360. plugin: string,
  361. schema: ISettingRegistry.ISchema
  362. ): ISchemaValidator.IError[] | null {
  363. const composer = this._composer;
  364. const validator = this._validator;
  365. const validate = validator.getSchema('main');
  366. // Validate against the main schema.
  367. if (!(validate(schema) as boolean)) {
  368. return validate.errors as ISchemaValidator.IError[];
  369. }
  370. // Validate against the JSON schema meta-schema.
  371. if (!(validator.validateSchema(schema) as boolean)) {
  372. return validator.errors as ISchemaValidator.IError[];
  373. }
  374. // Remove if schema already exists.
  375. composer.removeSchema(plugin);
  376. validator.removeSchema(plugin);
  377. // Add schema to the validator and composer.
  378. composer.addSchema(schema, plugin);
  379. validator.addSchema(schema, plugin);
  380. return null;
  381. }
  382. private _composer = new Ajv({ useDefaults: true });
  383. private _validator = new Ajv();
  384. }
  385. /**
  386. * The default concrete implementation of a setting registry.
  387. */
  388. export class SettingRegistry {
  389. /**
  390. * Create a new setting registry.
  391. */
  392. constructor(options: SettingRegistry.IOptions) {
  393. this._connector = options.connector;
  394. this.validator = options.validator || new DefaultSchemaValidator();
  395. }
  396. /**
  397. * The schema of the setting registry.
  398. */
  399. readonly schema = Private.SCHEMA;
  400. /**
  401. * The schema validator used by the setting registry.
  402. */
  403. readonly validator: ISchemaValidator;
  404. /**
  405. * A signal that emits the name of a plugin when its settings change.
  406. */
  407. get pluginChanged(): ISignal<this, string> {
  408. return this._pluginChanged;
  409. }
  410. /**
  411. * Returns a list of plugin settings held in the registry.
  412. */
  413. get plugins(): ISettingRegistry.IPlugin[] {
  414. const plugins = this._plugins;
  415. return Object.keys(plugins).map(
  416. p => copy(plugins[p]) as ISettingRegistry.IPlugin
  417. );
  418. }
  419. /**
  420. * Get an individual setting.
  421. *
  422. * @param plugin - The name of the plugin whose settings are being retrieved.
  423. *
  424. * @param key - The name of the setting being retrieved.
  425. *
  426. * @returns A promise that resolves when the setting is retrieved.
  427. */
  428. get(
  429. plugin: string,
  430. key: string
  431. ): Promise<{ composite: JSONValue; user: JSONValue }> {
  432. const plugins = this._plugins;
  433. if (plugin in plugins) {
  434. const { composite, user } = plugins[plugin].data;
  435. const result = {
  436. composite: key in composite ? copy(composite[key]) : undefined,
  437. user: key in user ? copy(user[key]) : undefined
  438. };
  439. return Promise.resolve(result);
  440. }
  441. return this.load(plugin).then(() => this.get(plugin, key));
  442. }
  443. /**
  444. * Load a plugin's settings into the setting registry.
  445. *
  446. * @param plugin - The name of the plugin whose settings are being loaded.
  447. *
  448. * @returns A promise that resolves with a plugin settings object or rejects
  449. * if the plugin is not found.
  450. */
  451. load(plugin: string): Promise<ISettingRegistry.ISettings> {
  452. const plugins = this._plugins;
  453. const registry = this;
  454. // If the plugin exists, resolve.
  455. if (plugin in plugins) {
  456. const settings = new Settings({ plugin: plugins[plugin], registry });
  457. return Promise.resolve(settings);
  458. }
  459. // If the plugin needs to be loaded from the data connector, fetch.
  460. return this.reload(plugin);
  461. }
  462. /**
  463. * Reload a plugin's settings into the registry even if they already exist.
  464. *
  465. * @param plugin - The name of the plugin whose settings are being reloaded.
  466. *
  467. * @returns A promise that resolves with a plugin settings object or rejects
  468. * with a list of `ISchemaValidator.IError` objects if it fails.
  469. */
  470. reload(plugin: string): Promise<ISettingRegistry.ISettings> {
  471. const connector = this._connector;
  472. const plugins = this._plugins;
  473. const registry = this;
  474. // If the plugin needs to be loaded from the connector, fetch.
  475. return connector.fetch(plugin).then(data => {
  476. // Validate the response from the connector; populate `composite` field.
  477. try {
  478. this._validate(data);
  479. } catch (errors) {
  480. const output = [`Validating ${plugin} failed:`];
  481. (errors as ISchemaValidator.IError[]).forEach((error, index) => {
  482. const { dataPath, schemaPath, keyword, message } = error;
  483. output.push(`${index} - schema @ ${schemaPath}, data @ ${dataPath}`);
  484. output.push(`\t${keyword} ${message}`);
  485. });
  486. console.error(output.join('\n'));
  487. throw new Error(`Failed validating ${plugin}`);
  488. }
  489. // Emit that a plugin has changed.
  490. this._pluginChanged.emit(plugin);
  491. return new Settings({ plugin: plugins[plugin], registry });
  492. });
  493. }
  494. /**
  495. * Remove a single setting in the registry.
  496. *
  497. * @param plugin - The name of the plugin whose setting is being removed.
  498. *
  499. * @param key - The name of the setting being removed.
  500. *
  501. * @returns A promise that resolves when the setting is removed.
  502. */
  503. remove(plugin: string, key: string): Promise<void> {
  504. const plugins = this._plugins;
  505. if (!(plugin in plugins)) {
  506. return Promise.resolve(undefined);
  507. }
  508. const raw = json.parse(plugins[plugin].raw, null, true);
  509. // Delete both the value and any associated comment.
  510. delete raw[key];
  511. delete raw[`// ${key}`];
  512. plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], raw);
  513. return this._save(plugin);
  514. }
  515. /**
  516. * Set a single setting in the registry.
  517. *
  518. * @param plugin - The name of the plugin whose setting is being set.
  519. *
  520. * @param key - The name of the setting being set.
  521. *
  522. * @param value - The value of the setting being set.
  523. *
  524. * @returns A promise that resolves when the setting has been saved.
  525. *
  526. */
  527. set(plugin: string, key: string, value: JSONValue): Promise<void> {
  528. const plugins = this._plugins;
  529. if (!(plugin in plugins)) {
  530. return this.load(plugin).then(() => this.set(plugin, key, value));
  531. }
  532. const raw = json.parse(plugins[plugin].raw, null, true);
  533. plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], {
  534. ...raw,
  535. [key]: value
  536. });
  537. return this._save(plugin);
  538. }
  539. /**
  540. * Upload a plugin's settings.
  541. *
  542. * @param plugin - The name of the plugin whose settings are being set.
  543. *
  544. * @param raw - The raw plugin settings being uploaded.
  545. *
  546. * @returns A promise that resolves when the settings have been saved.
  547. */
  548. upload(plugin: string, raw: string): Promise<void> {
  549. const plugins = this._plugins;
  550. if (!(plugin in plugins)) {
  551. return this.load(plugin).then(() => this.upload(plugin, raw));
  552. }
  553. // Set the local copy.
  554. plugins[plugin].raw = raw;
  555. return this._save(plugin);
  556. }
  557. /**
  558. * Save a plugin in the registry.
  559. */
  560. private _save(plugin: string): Promise<void> {
  561. const plugins = this._plugins;
  562. if (!(plugin in plugins)) {
  563. const message = `${plugin} does not exist in setting registry.`;
  564. return Promise.reject(new Error(message));
  565. }
  566. try {
  567. this._validate(plugins[plugin]);
  568. } catch (errors) {
  569. const message = `${plugin} failed to validate; check console for errors.`;
  570. console.warn(`${plugin} validation errors:`, errors);
  571. return Promise.reject(new Error(message));
  572. }
  573. return this._connector.save(plugin, plugins[plugin].raw).then(() => {
  574. this._pluginChanged.emit(plugin);
  575. });
  576. }
  577. /**
  578. * Validate a plugin's data and schema, compose the `composite` data.
  579. */
  580. private _validate(plugin: ISettingRegistry.IPlugin): void {
  581. // Validate the user data and create the composite data.
  582. const errors = this.validator.validateData(plugin);
  583. if (errors) {
  584. throw errors;
  585. }
  586. // Set the local copy.
  587. this._plugins[plugin.id] = plugin;
  588. }
  589. private _connector: IDataConnector<ISettingRegistry.IPlugin, string>;
  590. private _pluginChanged = new Signal<this, string>(this);
  591. private _plugins: {
  592. [name: string]: ISettingRegistry.IPlugin;
  593. } = Object.create(null);
  594. }
  595. /**
  596. * A manager for a specific plugin's settings.
  597. */
  598. export class Settings implements ISettingRegistry.ISettings {
  599. /**
  600. * Instantiate a new plugin settings manager.
  601. */
  602. constructor(options: Settings.IOptions) {
  603. const { plugin } = options;
  604. this.plugin = plugin.id;
  605. this.registry = options.registry;
  606. this._composite = plugin.data.composite || {};
  607. this._raw = plugin.raw || '{ }';
  608. this._schema = plugin.schema || { type: 'object' };
  609. this._user = plugin.data.user || {};
  610. this.registry.pluginChanged.connect(
  611. this._onPluginChanged,
  612. this
  613. );
  614. }
  615. /**
  616. * A signal that emits when the plugin's settings have changed.
  617. */
  618. get changed(): ISignal<this, void> {
  619. return this._changed;
  620. }
  621. /**
  622. * Get the composite of user settings and extension defaults.
  623. */
  624. get composite(): JSONObject {
  625. return this._composite;
  626. }
  627. /**
  628. * Test whether the plugin settings manager disposed.
  629. */
  630. get isDisposed(): boolean {
  631. return this._isDisposed;
  632. }
  633. /**
  634. * Get the plugin settings schema.
  635. */
  636. get schema(): ISettingRegistry.ISchema {
  637. return this._schema;
  638. }
  639. /**
  640. * Get the plugin settings raw text value.
  641. */
  642. get raw(): string {
  643. return this._raw;
  644. }
  645. /**
  646. * Get the user settings.
  647. */
  648. get user(): JSONObject {
  649. return this._user;
  650. }
  651. /*
  652. * The plugin name.
  653. */
  654. readonly plugin: string;
  655. /**
  656. * The system registry instance used by the settings manager.
  657. */
  658. readonly registry: SettingRegistry;
  659. /**
  660. * Return the defaults in a commented JSON format.
  661. */
  662. annotatedDefaults(): string {
  663. return Private.annotatedDefaults(this._schema, this.plugin);
  664. }
  665. /**
  666. * Calculate the default value of a setting by iterating through the schema.
  667. *
  668. * @param key - The name of the setting whose default value is calculated.
  669. *
  670. * @returns A calculated default JSON value for a specific setting.
  671. */
  672. default(key: string): JSONValue | undefined {
  673. return Private.reifyDefault(this.schema, key);
  674. }
  675. /**
  676. * Dispose of the plugin settings resources.
  677. */
  678. dispose(): void {
  679. if (this._isDisposed) {
  680. return;
  681. }
  682. this._isDisposed = true;
  683. Signal.clearData(this);
  684. }
  685. /**
  686. * Get an individual setting.
  687. *
  688. * @param key - The name of the setting being retrieved.
  689. *
  690. * @returns The setting value.
  691. *
  692. * #### Notes
  693. * This method returns synchronously because it uses a cached copy of the
  694. * plugin settings that is synchronized with the registry.
  695. */
  696. get(key: string): { composite: JSONValue; user: JSONValue } {
  697. const { composite, user } = this;
  698. return {
  699. composite: key in composite ? copy(composite[key]) : undefined,
  700. user: key in user ? copy(user[key]) : undefined
  701. };
  702. }
  703. /**
  704. * Remove a single setting.
  705. *
  706. * @param key - The name of the setting being removed.
  707. *
  708. * @returns A promise that resolves when the setting is removed.
  709. *
  710. * #### Notes
  711. * This function is asynchronous because it writes to the setting registry.
  712. */
  713. remove(key: string): Promise<void> {
  714. return this.registry.remove(this.plugin, key);
  715. }
  716. /**
  717. * Save all of the plugin's user settings at once.
  718. */
  719. save(raw: string): Promise<void> {
  720. return this.registry.upload(this.plugin, raw);
  721. }
  722. /**
  723. * Set a single setting.
  724. *
  725. * @param key - The name of the setting being set.
  726. *
  727. * @param value - The value of the setting.
  728. *
  729. * @returns A promise that resolves when the setting has been saved.
  730. *
  731. * #### Notes
  732. * This function is asynchronous because it writes to the setting registry.
  733. */
  734. set(key: string, value: JSONValue): Promise<void> {
  735. return this.registry.set(this.plugin, key, value);
  736. }
  737. /**
  738. * Validates raw settings with comments.
  739. *
  740. * @param raw - The JSON with comments string being validated.
  741. *
  742. * @returns A list of errors or `null` if valid.
  743. */
  744. validate(raw: string): ISchemaValidator.IError[] | null {
  745. const data = { composite: {}, user: {} };
  746. const id = this.plugin;
  747. const schema = this._schema;
  748. const validator = this.registry.validator;
  749. return validator.validateData({ data, id, raw, schema }, false);
  750. }
  751. /**
  752. * Handle plugin changes in the setting registry.
  753. */
  754. private _onPluginChanged(sender: any, plugin: string): void {
  755. if (plugin === this.plugin) {
  756. const found = find(this.registry.plugins, p => p.id === plugin);
  757. if (!found) {
  758. return;
  759. }
  760. const { composite, user } = found.data;
  761. const { raw, schema } = found;
  762. this._composite = composite || {};
  763. this._raw = raw;
  764. this._schema = schema || { type: 'object' };
  765. this._user = user || {};
  766. this._changed.emit(undefined);
  767. }
  768. }
  769. private _changed = new Signal<this, void>(this);
  770. private _composite: JSONObject = Object.create(null);
  771. private _isDisposed = false;
  772. private _raw = '{ }';
  773. private _schema: ISettingRegistry.ISchema = Object.create(null);
  774. private _user: JSONObject = Object.create(null);
  775. }
  776. /**
  777. * A namespace for `SettingRegistry` statics.
  778. */
  779. export namespace SettingRegistry {
  780. /**
  781. * The instantiation options for a setting registry
  782. */
  783. export interface IOptions {
  784. /**
  785. * The data connector used by the setting registry.
  786. */
  787. connector: IDataConnector<ISettingRegistry.IPlugin, string>;
  788. /**
  789. * The validator used to enforce the settings JSON schema.
  790. */
  791. validator?: ISchemaValidator;
  792. }
  793. }
  794. /**
  795. * A namespace for `Settings` statics.
  796. */
  797. export namespace Settings {
  798. /**
  799. * The instantiation options for a `Settings` object.
  800. */
  801. export interface IOptions {
  802. /**
  803. * The setting values for a plugin.
  804. */
  805. plugin: ISettingRegistry.IPlugin;
  806. /**
  807. * The system registry instance used by the settings manager.
  808. */
  809. registry: SettingRegistry;
  810. }
  811. }
  812. /**
  813. * A namespace for private module data.
  814. */
  815. export namespace Private {
  816. /* tslint:disable */
  817. /**
  818. * The schema for settings.
  819. */
  820. export const SCHEMA: ISettingRegistry.ISchema = {
  821. $schema: 'http://json-schema.org/draft-06/schema',
  822. title: 'Jupyter Settings/Preferences Schema',
  823. description: 'Jupyter settings/preferences schema v0.1.0',
  824. type: 'object',
  825. additionalProperties: true,
  826. properties: {
  827. [ICON_CLASS_KEY]: { type: 'string', default: 'jp-FileIcon' },
  828. [ICON_LABEL_KEY]: { type: 'string', default: 'Plugin' }
  829. }
  830. };
  831. /* tslint:enable */
  832. /**
  833. * The default indentation level, uses spaces instead of tabs.
  834. */
  835. const indent = ' ';
  836. /**
  837. * Replacement text for schema properties missing a `description` field.
  838. */
  839. const nondescript = '[missing schema description]';
  840. /**
  841. * Replacement text for schema properties missing a `default` field.
  842. */
  843. const undefaulted = '[missing schema default]';
  844. /**
  845. * Replacement text for schema properties missing a `title` field.
  846. */
  847. const untitled = '[missing schema title]';
  848. /**
  849. * Returns an annotated (JSON with comments) version of a schema's defaults.
  850. */
  851. export function annotatedDefaults(
  852. schema: ISettingRegistry.ISchema,
  853. plugin: string
  854. ): string {
  855. const { description, properties, title } = schema;
  856. const keys = Object.keys(properties).sort((a, b) => a.localeCompare(b));
  857. const length = Math.max((description || nondescript).length, plugin.length);
  858. return [
  859. '{',
  860. prefix(`${title || untitled}`),
  861. prefix(plugin),
  862. prefix(description || nondescript),
  863. prefix(line(length)),
  864. '',
  865. keys.map(key => defaultDocumentedValue(schema, key)).join(',\n\n'),
  866. '}'
  867. ].join('\n');
  868. }
  869. /**
  870. * Returns an annotated (JSON with comments) version of a plugin's
  871. * setting data.
  872. */
  873. export function annotatedPlugin(
  874. plugin: ISettingRegistry.IPlugin,
  875. data: JSONObject
  876. ): string {
  877. const { description, title } = plugin.schema;
  878. const keys = Object.keys(data).sort((a, b) => a.localeCompare(b));
  879. const length = Math.max(
  880. (description || nondescript).length,
  881. plugin.id.length
  882. );
  883. return [
  884. '{',
  885. prefix(`${title || untitled}`),
  886. prefix(plugin.id),
  887. prefix(description || nondescript),
  888. prefix(line(length)),
  889. '',
  890. keys
  891. .map(key => documentedValue(plugin.schema, key, data[key]))
  892. .join(',\n\n'),
  893. '}'
  894. ].join('\n');
  895. }
  896. /**
  897. * Returns the default value-with-documentation-string for a
  898. * specific schema property.
  899. */
  900. function defaultDocumentedValue(
  901. schema: ISettingRegistry.ISchema,
  902. key: string
  903. ): string {
  904. const props = schema.properties && schema.properties[key];
  905. const description = (props && props['description']) || nondescript;
  906. const title = (props && props['title']) || untitled;
  907. const reified = reifyDefault(schema, key);
  908. const defaults =
  909. reified === undefined
  910. ? prefix(`"${key}": ${undefaulted}`)
  911. : prefix(`"${key}": ${JSON.stringify(reified, null, 2)}`, indent);
  912. return [
  913. prefix(`${title || untitled}`),
  914. prefix(description || nondescript),
  915. defaults
  916. ].join('\n');
  917. }
  918. /**
  919. * Returns a value-with-documentation-string for a specific schema property.
  920. */
  921. function documentedValue(
  922. schema: ISettingRegistry.ISchema,
  923. key: string,
  924. value: JSONValue
  925. ): string {
  926. const props = schema.properties && schema.properties[key];
  927. const description = (props && props['description']) || nondescript;
  928. const title = (props && props['title']) || untitled;
  929. const attribute = prefix(
  930. `"${key}": ${JSON.stringify(value, null, 2)}`,
  931. indent
  932. );
  933. return [prefix(title), prefix(description), attribute].join('\n');
  934. }
  935. /**
  936. * Returns a line of a specified length.
  937. */
  938. function line(length: number, ch = '*'): string {
  939. return new Array(length + 1).join(ch);
  940. }
  941. /**
  942. * Returns a documentation string with a comment prefix added on every line.
  943. */
  944. function prefix(source: string, pre = `${indent}\/\/ `): string {
  945. return pre + source.split('\n').join(`\n${pre}`);
  946. }
  947. /**
  948. * Create a fully extrapolated default value for a root key in a schema.
  949. */
  950. export function reifyDefault(
  951. schema: ISettingRegistry.ISchema,
  952. root?: string
  953. ): JSONValue | undefined {
  954. // If the property is at the root level, traverse its schema.
  955. schema = (root ? schema.properties[root] : schema) || {};
  956. // If the property has no default or is a primitive, return.
  957. if (!('default' in schema) || schema.type !== 'object') {
  958. return schema.default;
  959. }
  960. // Make a copy of the default value to populate.
  961. const result = JSONExt.deepCopy(schema.default);
  962. // Iterate through and populate each child property.
  963. for (let property in schema.properties || {}) {
  964. result[property] = reifyDefault(schema.properties[property]);
  965. }
  966. return result;
  967. }
  968. }