index.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ILabShell,
  5. ILayoutRestorer,
  6. JupyterFrontEnd,
  7. JupyterFrontEndPlugin
  8. } from '@jupyterlab/application';
  9. import {
  10. Clipboard,
  11. InstanceTracker,
  12. MainAreaWidget,
  13. ToolbarButton
  14. } from '@jupyterlab/apputils';
  15. import {
  16. IStateDB,
  17. PageConfig,
  18. PathExt,
  19. URLExt,
  20. ISettingRegistry
  21. } from '@jupyterlab/coreutils';
  22. import { IDocumentManager } from '@jupyterlab/docmanager';
  23. import {
  24. FileBrowserModel,
  25. FileBrowser,
  26. FileUploadStatus,
  27. IFileBrowserFactory
  28. } from '@jupyterlab/filebrowser';
  29. import { Launcher } from '@jupyterlab/launcher';
  30. import { Contents } from '@jupyterlab/services';
  31. import { IStatusBar } from '@jupyterlab/statusbar';
  32. import { IIterator, map, reduce, toArray } from '@phosphor/algorithm';
  33. import { CommandRegistry } from '@phosphor/commands';
  34. import { Message } from '@phosphor/messaging';
  35. import { Menu } from '@phosphor/widgets';
  36. /**
  37. * The command IDs used by the file browser plugin.
  38. */
  39. namespace CommandIDs {
  40. export const copy = 'filebrowser:copy';
  41. export const copyDownloadLink = 'filebrowser:copy-download-link';
  42. // For main browser only.
  43. export const createLauncher = 'filebrowser:create-main-launcher';
  44. export const cut = 'filebrowser:cut';
  45. export const del = 'filebrowser:delete';
  46. export const download = 'filebrowser:download';
  47. export const duplicate = 'filebrowser:duplicate';
  48. // For main browser only.
  49. export const hideBrowser = 'filebrowser:hide-main';
  50. export const navigate = 'filebrowser:navigate';
  51. export const open = 'filebrowser:open';
  52. export const openBrowserTab = 'filebrowser:open-browser-tab';
  53. export const paste = 'filebrowser:paste';
  54. export const createNewDirectory = 'filebrowser:create-new-directory';
  55. export const rename = 'filebrowser:rename';
  56. // For main browser only.
  57. export const share = 'filebrowser:share-main';
  58. // For main browser only.
  59. export const copyPath = 'filebrowser:copy-path';
  60. export const showBrowser = 'filebrowser:activate';
  61. export const shutdown = 'filebrowser:shutdown';
  62. // For main browser only.
  63. export const toggleBrowser = 'filebrowser:toggle-main';
  64. }
  65. /**
  66. * The default file browser extension.
  67. */
  68. const browser: JupyterFrontEndPlugin<void> = {
  69. activate: activateBrowser,
  70. id: '@jupyterlab/filebrowser-extension:browser',
  71. requires: [
  72. IFileBrowserFactory,
  73. IDocumentManager,
  74. ILabShell,
  75. ILayoutRestorer,
  76. ISettingRegistry
  77. ],
  78. autoStart: true
  79. };
  80. /**
  81. * The default file browser factory provider.
  82. */
  83. const factory: JupyterFrontEndPlugin<IFileBrowserFactory> = {
  84. activate: activateFactory,
  85. id: '@jupyterlab/filebrowser-extension:factory',
  86. provides: IFileBrowserFactory,
  87. requires: [IDocumentManager, IStateDB]
  88. };
  89. /**
  90. * The default file browser share-file plugin
  91. *
  92. * This extension adds a "Copy Shareable Link" command that generates a copy-
  93. * pastable URL. This url can be used to open a particular file in JupyterLab,
  94. * handy for emailing links or bookmarking for reference.
  95. *
  96. * If you need to change how this link is generated (for instance, to copy a
  97. * /user-redirect URL for JupyterHub), disable this plugin and replace it
  98. * with another implementation.
  99. */
  100. const shareFile: JupyterFrontEndPlugin<void> = {
  101. activate: activateShareFile,
  102. id: '@jupyterlab/filebrowser-extension:share-file',
  103. requires: [IFileBrowserFactory],
  104. autoStart: true
  105. };
  106. /**
  107. * A plugin providing file upload status.
  108. */
  109. export const fileUploadStatus: JupyterFrontEndPlugin<void> = {
  110. id: '@jupyterlab/filebrowser-extension:file-upload-status',
  111. autoStart: true,
  112. requires: [IStatusBar, IFileBrowserFactory],
  113. activate: (
  114. app: JupyterFrontEnd,
  115. statusBar: IStatusBar,
  116. browser: IFileBrowserFactory
  117. ) => {
  118. const item = new FileUploadStatus({
  119. tracker: browser.tracker
  120. });
  121. statusBar.registerStatusItem(
  122. '@jupyterlab/filebrowser-extension:file-upload-status',
  123. {
  124. item,
  125. align: 'middle',
  126. isActive: () => {
  127. return !!item.model && item.model.items.length > 0;
  128. },
  129. activeStateChanged: item.model.stateChanged
  130. }
  131. );
  132. }
  133. };
  134. /**
  135. * The file browser namespace token.
  136. */
  137. const namespace = 'filebrowser';
  138. /**
  139. * Export the plugins as default.
  140. */
  141. const plugins: JupyterFrontEndPlugin<any>[] = [
  142. factory,
  143. browser,
  144. shareFile,
  145. fileUploadStatus
  146. ];
  147. export default plugins;
  148. /**
  149. * Activate the file browser factory provider.
  150. */
  151. function activateFactory(
  152. app: JupyterFrontEnd,
  153. docManager: IDocumentManager,
  154. state: IStateDB
  155. ): IFileBrowserFactory {
  156. const { commands } = app;
  157. const tracker = new InstanceTracker<FileBrowser>({ namespace });
  158. const createFileBrowser = (
  159. id: string,
  160. options: IFileBrowserFactory.IOptions = {}
  161. ) => {
  162. const model = new FileBrowserModel({
  163. manager: docManager,
  164. driveName: options.driveName || '',
  165. refreshInterval: options.refreshInterval,
  166. state: options.state === null ? null : options.state || state
  167. });
  168. const widget = new FileBrowser({
  169. id,
  170. model
  171. });
  172. // Add a launcher toolbar item.
  173. let launcher = new ToolbarButton({
  174. iconClassName: 'jp-AddIcon',
  175. onClick: () => {
  176. return Private.createLauncher(commands, widget);
  177. },
  178. tooltip: 'New Launcher'
  179. });
  180. widget.toolbar.insertItem(0, 'launch', launcher);
  181. // Track the newly created file browser.
  182. void tracker.add(widget);
  183. return widget;
  184. };
  185. const defaultBrowser = createFileBrowser('filebrowser');
  186. return { createFileBrowser, defaultBrowser, tracker };
  187. }
  188. /**
  189. * Activate the default file browser in the sidebar.
  190. */
  191. function activateBrowser(
  192. app: JupyterFrontEnd,
  193. factory: IFileBrowserFactory,
  194. docManager: IDocumentManager,
  195. labShell: ILabShell,
  196. restorer: ILayoutRestorer,
  197. settingRegistry: ISettingRegistry
  198. ): void {
  199. const browser = factory.defaultBrowser;
  200. const { commands } = app;
  201. // Let the application restorer track the primary file browser (that is
  202. // automatically created) for restoration of application state (e.g. setting
  203. // the file browser as the current side bar widget).
  204. //
  205. // All other file browsers created by using the factory function are
  206. // responsible for their own restoration behavior, if any.
  207. restorer.add(browser, namespace);
  208. addCommands(app, factory, labShell, docManager);
  209. browser.title.iconClass = 'jp-FolderIcon jp-SideBar-tabIcon';
  210. browser.title.caption = 'File Browser';
  211. labShell.add(browser, 'left', { rank: 100 });
  212. // If the layout is a fresh session without saved data, open file browser.
  213. void labShell.restored.then(layout => {
  214. if (layout.fresh) {
  215. void commands.execute(CommandIDs.showBrowser, void 0);
  216. }
  217. });
  218. void Promise.all([app.restored, browser.model.restored]).then(() => {
  219. function maybeCreate() {
  220. // Create a launcher if there are no open items.
  221. if (labShell.isEmpty('main')) {
  222. void Private.createLauncher(commands, browser);
  223. }
  224. }
  225. // When layout is modified, create a launcher if there are no open items.
  226. labShell.layoutModified.connect(() => {
  227. maybeCreate();
  228. });
  229. let navigateToCurrentDirectory: boolean = false;
  230. void settingRegistry
  231. .load('@jupyterlab/filebrowser-extension:browser')
  232. .then(settings => {
  233. settings.changed.connect(settings => {
  234. navigateToCurrentDirectory = settings.get(
  235. 'navigateToCurrentDirectory'
  236. ).composite as boolean;
  237. });
  238. navigateToCurrentDirectory = settings.get('navigateToCurrentDirectory')
  239. .composite as boolean;
  240. });
  241. // Whether to automatically navigate to a document's current directory
  242. labShell.currentChanged.connect((_, change) => {
  243. if (navigateToCurrentDirectory && change.newValue) {
  244. const { newValue } = change;
  245. const context = docManager.contextForWidget(newValue);
  246. if (context) {
  247. const { path } = context;
  248. Private.navigateToPath(path, factory)
  249. .then(() => {
  250. labShell.currentWidget.activate();
  251. })
  252. .catch((reason: any) => {
  253. console.warn(
  254. `${CommandIDs.navigate} failed to open: ${path}`,
  255. reason
  256. );
  257. });
  258. }
  259. }
  260. });
  261. maybeCreate();
  262. });
  263. }
  264. function activateShareFile(
  265. app: JupyterFrontEnd,
  266. factory: IFileBrowserFactory
  267. ): void {
  268. const { commands } = app;
  269. const { tracker } = factory;
  270. commands.addCommand(CommandIDs.share, {
  271. execute: () => {
  272. const widget = tracker.currentWidget;
  273. if (!widget) {
  274. return;
  275. }
  276. const path = encodeURI(widget.selectedItems().next().path);
  277. Clipboard.copyToSystem(URLExt.join(PageConfig.getTreeUrl(), path));
  278. },
  279. isVisible: () =>
  280. tracker.currentWidget &&
  281. toArray(tracker.currentWidget.selectedItems()).length === 1,
  282. iconClass: 'jp-MaterialIcon jp-LinkIcon',
  283. label: 'Copy Shareable Link'
  284. });
  285. }
  286. /**
  287. * Add the main file browser commands to the application's command registry.
  288. */
  289. function addCommands(
  290. app: JupyterFrontEnd,
  291. factory: IFileBrowserFactory,
  292. labShell: ILabShell,
  293. docManager: IDocumentManager
  294. ): void {
  295. const registry = app.docRegistry;
  296. const { commands } = app;
  297. const { defaultBrowser: browser } = factory;
  298. const { tracker } = factory;
  299. commands.addCommand(CommandIDs.del, {
  300. execute: () => {
  301. const widget = tracker.currentWidget;
  302. if (widget) {
  303. return widget.delete();
  304. }
  305. },
  306. iconClass: 'jp-MaterialIcon jp-CloseIcon',
  307. label: 'Delete',
  308. mnemonic: 0
  309. });
  310. commands.addCommand(CommandIDs.copy, {
  311. execute: () => {
  312. const widget = tracker.currentWidget;
  313. if (widget) {
  314. return widget.copy();
  315. }
  316. },
  317. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  318. label: 'Copy',
  319. mnemonic: 0
  320. });
  321. commands.addCommand(CommandIDs.cut, {
  322. execute: () => {
  323. const widget = tracker.currentWidget;
  324. if (widget) {
  325. return widget.cut();
  326. }
  327. },
  328. iconClass: 'jp-MaterialIcon jp-CutIcon',
  329. label: 'Cut'
  330. });
  331. commands.addCommand(CommandIDs.download, {
  332. execute: () => {
  333. const widget = tracker.currentWidget;
  334. if (widget) {
  335. return widget.download();
  336. }
  337. },
  338. iconClass: 'jp-MaterialIcon jp-DownloadIcon',
  339. label: 'Download'
  340. });
  341. commands.addCommand(CommandIDs.duplicate, {
  342. execute: () => {
  343. const widget = tracker.currentWidget;
  344. if (widget) {
  345. return widget.duplicate();
  346. }
  347. },
  348. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  349. label: 'Duplicate'
  350. });
  351. commands.addCommand(CommandIDs.hideBrowser, {
  352. execute: () => {
  353. const widget = tracker.currentWidget;
  354. if (widget && !widget.isHidden) {
  355. labShell.collapseLeft();
  356. }
  357. }
  358. });
  359. commands.addCommand(CommandIDs.navigate, {
  360. execute: args => {
  361. const path = (args.path as string) || '';
  362. return Private.navigateToPath(path, factory)
  363. .then(model => {
  364. if (model.type === 'directory') {
  365. return commands.execute(CommandIDs.showBrowser, { path });
  366. }
  367. return commands.execute('docmanager:open', { path });
  368. })
  369. .catch((reason: any) => {
  370. console.warn(
  371. `${CommandIDs.navigate} failed to open: ${path}`,
  372. reason
  373. );
  374. });
  375. }
  376. });
  377. commands.addCommand(CommandIDs.open, {
  378. execute: args => {
  379. const factory = (args['factory'] as string) || void 0;
  380. const widget = tracker.currentWidget;
  381. if (!widget) {
  382. return;
  383. }
  384. return Promise.all(
  385. toArray(
  386. map(widget.selectedItems(), item => {
  387. if (item.type === 'directory') {
  388. return widget.model.cd(item.name);
  389. }
  390. return commands.execute('docmanager:open', {
  391. factory: factory,
  392. path: item.path
  393. });
  394. })
  395. )
  396. );
  397. },
  398. iconClass: args => {
  399. const factory = (args['factory'] as string) || void 0;
  400. if (factory) {
  401. // if an explicit factory is passed...
  402. const ft = registry.getFileType(factory);
  403. if (ft) {
  404. // ...set an icon if the factory name corresponds to a file type name...
  405. return ft.iconClass;
  406. } else {
  407. // ...or leave the icon blank
  408. return '';
  409. }
  410. } else {
  411. return 'jp-MaterialIcon jp-OpenFolderIcon';
  412. }
  413. },
  414. label: args => (args['label'] || args['factory'] || 'Open') as string,
  415. mnemonic: 0
  416. });
  417. commands.addCommand(CommandIDs.openBrowserTab, {
  418. execute: () => {
  419. const widget = tracker.currentWidget;
  420. if (!widget) {
  421. return;
  422. }
  423. return Promise.all(
  424. toArray(
  425. map(widget.selectedItems(), item => {
  426. return commands.execute('docmanager:open-browser-tab', {
  427. path: item.path
  428. });
  429. })
  430. )
  431. );
  432. },
  433. iconClass: 'jp-MaterialIcon jp-AddIcon',
  434. label: 'Open in New Browser Tab',
  435. mnemonic: 0
  436. });
  437. commands.addCommand(CommandIDs.copyDownloadLink, {
  438. execute: () => {
  439. const widget = tracker.currentWidget;
  440. if (!widget) {
  441. return;
  442. }
  443. return widget.model.manager.services.contents
  444. .getDownloadUrl(widget.selectedItems().next().path)
  445. .then(url => {
  446. Clipboard.copyToSystem(url);
  447. });
  448. },
  449. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  450. label: 'Copy Download Link',
  451. mnemonic: 0
  452. });
  453. commands.addCommand(CommandIDs.paste, {
  454. execute: () => {
  455. const widget = tracker.currentWidget;
  456. if (widget) {
  457. return widget.paste();
  458. }
  459. },
  460. iconClass: 'jp-MaterialIcon jp-PasteIcon',
  461. label: 'Paste',
  462. mnemonic: 0
  463. });
  464. commands.addCommand(CommandIDs.createNewDirectory, {
  465. execute: () => {
  466. const widget = tracker.currentWidget;
  467. if (widget) {
  468. return widget.createNewDirectory();
  469. }
  470. },
  471. iconClass: 'jp-MaterialIcon jp-NewFolderIcon',
  472. label: 'New Folder'
  473. });
  474. commands.addCommand(CommandIDs.rename, {
  475. execute: args => {
  476. const widget = tracker.currentWidget;
  477. if (widget) {
  478. return widget.rename();
  479. }
  480. },
  481. iconClass: 'jp-MaterialIcon jp-EditIcon',
  482. label: 'Rename',
  483. mnemonic: 0
  484. });
  485. commands.addCommand(CommandIDs.copyPath, {
  486. execute: () => {
  487. const widget = tracker.currentWidget;
  488. if (!widget) {
  489. return;
  490. }
  491. const item = widget.selectedItems().next();
  492. if (!item) {
  493. return;
  494. }
  495. Clipboard.copyToSystem(item.path);
  496. },
  497. isVisible: () =>
  498. tracker.currentWidget &&
  499. tracker.currentWidget.selectedItems().next !== undefined,
  500. iconClass: 'jp-MaterialIcon jp-FileIcon',
  501. label: 'Copy Path'
  502. });
  503. commands.addCommand(CommandIDs.showBrowser, {
  504. execute: args => {
  505. const path = (args.path as string) || '';
  506. const browserForPath = Private.getBrowserForPath(path, factory);
  507. // Check for browser not found
  508. if (!browserForPath) {
  509. return;
  510. }
  511. // Shortcut if we are using the main file browser
  512. if (browser === browserForPath) {
  513. labShell.activateById(browser.id);
  514. return;
  515. } else {
  516. const areas: ILabShell.Area[] = ['left', 'right'];
  517. for (let area of areas) {
  518. const it = labShell.widgets(area);
  519. let widget = it.next();
  520. while (widget) {
  521. if (widget.contains(browserForPath)) {
  522. labShell.activateById(widget.id);
  523. return;
  524. }
  525. widget = it.next();
  526. }
  527. }
  528. }
  529. }
  530. });
  531. commands.addCommand(CommandIDs.shutdown, {
  532. execute: () => {
  533. const widget = tracker.currentWidget;
  534. if (widget) {
  535. return widget.shutdownKernels();
  536. }
  537. },
  538. iconClass: 'jp-MaterialIcon jp-StopIcon',
  539. label: 'Shut Down Kernel'
  540. });
  541. commands.addCommand(CommandIDs.toggleBrowser, {
  542. execute: () => {
  543. if (browser.isHidden) {
  544. return commands.execute(CommandIDs.showBrowser, void 0);
  545. }
  546. return commands.execute(CommandIDs.hideBrowser, void 0);
  547. }
  548. });
  549. commands.addCommand(CommandIDs.createLauncher, {
  550. label: 'New Launcher',
  551. execute: () => Private.createLauncher(commands, browser)
  552. });
  553. /**
  554. * A menu widget that dynamically populates with different widget factories
  555. * based on current filebrowser selection.
  556. */
  557. class OpenWithMenu extends Menu {
  558. protected onBeforeAttach(msg: Message): void {
  559. // clear the current menu items
  560. this.clearItems();
  561. // get the widget factories that could be used to open all of the items
  562. // in the current filebrowser selection
  563. let factories = OpenWithMenu._intersection(
  564. map(tracker.currentWidget.selectedItems(), i => {
  565. return OpenWithMenu._getFactories(i);
  566. })
  567. );
  568. if (factories) {
  569. // make new menu items from the widget factories
  570. factories.forEach(factory => {
  571. this.addItem({
  572. args: { factory: factory },
  573. command: CommandIDs.open
  574. });
  575. });
  576. }
  577. super.onBeforeAttach(msg);
  578. }
  579. static _getFactories(item: Contents.IModel): Array<string> {
  580. let factories = registry
  581. .preferredWidgetFactories(item.path)
  582. .map(f => f.name);
  583. const notebookFactory = registry.getWidgetFactory('notebook').name;
  584. if (
  585. item.type === 'notebook' &&
  586. factories.indexOf(notebookFactory) === -1
  587. ) {
  588. factories.unshift(notebookFactory);
  589. }
  590. return factories;
  591. }
  592. static _intersection<T>(iter: IIterator<Array<T>>): Set<T> | void {
  593. // pop the first element of iter
  594. let first = iter.next();
  595. // first will be undefined if iter is empty
  596. if (!first) {
  597. return;
  598. }
  599. // "initialize" the intersection from first
  600. let isect = new Set(first);
  601. // reduce over the remaining elements of iter
  602. return reduce(
  603. iter,
  604. (isect, subarr) => {
  605. // filter out all elements not present in both isect and subarr,
  606. // accumulate result in new set
  607. return new Set(subarr.filter(x => isect.has(x)));
  608. },
  609. isect
  610. );
  611. }
  612. }
  613. // matches anywhere on filebrowser
  614. const selectorContent = '.jp-DirListing-content';
  615. // matches all filebrowser items
  616. const selectorItem = '.jp-DirListing-item[data-isdir]';
  617. // matches only non-directory items
  618. const selectorNotDir = '.jp-DirListing-item[data-isdir="false"]';
  619. // If the user did not click on any file, we still want to show paste and new folder,
  620. // so target the content rather than an item.
  621. app.contextMenu.addItem({
  622. command: CommandIDs.createNewDirectory,
  623. selector: selectorContent,
  624. rank: 1
  625. });
  626. app.contextMenu.addItem({
  627. command: CommandIDs.paste,
  628. selector: selectorContent,
  629. rank: 2
  630. });
  631. app.contextMenu.addItem({
  632. command: CommandIDs.open,
  633. selector: selectorItem,
  634. rank: 1
  635. });
  636. const openWith = new OpenWithMenu({ commands });
  637. openWith.title.label = 'Open With';
  638. app.contextMenu.addItem({
  639. type: 'submenu',
  640. submenu: openWith,
  641. selector: selectorNotDir,
  642. rank: 2
  643. });
  644. app.contextMenu.addItem({
  645. command: CommandIDs.openBrowserTab,
  646. selector: selectorNotDir,
  647. rank: 3
  648. });
  649. app.contextMenu.addItem({
  650. command: CommandIDs.rename,
  651. selector: selectorItem,
  652. rank: 4
  653. });
  654. app.contextMenu.addItem({
  655. command: CommandIDs.del,
  656. selector: selectorItem,
  657. rank: 5
  658. });
  659. app.contextMenu.addItem({
  660. command: CommandIDs.cut,
  661. selector: selectorItem,
  662. rank: 6
  663. });
  664. app.contextMenu.addItem({
  665. command: CommandIDs.copy,
  666. selector: selectorNotDir,
  667. rank: 7
  668. });
  669. app.contextMenu.addItem({
  670. command: CommandIDs.duplicate,
  671. selector: selectorNotDir,
  672. rank: 8
  673. });
  674. app.contextMenu.addItem({
  675. command: CommandIDs.download,
  676. selector: selectorNotDir,
  677. rank: 9
  678. });
  679. app.contextMenu.addItem({
  680. command: CommandIDs.shutdown,
  681. selector: selectorNotDir,
  682. rank: 10
  683. });
  684. app.contextMenu.addItem({
  685. command: CommandIDs.share,
  686. selector: selectorItem,
  687. rank: 11
  688. });
  689. app.contextMenu.addItem({
  690. command: CommandIDs.copyPath,
  691. selector: selectorItem,
  692. rank: 12
  693. });
  694. app.contextMenu.addItem({
  695. command: CommandIDs.copyDownloadLink,
  696. selector: selectorNotDir,
  697. rank: 13
  698. });
  699. }
  700. /**
  701. * A namespace for private module data.
  702. */
  703. namespace Private {
  704. /**
  705. * Create a launcher for a given filebrowser widget.
  706. */
  707. export function createLauncher(
  708. commands: CommandRegistry,
  709. browser: FileBrowser
  710. ): Promise<MainAreaWidget<Launcher>> {
  711. const { model } = browser;
  712. return commands
  713. .execute('launcher:create', { cwd: model.path })
  714. .then((launcher: MainAreaWidget<Launcher>) => {
  715. model.pathChanged.connect(() => {
  716. launcher.content.cwd = model.path;
  717. }, launcher);
  718. return launcher;
  719. });
  720. }
  721. /**
  722. * Get browser object given file path.
  723. */
  724. export function getBrowserForPath(
  725. path: string,
  726. factory: IFileBrowserFactory
  727. ): FileBrowser {
  728. const { defaultBrowser: browser, tracker } = factory;
  729. const driveName = browser.model.manager.services.contents.driveName(path);
  730. if (driveName) {
  731. let browserForPath = tracker.find(
  732. _path => _path.model.driveName === driveName
  733. );
  734. if (!browserForPath) {
  735. // warn that no filebrowser could be found for this driveName
  736. console.warn(
  737. `${CommandIDs.navigate} failed to find filebrowser for path: ${path}`
  738. );
  739. return;
  740. }
  741. return browserForPath;
  742. }
  743. // if driveName is empty, assume the main filebrowser
  744. return browser;
  745. }
  746. /**
  747. * Navigate to a path.
  748. */
  749. export function navigateToPath(
  750. path: string,
  751. factory: IFileBrowserFactory
  752. ): Promise<Contents.IModel> {
  753. const browserForPath = Private.getBrowserForPath(path, factory);
  754. const { services } = browserForPath.model.manager;
  755. const localPath = services.contents.localPath(path);
  756. return services.ready
  757. .then(() => services.contents.get(path, { content: false }))
  758. .then(value => {
  759. const { model } = browserForPath;
  760. const { restored } = model;
  761. if (value.type === 'directory') {
  762. return restored.then(() => {
  763. model.cd(`/${localPath}`);
  764. return value;
  765. });
  766. }
  767. return restored.then(() => {
  768. model.cd(`/${PathExt.dirname(localPath)}`);
  769. return value;
  770. });
  771. });
  772. }
  773. }