raweditor.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { CommandToolbarButton, Toolbar } from '@jupyterlab/apputils';
  4. import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
  5. import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
  6. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  7. import { ITranslator, nullTranslator } from '@jupyterlab/translation';
  8. import { CommandRegistry } from '@lumino/commands';
  9. import { Message } from '@lumino/messaging';
  10. import { ISignal, Signal } from '@lumino/signaling';
  11. import { BoxLayout, Widget } from '@lumino/widgets';
  12. import { createInspector } from './inspector';
  13. import { SplitPanel } from './splitpanel';
  14. /**
  15. * A class name added to all raw editors.
  16. */
  17. const RAW_EDITOR_CLASS = 'jp-SettingsRawEditor';
  18. /**
  19. * A class name added to the user settings editor.
  20. */
  21. const USER_CLASS = 'jp-SettingsRawEditor-user';
  22. /**
  23. * A class name added to the user editor when there are validation errors.
  24. */
  25. const ERROR_CLASS = 'jp-mod-error';
  26. /**
  27. * A raw JSON settings editor.
  28. */
  29. export class RawEditor extends SplitPanel {
  30. /**
  31. * Create a new plugin editor.
  32. */
  33. constructor(options: RawEditor.IOptions) {
  34. super({
  35. orientation: 'horizontal',
  36. renderer: SplitPanel.defaultRenderer,
  37. spacing: 1
  38. });
  39. const { commands, editorFactory, registry, translator } = options;
  40. this.registry = registry;
  41. this.translator = translator || nullTranslator;
  42. this._commands = commands;
  43. // Create read-only defaults editor.
  44. const defaults = (this._defaults = new CodeEditorWrapper({
  45. model: new CodeEditor.Model(),
  46. factory: editorFactory
  47. }));
  48. defaults.editor.model.value.text = '';
  49. defaults.editor.model.mimeType = 'text/javascript';
  50. defaults.editor.setOption('readOnly', true);
  51. // Create read-write user settings editor.
  52. const user = (this._user = new CodeEditorWrapper({
  53. model: new CodeEditor.Model(),
  54. factory: editorFactory,
  55. config: { lineNumbers: true }
  56. }));
  57. user.addClass(USER_CLASS);
  58. user.editor.model.mimeType = 'text/javascript';
  59. user.editor.model.value.changed.connect(this._onTextChanged, this);
  60. // Create and set up an inspector.
  61. this._inspector = createInspector(
  62. this,
  63. options.rendermime,
  64. this.translator
  65. );
  66. this.addClass(RAW_EDITOR_CLASS);
  67. // FIXME-TRANS: onSaveError must have an optional translator?
  68. this._onSaveError = options.onSaveError;
  69. this.addWidget(Private.defaultsEditor(defaults, this.translator));
  70. this.addWidget(
  71. Private.userEditor(user, this._toolbar, this._inspector, this.translator)
  72. );
  73. }
  74. /**
  75. * The setting registry used by the editor.
  76. */
  77. readonly registry: ISettingRegistry;
  78. /**
  79. * Whether the raw editor revert functionality is enabled.
  80. */
  81. get canRevert(): boolean {
  82. return this._canRevert;
  83. }
  84. /**
  85. * Whether the raw editor save functionality is enabled.
  86. */
  87. get canSave(): boolean {
  88. return this._canSave;
  89. }
  90. /**
  91. * Emits when the commands passed in at instantiation change.
  92. */
  93. get commandsChanged(): ISignal<any, string[]> {
  94. return this._commandsChanged;
  95. }
  96. /**
  97. * Tests whether the settings have been modified and need saving.
  98. */
  99. get isDirty(): boolean {
  100. return this._user.editor.model.value.text !== this._settings?.raw ?? '';
  101. }
  102. /**
  103. * The plugin settings being edited.
  104. */
  105. get settings(): ISettingRegistry.ISettings | null {
  106. return this._settings;
  107. }
  108. set settings(settings: ISettingRegistry.ISettings | null) {
  109. if (!settings && !this._settings) {
  110. return;
  111. }
  112. const samePlugin =
  113. settings && this._settings && settings.plugin === this._settings.plugin;
  114. if (samePlugin) {
  115. return;
  116. }
  117. const defaults = this._defaults;
  118. const user = this._user;
  119. // Disconnect old settings change handler.
  120. if (this._settings) {
  121. this._settings.changed.disconnect(this._onSettingsChanged, this);
  122. }
  123. if (settings) {
  124. this._settings = settings;
  125. this._settings.changed.connect(this._onSettingsChanged, this);
  126. this._onSettingsChanged();
  127. } else {
  128. this._settings = null;
  129. defaults.editor.model.value.text = '';
  130. user.editor.model.value.text = '';
  131. }
  132. this.update();
  133. }
  134. /**
  135. * Get the relative sizes of the two editor panels.
  136. */
  137. get sizes(): number[] {
  138. return this.relativeSizes();
  139. }
  140. set sizes(sizes: number[]) {
  141. this.setRelativeSizes(sizes);
  142. }
  143. /**
  144. * The inspectable source editor for user input.
  145. */
  146. get source(): CodeEditor.IEditor {
  147. return this._user.editor;
  148. }
  149. /**
  150. * Dispose of the resources held by the raw editor.
  151. */
  152. dispose(): void {
  153. if (this.isDisposed) {
  154. return;
  155. }
  156. super.dispose();
  157. this._defaults.dispose();
  158. this._user.dispose();
  159. }
  160. /**
  161. * Revert the editor back to original settings.
  162. */
  163. revert(): void {
  164. this._user.editor.model.value.text = this.settings?.raw ?? '';
  165. this._updateToolbar(false, false);
  166. }
  167. /**
  168. * Save the contents of the raw editor.
  169. */
  170. save(): Promise<void> {
  171. if (!this.isDirty || !this._settings) {
  172. return Promise.resolve(undefined);
  173. }
  174. const settings = this._settings;
  175. const source = this._user.editor.model.value.text;
  176. return settings
  177. .save(source)
  178. .then(() => {
  179. this._updateToolbar(false, false);
  180. })
  181. .catch(reason => {
  182. this._updateToolbar(true, false);
  183. this._onSaveError(reason, this.translator);
  184. });
  185. }
  186. /**
  187. * Handle `after-attach` messages.
  188. */
  189. protected onAfterAttach(msg: Message): void {
  190. Private.populateToolbar(this._commands, this._toolbar);
  191. this.update();
  192. }
  193. /**
  194. * Handle `'update-request'` messages.
  195. */
  196. protected onUpdateRequest(msg: Message): void {
  197. const settings = this._settings;
  198. const defaults = this._defaults;
  199. const user = this._user;
  200. if (settings) {
  201. defaults.editor.refresh();
  202. user.editor.refresh();
  203. }
  204. }
  205. /**
  206. * Handle text changes in the underlying editor.
  207. */
  208. private _onTextChanged(): void {
  209. const raw = this._user.editor.model.value.text;
  210. const settings = this._settings;
  211. this.removeClass(ERROR_CLASS);
  212. // If there are no settings loaded or there are no changes, bail.
  213. if (!settings || settings.raw === raw) {
  214. this._updateToolbar(false, false);
  215. return;
  216. }
  217. const errors = settings.validate(raw);
  218. if (errors) {
  219. this.addClass(ERROR_CLASS);
  220. this._updateToolbar(true, false);
  221. return;
  222. }
  223. this._updateToolbar(true, true);
  224. }
  225. /**
  226. * Handle updates to the settings.
  227. */
  228. private _onSettingsChanged(): void {
  229. const settings = this._settings;
  230. const defaults = this._defaults;
  231. const user = this._user;
  232. defaults.editor.model.value.text = settings?.annotatedDefaults() ?? '';
  233. user.editor.model.value.text = settings?.raw ?? '';
  234. }
  235. private _updateToolbar(revert = this._canRevert, save = this._canSave): void {
  236. const commands = this._commands;
  237. this._canRevert = revert;
  238. this._canSave = save;
  239. this._commandsChanged.emit([commands.revert, commands.save]);
  240. }
  241. protected translator: ITranslator;
  242. private _canRevert = false;
  243. private _canSave = false;
  244. private _commands: RawEditor.ICommandBundle;
  245. private _commandsChanged = new Signal<this, string[]>(this);
  246. private _defaults: CodeEditorWrapper;
  247. private _inspector: Widget;
  248. private _onSaveError: (reason: any, translator?: ITranslator) => void;
  249. private _settings: ISettingRegistry.ISettings | null = null;
  250. private _toolbar = new Toolbar<Widget>();
  251. private _user: CodeEditorWrapper;
  252. }
  253. /**
  254. * A namespace for `RawEditor` statics.
  255. */
  256. export namespace RawEditor {
  257. /**
  258. * The toolbar commands and registry for the setting editor toolbar.
  259. */
  260. export interface ICommandBundle {
  261. /**
  262. * The command registry.
  263. */
  264. registry: CommandRegistry;
  265. /**
  266. * The revert command ID.
  267. */
  268. revert: string;
  269. /**
  270. * The save command ID.
  271. */
  272. save: string;
  273. }
  274. /**
  275. * The instantiation options for a raw editor.
  276. */
  277. export interface IOptions {
  278. /**
  279. * The toolbar commands and registry for the setting editor toolbar.
  280. */
  281. commands: ICommandBundle;
  282. /**
  283. * The editor factory used by the raw editor.
  284. */
  285. editorFactory: CodeEditor.Factory;
  286. /**
  287. * A function the raw editor calls on save errors.
  288. */
  289. onSaveError: (reason: any) => void;
  290. /**
  291. * The setting registry used by the editor.
  292. */
  293. registry: ISettingRegistry;
  294. /**
  295. * The optional MIME renderer to use for rendering debug messages.
  296. */
  297. rendermime?: IRenderMimeRegistry;
  298. /**
  299. * The application language translator.
  300. */
  301. translator?: ITranslator;
  302. }
  303. }
  304. /**
  305. * A namespace for private module data.
  306. */
  307. namespace Private {
  308. /**
  309. * Returns the wrapped setting defaults editor.
  310. */
  311. export function defaultsEditor(
  312. editor: Widget,
  313. translator?: ITranslator
  314. ): Widget {
  315. translator = translator || nullTranslator;
  316. const trans = translator.load('jupyterlab');
  317. const widget = new Widget();
  318. const layout = (widget.layout = new BoxLayout({ spacing: 0 }));
  319. const banner = new Widget();
  320. const bar = new Toolbar();
  321. const defaultTitle = trans.__('System Defaults');
  322. banner.node.innerText = defaultTitle;
  323. bar.insertItem(0, 'banner', banner);
  324. layout.addWidget(bar);
  325. layout.addWidget(editor);
  326. return widget;
  327. }
  328. /**
  329. * Populate the raw editor toolbar.
  330. */
  331. export function populateToolbar(
  332. commands: RawEditor.ICommandBundle,
  333. toolbar: Toolbar<Widget>
  334. ): void {
  335. const { registry, revert, save } = commands;
  336. toolbar.addItem('spacer', Toolbar.createSpacerItem());
  337. // Note the button order. The rationale here is that no matter what state
  338. // the toolbar is in, the relative location of the revert button in the
  339. // toolbar remains the same.
  340. [revert, save].forEach(name => {
  341. const item = new CommandToolbarButton({ commands: registry, id: name });
  342. toolbar.addItem(name, item);
  343. });
  344. }
  345. /**
  346. * Returns the wrapped user overrides editor.
  347. */
  348. export function userEditor(
  349. editor: Widget,
  350. toolbar: Toolbar<Widget>,
  351. inspector: Widget,
  352. translator?: ITranslator
  353. ): Widget {
  354. translator = translator || nullTranslator;
  355. const trans = translator.load('jupyterlab');
  356. const userTitle = trans.__('User Preferences');
  357. const widget = new Widget();
  358. const layout = (widget.layout = new BoxLayout({ spacing: 0 }));
  359. const banner = new Widget();
  360. banner.node.innerText = userTitle;
  361. toolbar.insertItem(0, 'banner', banner);
  362. layout.addWidget(toolbar);
  363. layout.addWidget(editor);
  364. layout.addWidget(inspector);
  365. return widget;
  366. }
  367. }