plugin.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. each
  5. } from 'phosphor/lib/algorithm/iteration';
  6. import {
  7. DisposableSet
  8. } from 'phosphor/lib/core/disposable';
  9. import {
  10. FocusTracker
  11. } from 'phosphor/lib/ui/focustracker';
  12. import {
  13. Menu
  14. } from 'phosphor/lib/ui/menu';
  15. import {
  16. Widget
  17. } from 'phosphor/lib/ui/widget';
  18. import {
  19. JupyterLab, JupyterLabPlugin
  20. } from '../application';
  21. import {
  22. ICommandPalette
  23. } from '../commandpalette';
  24. import {
  25. DocumentManager
  26. } from '../docmanager';
  27. import {
  28. IDocumentRegistry
  29. } from '../docregistry';
  30. import {
  31. IMainMenu
  32. } from '../mainmenu';
  33. import {
  34. IServiceManager
  35. } from '../services';
  36. import {
  37. FileBrowserModel, FileBrowserWidget, IPathTracker, IWidgetOpener
  38. } from './';
  39. /**
  40. * The default file browser provider.
  41. */
  42. export
  43. const fileBrowserProvider: JupyterLabPlugin<IPathTracker> = {
  44. id: 'jupyter.services.file-browser',
  45. provides: IPathTracker,
  46. requires: [IServiceManager, IDocumentRegistry, IMainMenu, ICommandPalette],
  47. activate: activateFileBrowser,
  48. autoStart: true
  49. };
  50. /**
  51. * The map of command ids used by the file browser.
  52. */
  53. const cmdIds = {
  54. save: 'file-operations:save',
  55. restoreCheckpoint: 'file-operations:restore-checkpoint',
  56. saveAs: 'file-operations:saveAs',
  57. close: 'file-operations:close',
  58. closeAllFiles: 'file-operations:closeAllFiles',
  59. open: 'file-operations:open',
  60. showBrowser: 'file-browser:activate',
  61. hideBrowser: 'file-browser:hide',
  62. toggleBrowser: 'file-browser:toggle'
  63. };
  64. /**
  65. * Activate the file browser.
  66. */
  67. function activateFileBrowser(app: JupyterLab, manager: IServiceManager, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette): IPathTracker {
  68. let id = 0;
  69. let tracker = new FocusTracker<Widget>();
  70. let opener: IWidgetOpener = {
  71. open: widget => {
  72. if (!widget.id) {
  73. widget.id = `document-manager-${++id}`;
  74. }
  75. if (!widget.isAttached) {
  76. app.shell.addToMainArea(widget);
  77. tracker.add(widget);
  78. }
  79. app.shell.activateMain(widget.id);
  80. }
  81. };
  82. let { commands, keymap } = app;
  83. let docManager = new DocumentManager({ registry, manager, opener });
  84. let fbModel = new FileBrowserModel({ manager });
  85. let fbWidget = new FileBrowserWidget({
  86. commands: commands,
  87. keymap: keymap,
  88. manager: docManager,
  89. model: fbModel,
  90. opener: opener
  91. });
  92. let category = 'File Operations';
  93. let creators = registry.creators;
  94. let creatorCmds: { [key: string]: DisposableSet } = Object.create(null);
  95. let addCreator = (name: string) => {
  96. let disposables = creatorCmds[name] = new DisposableSet();
  97. let command = Private.commandForName(name);
  98. disposables.add(commands.addCommand(command, {
  99. execute: () => {
  100. fbWidget.createFrom(name);
  101. },
  102. label: `New ${name}`
  103. }));
  104. disposables.add(palette.addItem({ command, category }));
  105. };
  106. each(creators, creator => {
  107. addCreator(creator.name);
  108. });
  109. // Add a context menu to the dir listing.
  110. let node = fbWidget.node.getElementsByClassName('jp-DirListing-content')[0];
  111. node.addEventListener('contextmenu', (event: MouseEvent) => {
  112. event.preventDefault();
  113. let path = fbWidget.pathForClick(event) || '';
  114. let ext = '.' + path.split('.').pop();
  115. let widgetNames = registry.listWidgetFactories(ext);
  116. let prefix = `file-browser-contextmenu-${++Private.id}`;
  117. let openWith: Menu = null;
  118. if (path && widgetNames.length > 1) {
  119. let disposables = new DisposableSet();
  120. let command: string;
  121. openWith = new Menu({ commands, keymap });
  122. openWith.title.label = 'Open With...';
  123. openWith.disposed.connect(() => disposables.dispose());
  124. for (let widgetName of widgetNames) {
  125. command = `${prefix}:${widgetName}`;
  126. disposables.add(commands.addCommand(command, {
  127. execute: () => fbWidget.openPath(path, widgetName),
  128. label: widgetName
  129. }));
  130. openWith.addItem({ command });
  131. }
  132. }
  133. let menu = createContextMenu(fbWidget, openWith);
  134. menu.open(event.clientX, event.clientY);
  135. });
  136. addCommands(app, tracker, fbWidget, docManager);
  137. [
  138. cmdIds.save,
  139. cmdIds.restoreCheckpoint,
  140. cmdIds.saveAs,
  141. cmdIds.close,
  142. cmdIds.closeAllFiles,
  143. ].forEach(command => palette.addItem({ command, category }));
  144. let menu = createMenu(app, Object.keys(creatorCmds));
  145. mainMenu.addMenu(menu, {rank: 1});
  146. fbWidget.title.label = 'Files';
  147. fbWidget.id = 'file-browser';
  148. app.shell.addToLeftArea(fbWidget, { rank: 40 });
  149. app.commands.execute(cmdIds.showBrowser, void 0);
  150. // Handle fileCreator items as they are added.
  151. registry.changed.connect((sender, args) => {
  152. if (args.type === 'fileCreator') {
  153. menu.dispose();
  154. let name = args.name;
  155. if (args.change === 'added') {
  156. addCreator(name);
  157. } else {
  158. creatorCmds[name].dispose();
  159. delete creatorCmds[name];
  160. }
  161. menu = createMenu(app, Object.keys(creatorCmds));
  162. mainMenu.addMenu(menu, {rank: 1});
  163. }
  164. });
  165. return fbModel;
  166. }
  167. /**
  168. * Add the filebrowser commands to the application's command registry.
  169. */
  170. function addCommands(app: JupyterLab, tracker: FocusTracker<Widget>, fbWidget: FileBrowserWidget, docManager: DocumentManager): void {
  171. let commands = app.commands;
  172. let fbModel = fbWidget.model;
  173. commands.addCommand(cmdIds.save, {
  174. label: 'Save',
  175. caption: 'Save and create checkpoint',
  176. execute: () => {
  177. if (tracker.currentWidget) {
  178. let context = docManager.contextForWidget(tracker.currentWidget);
  179. return context.save().then(() => {
  180. return context.createCheckpoint();
  181. });
  182. }
  183. }
  184. });
  185. commands.addCommand(cmdIds.restoreCheckpoint, {
  186. label: 'Revert to Checkpoint',
  187. caption: 'Revert contents to previous checkpoint',
  188. execute: () => {
  189. if (tracker.currentWidget) {
  190. let context = docManager.contextForWidget(tracker.currentWidget);
  191. context.restoreCheckpoint().then(() => {
  192. context.revert();
  193. });
  194. }
  195. }
  196. });
  197. commands.addCommand(cmdIds.saveAs, {
  198. label: 'Save As...',
  199. caption: 'Save with new path and create checkpoint',
  200. execute: () => {
  201. if (tracker.currentWidget) {
  202. let context = docManager.contextForWidget(tracker.currentWidget);
  203. return context.saveAs().then(() => {
  204. return context.createCheckpoint();
  205. }).then(() => {
  206. return fbModel.refresh();
  207. });
  208. }
  209. }
  210. });
  211. commands.addCommand(cmdIds.open, {
  212. execute: args => {
  213. let path = args['path'] as string;
  214. fbWidget.openPath(path);
  215. }
  216. });
  217. commands.addCommand(cmdIds.close, {
  218. label: 'Close',
  219. execute: () => {
  220. if (tracker.currentWidget) {
  221. tracker.currentWidget.close();
  222. }
  223. }
  224. });
  225. commands.addCommand(cmdIds.closeAllFiles, {
  226. label: 'Close All',
  227. execute: () => {
  228. each(tracker.widgets, widget => widget.close());
  229. }
  230. });
  231. commands.addCommand(cmdIds.showBrowser, {
  232. execute: () => app.shell.activateLeft(fbWidget.id)
  233. });
  234. commands.addCommand(cmdIds.hideBrowser, {
  235. execute: () => {
  236. if (!fbWidget.isHidden) {
  237. app.shell.collapseLeft();
  238. }
  239. }
  240. });
  241. commands.addCommand(cmdIds.toggleBrowser, {
  242. execute: () => {
  243. if (fbWidget.isHidden) {
  244. commands.execute(cmdIds.showBrowser, void 0);
  245. } else {
  246. commands.execute(cmdIds.hideBrowser, void 0);
  247. }
  248. }
  249. });
  250. }
  251. /**
  252. * Create a top level menu for the file browser.
  253. */
  254. function createMenu(app: JupyterLab, creatorCmds: string[]): Menu {
  255. let { commands, keymap } = app;
  256. let menu = new Menu({ commands, keymap });
  257. menu.title.label = 'File';
  258. creatorCmds.forEach(name => {
  259. menu.addItem({ command: Private.commandForName(name) });
  260. });
  261. [
  262. cmdIds.save,
  263. cmdIds.restoreCheckpoint,
  264. cmdIds.saveAs,
  265. cmdIds.close,
  266. cmdIds.closeAllFiles,
  267. ].forEach(command => { menu.addItem({ command }); });
  268. return menu;
  269. }
  270. /**
  271. * Create a context menu for the file browser listing.
  272. */
  273. function createContextMenu(fbWidget: FileBrowserWidget, openWith: Menu): Menu {
  274. let { commands, keymap } = fbWidget;
  275. let menu = new Menu({ commands, keymap });
  276. let prefix = `file-browser-${++Private.id}`;
  277. let disposables = new DisposableSet();
  278. let command: string;
  279. // // Remove all the commands associated with this menu upon disposal.
  280. menu.disposed.connect(() => { disposables.dispose(); });
  281. command = `${prefix}:open`;
  282. disposables.add(commands.addCommand(command, {
  283. execute: () => fbWidget.open(),
  284. icon: 'fa fa-folder-open-o',
  285. label: 'Open',
  286. mnemonic: 0
  287. }));
  288. menu.addItem({ command });
  289. if (openWith) {
  290. menu.addItem({ type: 'submenu', menu: openWith });
  291. }
  292. command = `${prefix}:rename`;
  293. disposables.add(commands.addCommand(command, {
  294. execute: () => fbWidget.rename(),
  295. icon: 'fa fa-edit',
  296. label: 'Rename',
  297. mnemonic: 0
  298. }));
  299. menu.addItem({ command });
  300. command = `${prefix}:delete`;
  301. disposables.add(commands.addCommand(command, {
  302. execute: () => fbWidget.delete(),
  303. icon: 'fa fa-remove',
  304. label: 'Delete',
  305. mnemonic: 0
  306. }));
  307. menu.addItem({ command });
  308. command = `${prefix}:duplicate`;
  309. disposables.add(commands.addCommand(command, {
  310. execute: () => fbWidget.duplicate(),
  311. icon: 'fa fa-copy',
  312. label: 'Duplicate'
  313. }));
  314. menu.addItem({ command });
  315. command = `${prefix}:cut`;
  316. disposables.add(commands.addCommand(command, {
  317. execute: () => fbWidget.cut(),
  318. icon: 'fa fa-cut',
  319. label: 'Cut'
  320. }));
  321. menu.addItem({ command });
  322. command = `${prefix}:copy`;
  323. disposables.add(commands.addCommand(command, {
  324. execute: () => fbWidget.copy(),
  325. icon: 'fa fa-copy',
  326. label: 'Copy',
  327. mnemonic: 0
  328. }));
  329. menu.addItem({ command });
  330. command = `${prefix}:paste`;
  331. disposables.add(commands.addCommand(command, {
  332. execute: () => fbWidget.paste(),
  333. icon: 'fa fa-paste',
  334. label: 'Paste',
  335. mnemonic: 0
  336. }));
  337. menu.addItem({ command });
  338. command = `${prefix}:download`;
  339. disposables.add(commands.addCommand(command, {
  340. execute: () => fbWidget.download(),
  341. icon: 'fa fa-download',
  342. label: 'Download'
  343. }));
  344. menu.addItem({ command });
  345. command = `${prefix}:shutdown`;
  346. disposables.add(commands.addCommand(command, {
  347. execute: () => fbWidget.shutdownKernels(),
  348. icon: 'fa fa-stop-circle-o',
  349. label: 'Shutdown Kernel'
  350. }));
  351. menu.addItem({ command });
  352. menu.disposed.connect(() => disposables.dispose());
  353. return menu;
  354. }
  355. /**
  356. * A namespace for private data.
  357. */
  358. namespace Private {
  359. /**
  360. * The ID counter prefix for new commands.
  361. *
  362. * #### Notes
  363. * Even though the commands are disposed when the menus are disposed,
  364. * in order to guarantee there are no race conditions, each set of commands
  365. * is prefixed.
  366. */
  367. export
  368. let id = 0;
  369. /**
  370. * Get the command for a name.
  371. */
  372. export
  373. function commandForName(name: string): string {
  374. name = name.split(' ').join('-').toLocaleLowerCase();
  375. return `file-operations:new-${name}`;
  376. }
  377. }