index.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JupyterLab, JupyterLabPlugin
  5. } from '@jupyterlab/application';
  6. import {
  7. showDialog, showErrorMessage, Spinner, Dialog, ICommandPalette, IMainMenu
  8. } from '@jupyterlab/apputils';
  9. import {
  10. IChangedArgs
  11. } from '@jupyterlab/coreutils';
  12. import {
  13. renameDialog, DocumentManager, IDocumentManager
  14. } from '@jupyterlab/docmanager';
  15. import {
  16. DocumentRegistry
  17. } from '@jupyterlab/docregistry';
  18. import {
  19. Contents, Kernel
  20. } from '@jupyterlab/services';
  21. import {
  22. IDisposable
  23. } from '@phosphor/disposable';
  24. /**
  25. * The command IDs used by the document manager plugin.
  26. */
  27. namespace CommandIDs {
  28. export
  29. const clone = 'docmanager:clone';
  30. export
  31. const close = 'docmanager:close';
  32. export
  33. const closeAllFiles = 'docmanager:close-all-files';
  34. export
  35. const createFrom = 'docmanager:create-from';
  36. export
  37. const deleteFile = 'docmanager:delete-file';
  38. export
  39. const newUntitled = 'docmanager:new-untitled';
  40. export
  41. const open = 'docmanager:open';
  42. export
  43. const rename = 'docmanager:rename';
  44. export
  45. const restoreCheckpoint = 'docmanager:restore-checkpoint';
  46. export
  47. const save = 'docmanager:save';
  48. export
  49. const saveAs = 'docmanager:save-as';
  50. }
  51. /**
  52. * The default document manager provider.
  53. */
  54. const plugin: JupyterLabPlugin<IDocumentManager> = {
  55. id: '@jupyterlab/docmanager-extension:plugin',
  56. provides: IDocumentManager,
  57. requires: [ICommandPalette, IMainMenu],
  58. activate: (app: JupyterLab, palette: ICommandPalette, mainMenu: IMainMenu): IDocumentManager => {
  59. const manager = app.serviceManager;
  60. const contexts = new WeakSet<DocumentRegistry.Context>();
  61. const opener: DocumentManager.IWidgetOpener = {
  62. open: widget => {
  63. if (!widget.id) {
  64. widget.id = `document-manager-${++Private.id}`;
  65. }
  66. widget.title.dataset = {
  67. 'type': 'document-title',
  68. ...widget.title.dataset
  69. };
  70. if (!widget.isAttached) {
  71. app.shell.addToMainArea(widget);
  72. // Add a loading spinner, and remove it when the widget is ready.
  73. let spinner = new Spinner();
  74. widget.node.appendChild(spinner.node);
  75. widget.ready.then(() => { widget.node.removeChild(spinner.node); });
  76. }
  77. app.shell.activateById(widget.id);
  78. // Handle dirty state for open documents.
  79. let context = docManager.contextForWidget(widget);
  80. if (!contexts.has(context)) {
  81. handleContext(app, context);
  82. contexts.add(context);
  83. }
  84. }
  85. };
  86. const registry = app.docRegistry;
  87. const docManager = new DocumentManager({ registry, manager, opener });
  88. // Register the file operations commands.
  89. addCommands(app, docManager, palette, opener);
  90. return docManager;
  91. }
  92. };
  93. /**
  94. * Export the plugin as default.
  95. */
  96. export default plugin;
  97. /**
  98. * Add the file operations commands to the application's command registry.
  99. */
  100. function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICommandPalette, opener: DocumentManager.IWidgetOpener): void {
  101. const { commands } = app;
  102. const category = 'File Operations';
  103. const isEnabled = () => {
  104. const { currentWidget } = app.shell;
  105. return !!(currentWidget && docManager.contextForWidget(currentWidget));
  106. };
  107. commands.addCommand(CommandIDs.close, {
  108. label: 'Close',
  109. execute: () => {
  110. if (app.shell.currentWidget) {
  111. app.shell.currentWidget.close();
  112. }
  113. }
  114. });
  115. commands.addCommand(CommandIDs.closeAllFiles, {
  116. label: 'Close All',
  117. execute: () => { app.shell.closeAll(); }
  118. });
  119. commands.addCommand(CommandIDs.deleteFile, {
  120. execute: args => {
  121. const path = typeof args['path'] === 'undefined' ? ''
  122. : args['path'] as string;
  123. if (!path) {
  124. const command = CommandIDs.deleteFile;
  125. throw new Error(`A non-empty path is required for ${command}.`);
  126. }
  127. return docManager.deleteFile(path);
  128. }
  129. });
  130. commands.addCommand(CommandIDs.newUntitled, {
  131. execute: args => {
  132. const errorTitle = args['error'] as string || 'Error';
  133. const path = typeof args['path'] === 'undefined' ? ''
  134. : args['path'] as string;
  135. let options: Partial<Contents.ICreateOptions> = {
  136. type: args['type'] as Contents.ContentType,
  137. path
  138. };
  139. if (args['type'] === 'file') {
  140. options.ext = args['ext'] as string || '.txt';
  141. }
  142. return docManager.services.contents.newUntitled(options)
  143. .catch(error => showErrorMessage(errorTitle, error));
  144. },
  145. label: args => args['label'] as string || `New ${args['type'] as string}`
  146. });
  147. commands.addCommand(CommandIDs.open, {
  148. execute: args => {
  149. const path = typeof args['path'] === 'undefined' ? ''
  150. : args['path'] as string;
  151. const factory = args['factory'] as string || void 0;
  152. const kernel = args['kernel'] as Kernel.IModel || void 0;
  153. return docManager.services.contents.get(path, { content: false })
  154. .then(() => docManager.openOrReveal(path, factory, kernel));
  155. },
  156. icon: args => args['icon'] as string || '',
  157. label: args => (args['label'] || args['factory']) as string,
  158. mnemonic: args => args['mnemonic'] as number || -1
  159. });
  160. commands.addCommand(CommandIDs.restoreCheckpoint, {
  161. label: 'Revert to Checkpoint',
  162. caption: 'Revert contents to previous checkpoint',
  163. isEnabled,
  164. execute: () => {
  165. if (isEnabled()) {
  166. let context = docManager.contextForWidget(app.shell.currentWidget);
  167. if (context.model.readOnly) {
  168. return context.revert();
  169. }
  170. return context.restoreCheckpoint().then(() => context.revert());
  171. }
  172. }
  173. });
  174. commands.addCommand(CommandIDs.save, {
  175. label: 'Save',
  176. caption: 'Save and create checkpoint',
  177. isEnabled,
  178. execute: () => {
  179. if (isEnabled()) {
  180. let context = docManager.contextForWidget(app.shell.currentWidget);
  181. if (context.model.readOnly) {
  182. return showDialog({
  183. title: 'Cannot Save',
  184. body: 'Document is read-only',
  185. buttons: [Dialog.okButton()]
  186. });
  187. }
  188. return context.save().then(() => context.createCheckpoint());
  189. }
  190. }
  191. });
  192. commands.addCommand(CommandIDs.saveAs, {
  193. label: 'Save As...',
  194. caption: 'Save with new path and create checkpoint',
  195. isEnabled,
  196. execute: () => {
  197. if (isEnabled()) {
  198. let context = docManager.contextForWidget(app.shell.currentWidget);
  199. return context.saveAs().then(() => context.createCheckpoint());
  200. }
  201. }
  202. });
  203. commands.addCommand(CommandIDs.rename, {
  204. isVisible: () => {
  205. const widget = app.shell.currentWidget;
  206. if (!widget) {
  207. return;
  208. }
  209. // Find the context for the widget.
  210. let context = docManager.contextForWidget(widget);
  211. return context !== null;
  212. },
  213. execute: () => {
  214. const widget = app.shell.currentWidget;
  215. if (!widget) {
  216. return;
  217. }
  218. // Find the context for the widget.
  219. let context = docManager.contextForWidget(widget);
  220. if (context) {
  221. return renameDialog(docManager, context.path);
  222. }
  223. },
  224. label: 'Rename'
  225. });
  226. commands.addCommand(CommandIDs.clone, {
  227. isVisible: () => {
  228. const widget = app.shell.currentWidget;
  229. if (!widget) {
  230. return;
  231. }
  232. // Find the context for the widget.
  233. let context = docManager.contextForWidget(widget);
  234. return context !== null;
  235. },
  236. execute: () => {
  237. const widget = app.shell.currentWidget;
  238. if (!widget) {
  239. return;
  240. }
  241. // Clone the widget.
  242. let child = docManager.cloneWidget(widget);
  243. if (child) {
  244. opener.open(child);
  245. }
  246. },
  247. label: 'New View into File'
  248. });
  249. app.contextMenu.addItem({
  250. command: CommandIDs.rename,
  251. selector: '[data-type="document-title"]',
  252. rank: 1
  253. });
  254. app.contextMenu.addItem({
  255. command: CommandIDs.clone,
  256. selector: '[data-type="document-title"]',
  257. rank: 2
  258. });
  259. [
  260. CommandIDs.save,
  261. CommandIDs.restoreCheckpoint,
  262. CommandIDs.saveAs,
  263. CommandIDs.clone,
  264. CommandIDs.close,
  265. CommandIDs.closeAllFiles
  266. ].forEach(command => { palette.addItem({ command, category }); });
  267. }
  268. /**
  269. * Handle dirty state for a context.
  270. */
  271. function handleContext(app: JupyterLab, context: DocumentRegistry.Context): void {
  272. let disposable: IDisposable | null = null;
  273. let onStateChanged = (sender: any, args: IChangedArgs<any>) => {
  274. if (args.name === 'dirty') {
  275. if (args.newValue === true) {
  276. if (!disposable) {
  277. disposable = app.setDirty();
  278. }
  279. } else if (disposable) {
  280. disposable.dispose();
  281. disposable = null;
  282. }
  283. }
  284. };
  285. context.ready.then(() => {
  286. context.model.stateChanged.connect(onStateChanged);
  287. if (context.model.dirty) {
  288. disposable = app.setDirty();
  289. }
  290. });
  291. context.disposed.connect(() => {
  292. if (disposable) {
  293. disposable.dispose();
  294. }
  295. });
  296. }
  297. /**
  298. * A namespace for private module data.
  299. */
  300. namespace Private {
  301. /**
  302. * A counter for unique IDs.
  303. */
  304. export
  305. let id = 0;
  306. }