index.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  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. IWindowResolver,
  13. MainAreaWidget,
  14. ToolbarButton
  15. } from '@jupyterlab/apputils';
  16. import {
  17. IStateDB,
  18. PageConfig,
  19. PathExt,
  20. URLExt,
  21. ISettingRegistry
  22. } from '@jupyterlab/coreutils';
  23. import { IDocumentManager } from '@jupyterlab/docmanager';
  24. import {
  25. FileBrowserModel,
  26. FileBrowser,
  27. FileUploadStatus,
  28. IFileBrowserFactory
  29. } from '@jupyterlab/filebrowser';
  30. import { Launcher } from '@jupyterlab/launcher';
  31. import { Contents } from '@jupyterlab/services';
  32. import { IStatusBar } from '@jupyterlab/statusbar';
  33. import { IIterator, map, reduce, toArray } from '@phosphor/algorithm';
  34. import { CommandRegistry } from '@phosphor/commands';
  35. import { Message } from '@phosphor/messaging';
  36. import { Menu } from '@phosphor/widgets';
  37. /**
  38. * The command IDs used by the file browser plugin.
  39. */
  40. namespace CommandIDs {
  41. export const copy = 'filebrowser:copy';
  42. export const copyDownloadLink = 'filebrowser:copy-download-link';
  43. // For main browser only.
  44. export const createLauncher = 'filebrowser:create-main-launcher';
  45. export const cut = 'filebrowser:cut';
  46. export const del = 'filebrowser:delete';
  47. export const download = 'filebrowser:download';
  48. export const duplicate = 'filebrowser:duplicate';
  49. // For main browser only.
  50. export const hideBrowser = 'filebrowser:hide-main';
  51. export const navigate = 'filebrowser:navigate';
  52. export const open = 'filebrowser:open';
  53. export const openBrowserTab = 'filebrowser:open-browser-tab';
  54. export const paste = 'filebrowser:paste';
  55. export const createNewDirectory = 'filebrowser:create-new-directory';
  56. export const rename = 'filebrowser:rename';
  57. // For main browser only.
  58. export const share = 'filebrowser:share-main';
  59. // For main browser only.
  60. export const copyPath = 'filebrowser:copy-path';
  61. export const showBrowser = 'filebrowser:activate';
  62. export const shutdown = 'filebrowser:shutdown';
  63. // For main browser only.
  64. export const toggleBrowser = 'filebrowser:toggle-main';
  65. }
  66. /**
  67. * The default file browser extension.
  68. */
  69. const browser: JupyterFrontEndPlugin<void> = {
  70. activate: activateBrowser,
  71. id: '@jupyterlab/filebrowser-extension:browser',
  72. requires: [
  73. IFileBrowserFactory,
  74. IDocumentManager,
  75. ILabShell,
  76. ILayoutRestorer,
  77. ISettingRegistry
  78. ],
  79. autoStart: true
  80. };
  81. /**
  82. * The default file browser factory provider.
  83. */
  84. const factory: JupyterFrontEndPlugin<IFileBrowserFactory> = {
  85. activate: activateFactory,
  86. id: '@jupyterlab/filebrowser-extension:factory',
  87. provides: IFileBrowserFactory,
  88. requires: [IDocumentManager, IStateDB]
  89. };
  90. /**
  91. * The default file browser share-file plugin
  92. *
  93. * This extension adds a "Copy Shareable Link" command that generates a copy-
  94. * pastable URL. This url can be used to open a particular file in JupyterLab,
  95. * handy for emailing links or bookmarking for reference.
  96. *
  97. * If you need to change how this link is generated (for instance, to copy a
  98. * /user-redirect URL for JupyterHub), disable this plugin and replace it
  99. * with another implementation.
  100. */
  101. const shareFile: JupyterFrontEndPlugin<void> = {
  102. activate: activateShareFile,
  103. id: '@jupyterlab/filebrowser-extension:share-file',
  104. requires: [IFileBrowserFactory, IWindowResolver],
  105. autoStart: true
  106. };
  107. /**
  108. * A plugin providing file upload status.
  109. */
  110. export const fileUploadStatus: JupyterFrontEndPlugin<void> = {
  111. id: '@jupyterlab/filebrowser-extension:file-upload-status',
  112. autoStart: true,
  113. requires: [IStatusBar, IFileBrowserFactory],
  114. activate: (
  115. app: JupyterFrontEnd,
  116. statusBar: IStatusBar,
  117. browser: IFileBrowserFactory
  118. ) => {
  119. const item = new FileUploadStatus({
  120. tracker: browser.tracker
  121. });
  122. statusBar.registerStatusItem(
  123. '@jupyterlab/filebrowser-extension:file-upload-status',
  124. {
  125. item,
  126. align: 'middle',
  127. isActive: () => {
  128. return !!item.model && item.model.items.length > 0;
  129. },
  130. activeStateChanged: item.model.stateChanged
  131. }
  132. );
  133. }
  134. };
  135. /**
  136. * The file browser namespace token.
  137. */
  138. const namespace = 'filebrowser';
  139. /**
  140. * Export the plugins as default.
  141. */
  142. const plugins: JupyterFrontEndPlugin<any>[] = [
  143. factory,
  144. browser,
  145. shareFile,
  146. fileUploadStatus
  147. ];
  148. export default plugins;
  149. /**
  150. * Activate the file browser factory provider.
  151. */
  152. function activateFactory(
  153. app: JupyterFrontEnd,
  154. docManager: IDocumentManager,
  155. state: IStateDB
  156. ): IFileBrowserFactory {
  157. const { commands } = app;
  158. const tracker = new InstanceTracker<FileBrowser>({ namespace });
  159. const createFileBrowser = (
  160. id: string,
  161. options: IFileBrowserFactory.IOptions = {}
  162. ) => {
  163. const model = new FileBrowserModel({
  164. manager: docManager,
  165. driveName: options.driveName || '',
  166. refreshInterval: options.refreshInterval,
  167. state: options.state === null ? null : options.state || state
  168. });
  169. const widget = new FileBrowser({
  170. id,
  171. model
  172. });
  173. // Add a launcher toolbar item.
  174. let launcher = new ToolbarButton({
  175. iconClassName: 'jp-AddIcon jp-Icon jp-Icon-16',
  176. onClick: () => {
  177. return Private.createLauncher(commands, widget);
  178. },
  179. tooltip: 'New Launcher'
  180. });
  181. widget.toolbar.insertItem(0, 'launch', launcher);
  182. // Track the newly created file browser.
  183. tracker.add(widget);
  184. return widget;
  185. };
  186. const defaultBrowser = createFileBrowser('filebrowser');
  187. return { createFileBrowser, defaultBrowser, tracker };
  188. }
  189. /**
  190. * Activate the default file browser in the sidebar.
  191. */
  192. function activateBrowser(
  193. app: JupyterFrontEnd,
  194. factory: IFileBrowserFactory,
  195. docManager: IDocumentManager,
  196. labShell: ILabShell,
  197. restorer: ILayoutRestorer,
  198. settingRegistry: ISettingRegistry
  199. ): void {
  200. const browser = factory.defaultBrowser;
  201. const { commands } = app;
  202. // Let the application restorer track the primary file browser (that is
  203. // automatically created) for restoration of application state (e.g. setting
  204. // the file browser as the current side bar widget).
  205. //
  206. // All other file browsers created by using the factory function are
  207. // responsible for their own restoration behavior, if any.
  208. restorer.add(browser, namespace);
  209. addCommands(app, factory, labShell, docManager);
  210. browser.title.iconClass = 'jp-FolderIcon jp-SideBar-tabIcon';
  211. browser.title.caption = 'File Browser';
  212. labShell.add(browser, 'left', { rank: 100 });
  213. // If the layout is a fresh session without saved data, open file browser.
  214. labShell.restored.then(layout => {
  215. if (layout.fresh) {
  216. commands.execute(CommandIDs.showBrowser, void 0);
  217. }
  218. });
  219. Promise.all([app.restored, browser.model.restored]).then(() => {
  220. function maybeCreate() {
  221. // Create a launcher if there are no open items.
  222. if (labShell.isEmpty('main')) {
  223. Private.createLauncher(commands, browser);
  224. }
  225. }
  226. // When layout is modified, create a launcher if there are no open items.
  227. labShell.layoutModified.connect(() => {
  228. maybeCreate();
  229. });
  230. let navigateToCurrentDirectory: boolean = false;
  231. settingRegistry
  232. .load('@jupyterlab/filebrowser-extension:browser')
  233. .then(settings => {
  234. settings.changed.connect(settings => {
  235. navigateToCurrentDirectory = settings.get(
  236. 'navigateToCurrentDirectory'
  237. ).composite as boolean;
  238. });
  239. navigateToCurrentDirectory = settings.get('navigateToCurrentDirectory')
  240. .composite as boolean;
  241. });
  242. // Whether to automatically navigate to a document's current directory
  243. labShell.currentChanged.connect((_, change) => {
  244. if (navigateToCurrentDirectory && change.newValue) {
  245. const { newValue } = change;
  246. const context = docManager.contextForWidget(newValue);
  247. if (context) {
  248. const { path } = context;
  249. Private.navigateToPath(path, factory)
  250. .then(() => {
  251. docManager.findWidget(path).activate();
  252. })
  253. .catch((reason: any) => {
  254. console.warn(
  255. `${CommandIDs.navigate} failed to open: ${path}`,
  256. reason
  257. );
  258. });
  259. }
  260. }
  261. });
  262. maybeCreate();
  263. });
  264. }
  265. function activateShareFile(
  266. app: JupyterFrontEnd,
  267. factory: IFileBrowserFactory,
  268. resolver: IWindowResolver
  269. ): void {
  270. const { commands } = app;
  271. const { tracker } = factory;
  272. commands.addCommand(CommandIDs.share, {
  273. execute: () => {
  274. const widget = tracker.currentWidget;
  275. if (!widget) {
  276. return;
  277. }
  278. const path = encodeURI(widget.selectedItems().next().path);
  279. const tree = PageConfig.getTreeUrl({ workspace: resolver.name });
  280. Clipboard.copyToSystem(URLExt.join(tree, path));
  281. },
  282. isVisible: () =>
  283. tracker.currentWidget &&
  284. toArray(tracker.currentWidget.selectedItems()).length === 1,
  285. iconClass: 'jp-MaterialIcon jp-LinkIcon',
  286. label: 'Copy Shareable Link'
  287. });
  288. }
  289. /**
  290. * Add the main file browser commands to the application's command registry.
  291. */
  292. function addCommands(
  293. app: JupyterFrontEnd,
  294. factory: IFileBrowserFactory,
  295. labShell: ILabShell,
  296. docManager: IDocumentManager
  297. ): void {
  298. const registry = app.docRegistry;
  299. const { commands } = app;
  300. const { defaultBrowser: browser } = factory;
  301. const { tracker } = factory;
  302. commands.addCommand(CommandIDs.del, {
  303. execute: () => {
  304. const widget = tracker.currentWidget;
  305. if (widget) {
  306. return widget.delete();
  307. }
  308. },
  309. iconClass: 'jp-MaterialIcon jp-CloseIcon',
  310. label: 'Delete',
  311. mnemonic: 0
  312. });
  313. commands.addCommand(CommandIDs.copy, {
  314. execute: () => {
  315. const widget = tracker.currentWidget;
  316. if (widget) {
  317. return widget.copy();
  318. }
  319. },
  320. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  321. label: 'Copy',
  322. mnemonic: 0
  323. });
  324. commands.addCommand(CommandIDs.cut, {
  325. execute: () => {
  326. const widget = tracker.currentWidget;
  327. if (widget) {
  328. return widget.cut();
  329. }
  330. },
  331. iconClass: 'jp-MaterialIcon jp-CutIcon',
  332. label: 'Cut'
  333. });
  334. commands.addCommand(CommandIDs.download, {
  335. execute: () => {
  336. const widget = tracker.currentWidget;
  337. if (widget) {
  338. return widget.download();
  339. }
  340. },
  341. iconClass: 'jp-MaterialIcon jp-DownloadIcon',
  342. label: 'Download'
  343. });
  344. commands.addCommand(CommandIDs.duplicate, {
  345. execute: () => {
  346. const widget = tracker.currentWidget;
  347. if (widget) {
  348. return widget.duplicate();
  349. }
  350. },
  351. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  352. label: 'Duplicate'
  353. });
  354. commands.addCommand(CommandIDs.hideBrowser, {
  355. execute: () => {
  356. const widget = tracker.currentWidget;
  357. if (widget && !widget.isHidden) {
  358. labShell.collapseLeft();
  359. }
  360. }
  361. });
  362. commands.addCommand(CommandIDs.navigate, {
  363. execute: args => {
  364. const path = (args.path as string) || '';
  365. Private.navigateToPath(path, factory)
  366. .then(() => {
  367. 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: 'Shutdown 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. () => {
  717. launcher.content.cwd = model.path;
  718. },
  719. launcher
  720. );
  721. return launcher;
  722. });
  723. }
  724. /**
  725. * Get browser object given file path.
  726. */
  727. export function getBrowserForPath(
  728. path: string,
  729. factory: IFileBrowserFactory
  730. ): FileBrowser {
  731. const { defaultBrowser: browser, tracker } = factory;
  732. const driveName = browser.model.manager.services.contents.driveName(path);
  733. if (driveName) {
  734. let browserForPath = tracker.find(
  735. _path => _path.model.driveName === driveName
  736. );
  737. if (!browserForPath) {
  738. // warn that no filebrowser could be found for this driveName
  739. console.warn(
  740. `${CommandIDs.navigate} failed to find filebrowser for path: ${path}`
  741. );
  742. return;
  743. }
  744. return browserForPath;
  745. }
  746. // if driveName is empty, assume the main filebrowser
  747. return browser;
  748. }
  749. /**
  750. * Navigate to a path.
  751. */
  752. export function navigateToPath(
  753. path: string,
  754. factory: IFileBrowserFactory
  755. ): Promise<any> {
  756. const browserForPath = Private.getBrowserForPath(path, factory);
  757. const { services } = browserForPath.model.manager;
  758. const localPath = services.contents.localPath(path);
  759. return services.ready
  760. .then(() => services.contents.get(path, { content: false }))
  761. .then(value => {
  762. const { model } = browserForPath;
  763. const { restored } = model;
  764. if (value.type === 'directory') {
  765. return restored.then(() => model.cd(`/${localPath}`));
  766. }
  767. return restored.then(() => model.cd(`/${PathExt.dirname(localPath)}`));
  768. });
  769. }
  770. }