index.ts 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. /**
  4. * @packageDocumentation
  5. * @module filebrowser-extension
  6. */
  7. import {
  8. ILabShell,
  9. ILayoutRestorer,
  10. IRouter,
  11. ITreePathUpdater,
  12. JupyterFrontEnd,
  13. JupyterFrontEndPlugin
  14. } from '@jupyterlab/application';
  15. import {
  16. Clipboard,
  17. createToolbarFactory,
  18. ICommandPalette,
  19. InputDialog,
  20. IToolbarWidgetRegistry,
  21. MainAreaWidget,
  22. setToolbar,
  23. showErrorMessage,
  24. WidgetTracker
  25. } from '@jupyterlab/apputils';
  26. import { PageConfig, PathExt } from '@jupyterlab/coreutils';
  27. import { IDocumentManager } from '@jupyterlab/docmanager';
  28. import { DocumentRegistry } from '@jupyterlab/docregistry';
  29. import {
  30. FileBrowser,
  31. FileUploadStatus,
  32. FilterFileBrowserModel,
  33. IFileBrowserCommands,
  34. IFileBrowserFactory,
  35. Uploader
  36. } from '@jupyterlab/filebrowser';
  37. import { Launcher } from '@jupyterlab/launcher';
  38. import { Contents } from '@jupyterlab/services';
  39. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  40. import { IStateDB } from '@jupyterlab/statedb';
  41. import { IStatusBar } from '@jupyterlab/statusbar';
  42. import { ITranslator } from '@jupyterlab/translation';
  43. import {
  44. addIcon,
  45. closeIcon,
  46. copyIcon,
  47. cutIcon,
  48. downloadIcon,
  49. editIcon,
  50. fileIcon,
  51. folderIcon,
  52. linkIcon,
  53. markdownIcon,
  54. newFolderIcon,
  55. pasteIcon,
  56. refreshIcon,
  57. stopIcon,
  58. textEditorIcon
  59. } from '@jupyterlab/ui-components';
  60. import { find, IIterator, map, reduce, toArray } from '@lumino/algorithm';
  61. import { CommandRegistry } from '@lumino/commands';
  62. import { ContextMenu } from '@lumino/widgets';
  63. import { JSONObject } from '@lumino/coreutils';
  64. const FILE_BROWSER_FACTORY = 'FileBrowser';
  65. /**
  66. * The command IDs used by the file browser plugin.
  67. */
  68. namespace CommandIDs {
  69. export const copy = 'filebrowser:copy';
  70. export const copyDownloadLink = 'filebrowser:copy-download-link';
  71. // For main browser only.
  72. export const createLauncher = 'filebrowser:create-main-launcher';
  73. export const cut = 'filebrowser:cut';
  74. export const del = 'filebrowser:delete';
  75. export const download = 'filebrowser:download';
  76. export const duplicate = 'filebrowser:duplicate';
  77. // For main browser only.
  78. export const hideBrowser = 'filebrowser:hide-main';
  79. export const goToPath = 'filebrowser:go-to-path';
  80. export const goUp = 'filebrowser:go-up';
  81. export const openPath = 'filebrowser:open-path';
  82. export const openUrl = 'filebrowser:open-url';
  83. export const open = 'filebrowser:open';
  84. export const openBrowserTab = 'filebrowser:open-browser-tab';
  85. export const paste = 'filebrowser:paste';
  86. export const createNewDirectory = 'filebrowser:create-new-directory';
  87. export const createNewFile = 'filebrowser:create-new-file';
  88. export const createNewMarkdownFile = 'filebrowser:create-new-markdown-file';
  89. export const refresh = 'filebrowser:refresh';
  90. export const rename = 'filebrowser:rename';
  91. // For main browser only.
  92. export const copyShareableLink = 'filebrowser:share-main';
  93. // For main browser only.
  94. export const copyPath = 'filebrowser:copy-path';
  95. export const showBrowser = 'filebrowser:activate';
  96. export const shutdown = 'filebrowser:shutdown';
  97. // For main browser only.
  98. export const toggleBrowser = 'filebrowser:toggle-main';
  99. export const toggleNavigateToCurrentDirectory =
  100. 'filebrowser:toggle-navigate-to-current-directory';
  101. export const toggleLastModified = 'filebrowser:toggle-last-modified';
  102. export const search = 'filebrowser:search';
  103. export const toggleHiddenFiles = 'filebrowser:toggle-hidden-files';
  104. }
  105. /**
  106. * The file browser namespace token.
  107. */
  108. const namespace = 'filebrowser';
  109. /**
  110. * The default file browser extension.
  111. */
  112. const browser: JupyterFrontEndPlugin<void> = {
  113. id: '@jupyterlab/filebrowser-extension:browser',
  114. requires: [IFileBrowserFactory, ITranslator],
  115. optional: [
  116. ILayoutRestorer,
  117. ISettingRegistry,
  118. ITreePathUpdater,
  119. ICommandPalette
  120. ],
  121. provides: IFileBrowserCommands,
  122. autoStart: true,
  123. activate: async (
  124. app: JupyterFrontEnd,
  125. factory: IFileBrowserFactory,
  126. translator: ITranslator,
  127. restorer: ILayoutRestorer | null,
  128. settingRegistry: ISettingRegistry | null,
  129. treePathUpdater: ITreePathUpdater | null,
  130. commandPalette: ICommandPalette | null
  131. ): Promise<void> => {
  132. const trans = translator.load('jupyterlab');
  133. const browser = factory.defaultBrowser;
  134. // Let the application restorer track the primary file browser (that is
  135. // automatically created) for restoration of application state (e.g. setting
  136. // the file browser as the current side bar widget).
  137. //
  138. // All other file browsers created by using the factory function are
  139. // responsible for their own restoration behavior, if any.
  140. if (restorer) {
  141. restorer.add(browser, namespace);
  142. }
  143. // Navigate to preferred-dir trait if found
  144. const preferredPath = PageConfig.getOption('preferredPath');
  145. if (preferredPath) {
  146. await browser.model.cd(preferredPath);
  147. }
  148. addCommands(app, factory, translator, settingRegistry, commandPalette);
  149. // Show the current file browser shortcut in its title.
  150. const updateBrowserTitle = () => {
  151. const binding = find(
  152. app.commands.keyBindings,
  153. b => b.command === CommandIDs.toggleBrowser
  154. );
  155. if (binding) {
  156. const ks = CommandRegistry.formatKeystroke(binding.keys.join(' '));
  157. browser.title.caption = trans.__('File Browser (%1)', ks);
  158. } else {
  159. browser.title.caption = trans.__('File Browser');
  160. }
  161. };
  162. updateBrowserTitle();
  163. app.commands.keyBindingChanged.connect(() => {
  164. updateBrowserTitle();
  165. });
  166. return void Promise.all([app.restored, browser.model.restored]).then(() => {
  167. if (treePathUpdater) {
  168. browser.model.pathChanged.connect((sender, args) => {
  169. treePathUpdater(args.newValue);
  170. });
  171. }
  172. let navigateToCurrentDirectory: boolean = false;
  173. let showLastModifiedColumn: boolean = true;
  174. let useFuzzyFilter: boolean = true;
  175. let showHiddenFiles: boolean = false;
  176. if (settingRegistry) {
  177. void settingRegistry
  178. .load('@jupyterlab/filebrowser-extension:browser')
  179. .then(settings => {
  180. settings.changed.connect(settings => {
  181. navigateToCurrentDirectory = settings.get(
  182. 'navigateToCurrentDirectory'
  183. ).composite as boolean;
  184. browser.navigateToCurrentDirectory = navigateToCurrentDirectory;
  185. });
  186. navigateToCurrentDirectory = settings.get(
  187. 'navigateToCurrentDirectory'
  188. ).composite as boolean;
  189. browser.navigateToCurrentDirectory = navigateToCurrentDirectory;
  190. settings.changed.connect(settings => {
  191. showLastModifiedColumn = settings.get('showLastModifiedColumn')
  192. .composite as boolean;
  193. browser.showLastModifiedColumn = showLastModifiedColumn;
  194. });
  195. showLastModifiedColumn = settings.get('showLastModifiedColumn')
  196. .composite as boolean;
  197. browser.showLastModifiedColumn = showLastModifiedColumn;
  198. settings.changed.connect(settings => {
  199. useFuzzyFilter = settings.get('useFuzzyFilter')
  200. .composite as boolean;
  201. browser.useFuzzyFilter = useFuzzyFilter;
  202. });
  203. useFuzzyFilter = settings.get('useFuzzyFilter')
  204. .composite as boolean;
  205. browser.useFuzzyFilter = useFuzzyFilter;
  206. settings.changed.connect(settings => {
  207. showHiddenFiles = settings.get('showHiddenFiles')
  208. .composite as boolean;
  209. browser.showHiddenFiles = showHiddenFiles;
  210. });
  211. showHiddenFiles = settings.get('showHiddenFiles')
  212. .composite as boolean;
  213. browser.showHiddenFiles = showHiddenFiles;
  214. });
  215. }
  216. });
  217. }
  218. };
  219. /**
  220. * The default file browser factory provider.
  221. */
  222. const factory: JupyterFrontEndPlugin<IFileBrowserFactory> = {
  223. id: '@jupyterlab/filebrowser-extension:factory',
  224. provides: IFileBrowserFactory,
  225. requires: [IDocumentManager, ITranslator],
  226. optional: [IStateDB, IRouter, JupyterFrontEnd.ITreeResolver],
  227. activate: async (
  228. app: JupyterFrontEnd,
  229. docManager: IDocumentManager,
  230. translator: ITranslator,
  231. state: IStateDB | null,
  232. router: IRouter | null,
  233. tree: JupyterFrontEnd.ITreeResolver | null
  234. ): Promise<IFileBrowserFactory> => {
  235. const { commands } = app;
  236. const tracker = new WidgetTracker<FileBrowser>({ namespace });
  237. const createFileBrowser = (
  238. id: string,
  239. options: IFileBrowserFactory.IOptions = {}
  240. ) => {
  241. const model = new FilterFileBrowserModel({
  242. translator: translator,
  243. auto: options.auto ?? true,
  244. manager: docManager,
  245. driveName: options.driveName || '',
  246. refreshInterval: options.refreshInterval,
  247. state:
  248. options.state === null
  249. ? undefined
  250. : options.state || state || undefined
  251. });
  252. const restore = options.restore;
  253. const widget = new FileBrowser({ id, model, restore, translator });
  254. // Track the newly created file browser.
  255. void tracker.add(widget);
  256. return widget;
  257. };
  258. // Manually restore and load the default file browser.
  259. const defaultBrowser = createFileBrowser('filebrowser', {
  260. auto: false,
  261. restore: false
  262. });
  263. void Private.restoreBrowser(defaultBrowser, commands, router, tree);
  264. return { createFileBrowser, defaultBrowser, tracker };
  265. }
  266. };
  267. /**
  268. * A plugin providing download + copy download link commands in the context menu.
  269. *
  270. * Disabling this plugin will NOT disable downloading files from the server.
  271. * Users will still be able to retrieve files from the file download URLs the
  272. * server provides.
  273. */
  274. const downloadPlugin: JupyterFrontEndPlugin<void> = {
  275. id: '@jupyterlab/filebrowser-extension:download',
  276. requires: [IFileBrowserFactory, ITranslator],
  277. autoStart: true,
  278. activate: (
  279. app: JupyterFrontEnd,
  280. factory: IFileBrowserFactory,
  281. translator: ITranslator
  282. ): void => {
  283. const trans = translator.load('jupyterlab');
  284. const { commands } = app;
  285. const { tracker } = factory;
  286. commands.addCommand(CommandIDs.download, {
  287. execute: () => {
  288. const widget = tracker.currentWidget;
  289. if (widget) {
  290. return widget.download();
  291. }
  292. },
  293. icon: downloadIcon.bindprops({ stylesheet: 'menuItem' }),
  294. label: trans.__('Download')
  295. });
  296. commands.addCommand(CommandIDs.copyDownloadLink, {
  297. execute: () => {
  298. const widget = tracker.currentWidget;
  299. if (!widget) {
  300. return;
  301. }
  302. return widget.model.manager.services.contents
  303. .getDownloadUrl(widget.selectedItems().next()!.path)
  304. .then(url => {
  305. Clipboard.copyToSystem(url);
  306. });
  307. },
  308. icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
  309. label: trans.__('Copy Download Link'),
  310. mnemonic: 0
  311. });
  312. }
  313. };
  314. /**
  315. * A plugin to add the file browser widget to an ILabShell
  316. */
  317. const browserWidget: JupyterFrontEndPlugin<void> = {
  318. id: '@jupyterlab/filebrowser-extension:widget',
  319. requires: [
  320. IDocumentManager,
  321. IFileBrowserFactory,
  322. ISettingRegistry,
  323. IToolbarWidgetRegistry,
  324. ITranslator,
  325. ILabShell,
  326. IFileBrowserCommands
  327. ],
  328. autoStart: true,
  329. activate: (
  330. app: JupyterFrontEnd,
  331. docManager: IDocumentManager,
  332. factory: IFileBrowserFactory,
  333. settings: ISettingRegistry,
  334. toolbarRegistry: IToolbarWidgetRegistry,
  335. translator: ITranslator,
  336. labShell: ILabShell
  337. ): void => {
  338. const { commands } = app;
  339. const { defaultBrowser: browser, tracker } = factory;
  340. const trans = translator.load('jupyterlab');
  341. // Set attributes when adding the browser to the UI
  342. browser.node.setAttribute('role', 'region');
  343. browser.node.setAttribute('aria-label', trans.__('File Browser Section'));
  344. browser.title.icon = folderIcon;
  345. // Toolbar
  346. toolbarRegistry.registerFactory(
  347. FILE_BROWSER_FACTORY,
  348. 'uploader',
  349. (browser: FileBrowser) =>
  350. new Uploader({ model: browser.model, translator })
  351. );
  352. setToolbar(
  353. browser,
  354. createToolbarFactory(
  355. toolbarRegistry,
  356. settings,
  357. FILE_BROWSER_FACTORY,
  358. browserWidget.id,
  359. translator
  360. )
  361. );
  362. labShell.add(browser, 'left', { rank: 100 });
  363. commands.addCommand(CommandIDs.showBrowser, {
  364. execute: args => {
  365. const path = (args.path as string) || '';
  366. const browserForPath = Private.getBrowserForPath(path, factory);
  367. // Check for browser not found
  368. if (!browserForPath) {
  369. return;
  370. }
  371. // Shortcut if we are using the main file browser
  372. if (browser === browserForPath) {
  373. labShell.activateById(browser.id);
  374. return;
  375. } else {
  376. const areas: ILabShell.Area[] = ['left', 'right'];
  377. for (const area of areas) {
  378. const it = labShell.widgets(area);
  379. let widget = it.next();
  380. while (widget) {
  381. if (widget.contains(browserForPath)) {
  382. labShell.activateById(widget.id);
  383. return;
  384. }
  385. widget = it.next();
  386. }
  387. }
  388. }
  389. }
  390. });
  391. commands.addCommand(CommandIDs.hideBrowser, {
  392. execute: () => {
  393. const widget = tracker.currentWidget;
  394. if (widget && !widget.isHidden) {
  395. labShell.collapseLeft();
  396. }
  397. }
  398. });
  399. // If the layout is a fresh session without saved data and not in single document
  400. // mode, open file browser.
  401. void labShell.restored.then(layout => {
  402. if (layout.fresh && labShell.mode !== 'single-document') {
  403. void commands.execute(CommandIDs.showBrowser, void 0);
  404. }
  405. });
  406. void Promise.all([app.restored, browser.model.restored]).then(() => {
  407. function maybeCreate() {
  408. // Create a launcher if there are no open items.
  409. if (
  410. labShell.isEmpty('main') &&
  411. commands.hasCommand('launcher:create')
  412. ) {
  413. void Private.createLauncher(commands, browser);
  414. }
  415. }
  416. // When layout is modified, create a launcher if there are no open items.
  417. labShell.layoutModified.connect(() => {
  418. maybeCreate();
  419. });
  420. // Whether to automatically navigate to a document's current directory
  421. labShell.currentChanged.connect(async (_, change) => {
  422. if (browser.navigateToCurrentDirectory && change.newValue) {
  423. const { newValue } = change;
  424. const context = docManager.contextForWidget(newValue);
  425. if (context) {
  426. const { path } = context;
  427. try {
  428. await Private.navigateToPath(path, factory, translator);
  429. } catch (reason) {
  430. console.warn(
  431. `${CommandIDs.goToPath} failed to open: ${path}`,
  432. reason
  433. );
  434. }
  435. }
  436. }
  437. });
  438. maybeCreate();
  439. });
  440. }
  441. };
  442. /**
  443. * The default file browser share-file plugin
  444. *
  445. * This extension adds a "Copy Shareable Link" command that generates a copy-
  446. * pastable URL. This url can be used to open a particular file in JupyterLab,
  447. * handy for emailing links or bookmarking for reference.
  448. *
  449. * If you need to change how this link is generated (for instance, to copy a
  450. * /user-redirect URL for JupyterHub), disable this plugin and replace it
  451. * with another implementation.
  452. */
  453. const shareFile: JupyterFrontEndPlugin<void> = {
  454. id: '@jupyterlab/filebrowser-extension:share-file',
  455. requires: [IFileBrowserFactory, ITranslator],
  456. autoStart: true,
  457. activate: (
  458. app: JupyterFrontEnd,
  459. factory: IFileBrowserFactory,
  460. translator: ITranslator
  461. ): void => {
  462. const trans = translator.load('jupyterlab');
  463. const { commands } = app;
  464. const { tracker } = factory;
  465. commands.addCommand(CommandIDs.copyShareableLink, {
  466. execute: () => {
  467. const widget = tracker.currentWidget;
  468. const model = widget?.selectedItems().next();
  469. if (!model) {
  470. return;
  471. }
  472. Clipboard.copyToSystem(
  473. PageConfig.getUrl({
  474. workspace: PageConfig.defaultWorkspace,
  475. treePath: model.path,
  476. toShare: true
  477. })
  478. );
  479. },
  480. isVisible: () =>
  481. !!tracker.currentWidget &&
  482. toArray(tracker.currentWidget.selectedItems()).length === 1,
  483. icon: linkIcon.bindprops({ stylesheet: 'menuItem' }),
  484. label: trans.__('Copy Shareable Link')
  485. });
  486. }
  487. };
  488. /**
  489. * The "Open With" context menu.
  490. *
  491. * This is its own plugin in case you would like to disable this feature.
  492. * e.g. jupyter labextension disable @jupyterlab/filebrowser-extension:open-with
  493. */
  494. const openWithPlugin: JupyterFrontEndPlugin<void> = {
  495. id: '@jupyterlab/filebrowser-extension:open-with',
  496. requires: [IFileBrowserFactory],
  497. autoStart: true,
  498. activate: (app: JupyterFrontEnd, factory: IFileBrowserFactory): void => {
  499. const { docRegistry } = app;
  500. const { tracker } = factory;
  501. function updateOpenWithMenu(contextMenu: ContextMenu) {
  502. const openWith =
  503. contextMenu.menu.items.find(
  504. item =>
  505. item.type === 'submenu' &&
  506. item.submenu?.id === 'jp-contextmenu-open-with'
  507. )?.submenu ?? null;
  508. if (!openWith) {
  509. return; // Bail early if the open with menu is not displayed
  510. }
  511. // clear the current menu items
  512. openWith.clearItems();
  513. // get the widget factories that could be used to open all of the items
  514. // in the current filebrowser selection
  515. const factories = tracker.currentWidget
  516. ? Private.OpenWith.intersection<string>(
  517. map(tracker.currentWidget.selectedItems(), i => {
  518. return Private.OpenWith.getFactories(docRegistry, i);
  519. })
  520. )
  521. : new Set<string>();
  522. // make new menu items from the widget factories
  523. factories.forEach(factory => {
  524. openWith.addItem({
  525. args: { factory: factory },
  526. command: CommandIDs.open
  527. });
  528. });
  529. }
  530. app.contextMenu.opened.connect(updateOpenWithMenu);
  531. }
  532. };
  533. /**
  534. * The "Open in New Browser Tab" context menu.
  535. *
  536. * This is its own plugin in case you would like to disable this feature.
  537. * e.g. jupyter labextension disable @jupyterlab/filebrowser-extension:open-browser-tab
  538. *
  539. * Note: If disabling this, you may also want to disable:
  540. * @jupyterlab/docmanager-extension:open-browser-tab
  541. */
  542. const openBrowserTabPlugin: JupyterFrontEndPlugin<void> = {
  543. id: '@jupyterlab/filebrowser-extension:open-browser-tab',
  544. requires: [IFileBrowserFactory, ITranslator],
  545. autoStart: true,
  546. activate: (
  547. app: JupyterFrontEnd,
  548. factory: IFileBrowserFactory,
  549. translator: ITranslator
  550. ): void => {
  551. const { commands } = app;
  552. const trans = translator.load('jupyterlab');
  553. const { tracker } = factory;
  554. commands.addCommand(CommandIDs.openBrowserTab, {
  555. execute: args => {
  556. const widget = tracker.currentWidget;
  557. if (!widget) {
  558. return;
  559. }
  560. const mode = args['mode'] as string | undefined;
  561. return Promise.all(
  562. toArray(
  563. map(widget.selectedItems(), item => {
  564. if (mode === 'single-document') {
  565. const url = PageConfig.getUrl({
  566. mode: 'single-document',
  567. treePath: item.path
  568. });
  569. const opened = window.open();
  570. if (opened) {
  571. opened.opener = null;
  572. opened.location.href = url;
  573. } else {
  574. throw new Error('Failed to open new browser tab.');
  575. }
  576. } else {
  577. return commands.execute('docmanager:open-browser-tab', {
  578. path: item.path
  579. });
  580. }
  581. })
  582. )
  583. );
  584. },
  585. icon: addIcon.bindprops({ stylesheet: 'menuItem' }),
  586. label: args =>
  587. args['mode'] === 'single-document'
  588. ? trans.__('Open in Simple Mode')
  589. : trans.__('Open in New Browser Tab'),
  590. mnemonic: 0
  591. });
  592. }
  593. };
  594. /**
  595. * A plugin providing file upload status.
  596. */
  597. export const fileUploadStatus: JupyterFrontEndPlugin<void> = {
  598. id: '@jupyterlab/filebrowser-extension:file-upload-status',
  599. autoStart: true,
  600. requires: [IFileBrowserFactory, ITranslator],
  601. optional: [IStatusBar],
  602. activate: (
  603. app: JupyterFrontEnd,
  604. browser: IFileBrowserFactory,
  605. translator: ITranslator,
  606. statusBar: IStatusBar | null
  607. ) => {
  608. if (!statusBar) {
  609. // Automatically disable if statusbar missing
  610. return;
  611. }
  612. const item = new FileUploadStatus({
  613. tracker: browser.tracker,
  614. translator
  615. });
  616. statusBar.registerStatusItem(
  617. '@jupyterlab/filebrowser-extension:file-upload-status',
  618. {
  619. item,
  620. align: 'middle',
  621. isActive: () => {
  622. return !!item.model && item.model.items.length > 0;
  623. },
  624. activeStateChanged: item.model.stateChanged
  625. }
  626. );
  627. }
  628. };
  629. /**
  630. * A plugin to open files from remote URLs
  631. */
  632. const openUrlPlugin: JupyterFrontEndPlugin<void> = {
  633. id: '@jupyterlab/filebrowser-extension:open-url',
  634. autoStart: true,
  635. requires: [IFileBrowserFactory, ITranslator],
  636. optional: [ICommandPalette],
  637. activate: (
  638. app: JupyterFrontEnd,
  639. factory: IFileBrowserFactory,
  640. translator: ITranslator,
  641. palette: ICommandPalette | null
  642. ) => {
  643. const { commands } = app;
  644. const trans = translator.load('jupyterlab');
  645. const { defaultBrowser: browser } = factory;
  646. const command = CommandIDs.openUrl;
  647. commands.addCommand(command, {
  648. label: args =>
  649. args.url ? trans.__('Open %1', args.url) : trans.__('Open from URL…'),
  650. caption: args =>
  651. args.url ? trans.__('Open %1', args.url) : trans.__('Open from URL'),
  652. execute: async args => {
  653. let url: string | undefined = (args?.url as string) ?? '';
  654. if (!url) {
  655. url =
  656. (
  657. await InputDialog.getText({
  658. label: trans.__('URL'),
  659. placeholder: 'https://example.com/path/to/file',
  660. title: trans.__('Open URL'),
  661. okLabel: trans.__('Open')
  662. })
  663. ).value ?? undefined;
  664. }
  665. if (!url) {
  666. return;
  667. }
  668. let type = '';
  669. let blob;
  670. // fetch the file from the URL
  671. try {
  672. const req = await fetch(url);
  673. blob = await req.blob();
  674. type = req.headers.get('Content-Type') ?? '';
  675. } catch (reason) {
  676. if (reason.response && reason.response.status !== 200) {
  677. reason.message = trans.__('Could not open URL: %1', url);
  678. }
  679. return showErrorMessage(trans.__('Cannot fetch'), reason);
  680. }
  681. // upload the content of the file to the server
  682. try {
  683. const name = PathExt.basename(url);
  684. const file = new File([blob], name, { type });
  685. const model = await browser.model.upload(file);
  686. return commands.execute('docmanager:open', {
  687. path: model.path
  688. });
  689. } catch (error) {
  690. return showErrorMessage(
  691. trans._p('showErrorMessage', 'Upload Error'),
  692. error
  693. );
  694. }
  695. }
  696. });
  697. if (palette) {
  698. palette.addItem({
  699. command,
  700. category: trans.__('File Operations')
  701. });
  702. }
  703. }
  704. };
  705. /**
  706. * Add the main file browser commands to the application's command registry.
  707. */
  708. function addCommands(
  709. app: JupyterFrontEnd,
  710. factory: IFileBrowserFactory,
  711. translator: ITranslator,
  712. settingRegistry: ISettingRegistry | null,
  713. commandPalette: ICommandPalette | null
  714. ): void {
  715. const trans = translator.load('jupyterlab');
  716. const { docRegistry: registry, commands } = app;
  717. const { defaultBrowser: browser, tracker } = factory;
  718. commands.addCommand(CommandIDs.del, {
  719. execute: () => {
  720. const widget = tracker.currentWidget;
  721. if (widget) {
  722. return widget.delete();
  723. }
  724. },
  725. icon: closeIcon.bindprops({ stylesheet: 'menuItem' }),
  726. label: trans.__('Delete'),
  727. mnemonic: 0
  728. });
  729. commands.addCommand(CommandIDs.copy, {
  730. execute: () => {
  731. const widget = tracker.currentWidget;
  732. if (widget) {
  733. return widget.copy();
  734. }
  735. },
  736. icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
  737. label: trans.__('Copy'),
  738. mnemonic: 0
  739. });
  740. commands.addCommand(CommandIDs.cut, {
  741. execute: () => {
  742. const widget = tracker.currentWidget;
  743. if (widget) {
  744. return widget.cut();
  745. }
  746. },
  747. icon: cutIcon.bindprops({ stylesheet: 'menuItem' }),
  748. label: trans.__('Cut')
  749. });
  750. commands.addCommand(CommandIDs.duplicate, {
  751. execute: () => {
  752. const widget = tracker.currentWidget;
  753. if (widget) {
  754. return widget.duplicate();
  755. }
  756. },
  757. icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
  758. label: trans.__('Duplicate')
  759. });
  760. commands.addCommand(CommandIDs.goToPath, {
  761. execute: async args => {
  762. const path = (args.path as string) || '';
  763. const showBrowser = !(args?.dontShowBrowser ?? false);
  764. try {
  765. const item = await Private.navigateToPath(path, factory, translator);
  766. if (item.type !== 'directory' && showBrowser) {
  767. const browserForPath = Private.getBrowserForPath(path, factory);
  768. if (browserForPath) {
  769. browserForPath.clearSelectedItems();
  770. const parts = path.split('/');
  771. const name = parts[parts.length - 1];
  772. if (name) {
  773. await browserForPath.selectItemByName(name);
  774. }
  775. }
  776. }
  777. } catch (reason) {
  778. console.warn(`${CommandIDs.goToPath} failed to go to: ${path}`, reason);
  779. }
  780. if (showBrowser) {
  781. return commands.execute(CommandIDs.showBrowser, { path });
  782. }
  783. }
  784. });
  785. commands.addCommand(CommandIDs.goUp, {
  786. label: 'go up',
  787. execute: async () => {
  788. const browserForPath = Private.getBrowserForPath('', factory);
  789. if (!browserForPath) {
  790. return;
  791. }
  792. const { model } = browserForPath;
  793. await model.restored;
  794. if (model.path === model.rootPath) {
  795. return;
  796. }
  797. try {
  798. await model.cd('..');
  799. } catch (reason) {
  800. console.warn(
  801. `${CommandIDs.goUp} failed to go to parent directory of ${model.path}`,
  802. reason
  803. );
  804. }
  805. }
  806. });
  807. commands.addCommand(CommandIDs.openPath, {
  808. label: args =>
  809. args.path ? trans.__('Open %1', args.path) : trans.__('Open from Path…'),
  810. caption: args =>
  811. args.path ? trans.__('Open %1', args.path) : trans.__('Open from path'),
  812. execute: async args => {
  813. let path: string | undefined;
  814. if (args?.path) {
  815. path = args.path as string;
  816. } else {
  817. path =
  818. (
  819. await InputDialog.getText({
  820. label: trans.__('Path'),
  821. placeholder: '/path/relative/to/jlab/root',
  822. title: trans.__('Open Path'),
  823. okLabel: trans.__('Open')
  824. })
  825. ).value ?? undefined;
  826. }
  827. if (!path) {
  828. return;
  829. }
  830. try {
  831. const trailingSlash = path !== '/' && path.endsWith('/');
  832. if (trailingSlash) {
  833. // The normal contents service errors on paths ending in slash
  834. path = path.slice(0, path.length - 1);
  835. }
  836. const browserForPath = Private.getBrowserForPath(path, factory)!;
  837. const { services } = browserForPath.model.manager;
  838. const item = await services.contents.get(path, {
  839. content: false
  840. });
  841. if (trailingSlash && item.type !== 'directory') {
  842. throw new Error(`Path ${path}/ is not a directory`);
  843. }
  844. await commands.execute(CommandIDs.goToPath, {
  845. path,
  846. dontShowBrowser: args.dontShowBrowser
  847. });
  848. if (item.type === 'directory') {
  849. return;
  850. }
  851. return commands.execute('docmanager:open', { path });
  852. } catch (reason) {
  853. if (reason.response && reason.response.status === 404) {
  854. reason.message = trans.__('Could not find path: %1', path);
  855. }
  856. return showErrorMessage(trans.__('Cannot open'), reason);
  857. }
  858. }
  859. });
  860. // Add the openPath command to the command palette
  861. if (commandPalette) {
  862. commandPalette.addItem({
  863. command: CommandIDs.openPath,
  864. category: trans.__('File Operations')
  865. });
  866. }
  867. commands.addCommand(CommandIDs.open, {
  868. execute: args => {
  869. const factory = (args['factory'] as string) || void 0;
  870. const widget = tracker.currentWidget;
  871. if (!widget) {
  872. return;
  873. }
  874. const { contents } = widget.model.manager.services;
  875. return Promise.all(
  876. toArray(
  877. map(widget.selectedItems(), item => {
  878. if (item.type === 'directory') {
  879. const localPath = contents.localPath(item.path);
  880. return widget.model.cd(`/${localPath}`);
  881. }
  882. return commands.execute('docmanager:open', {
  883. factory: factory,
  884. path: item.path
  885. });
  886. })
  887. )
  888. );
  889. },
  890. icon: args => {
  891. const factory = (args['factory'] as string) || void 0;
  892. if (factory) {
  893. // if an explicit factory is passed...
  894. const ft = registry.getFileType(factory);
  895. // ...set an icon if the factory name corresponds to a file type name...
  896. // ...or leave the icon blank
  897. return ft?.icon?.bindprops({ stylesheet: 'menuItem' });
  898. } else {
  899. return folderIcon.bindprops({ stylesheet: 'menuItem' });
  900. }
  901. },
  902. // FIXME-TRANS: Is this localizable?
  903. label: args =>
  904. (args['label'] || args['factory'] || trans.__('Open')) as string,
  905. mnemonic: 0
  906. });
  907. commands.addCommand(CommandIDs.paste, {
  908. execute: () => {
  909. const widget = tracker.currentWidget;
  910. if (widget) {
  911. return widget.paste();
  912. }
  913. },
  914. icon: pasteIcon.bindprops({ stylesheet: 'menuItem' }),
  915. label: trans.__('Paste'),
  916. mnemonic: 0
  917. });
  918. commands.addCommand(CommandIDs.createNewDirectory, {
  919. execute: () => {
  920. const widget = tracker.currentWidget;
  921. if (widget) {
  922. return widget.createNewDirectory();
  923. }
  924. },
  925. icon: newFolderIcon.bindprops({ stylesheet: 'menuItem' }),
  926. label: trans.__('New Folder')
  927. });
  928. commands.addCommand(CommandIDs.createNewFile, {
  929. execute: () => {
  930. const widget = tracker.currentWidget;
  931. if (widget) {
  932. return widget.createNewFile({ ext: 'txt' });
  933. }
  934. },
  935. icon: textEditorIcon.bindprops({ stylesheet: 'menuItem' }),
  936. label: trans.__('New File')
  937. });
  938. commands.addCommand(CommandIDs.createNewMarkdownFile, {
  939. execute: () => {
  940. const widget = tracker.currentWidget;
  941. if (widget) {
  942. return widget.createNewFile({ ext: 'md' });
  943. }
  944. },
  945. icon: markdownIcon.bindprops({ stylesheet: 'menuItem' }),
  946. label: trans.__('New Markdown File')
  947. });
  948. commands.addCommand(CommandIDs.refresh, {
  949. execute: args => {
  950. const widget = tracker.currentWidget;
  951. if (widget) {
  952. return widget.model.refresh();
  953. }
  954. },
  955. icon: refreshIcon.bindprops({ stylesheet: 'menuItem' }),
  956. caption: trans.__('Refresh the file browser.'),
  957. label: trans.__('Refresh File List')
  958. });
  959. commands.addCommand(CommandIDs.rename, {
  960. execute: args => {
  961. const widget = tracker.currentWidget;
  962. if (widget) {
  963. return widget.rename();
  964. }
  965. },
  966. icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
  967. label: trans.__('Rename'),
  968. mnemonic: 0
  969. });
  970. commands.addCommand(CommandIDs.copyPath, {
  971. execute: () => {
  972. const widget = tracker.currentWidget;
  973. if (!widget) {
  974. return;
  975. }
  976. const item = widget.selectedItems().next();
  977. if (!item) {
  978. return;
  979. }
  980. Clipboard.copyToSystem(item.path);
  981. },
  982. isVisible: () =>
  983. !!tracker.currentWidget &&
  984. tracker.currentWidget.selectedItems().next !== undefined,
  985. icon: fileIcon.bindprops({ stylesheet: 'menuItem' }),
  986. label: trans.__('Copy Path')
  987. });
  988. commands.addCommand(CommandIDs.shutdown, {
  989. execute: () => {
  990. const widget = tracker.currentWidget;
  991. if (widget) {
  992. return widget.shutdownKernels();
  993. }
  994. },
  995. icon: stopIcon.bindprops({ stylesheet: 'menuItem' }),
  996. label: trans.__('Shut Down Kernel')
  997. });
  998. commands.addCommand(CommandIDs.toggleBrowser, {
  999. execute: () => {
  1000. if (browser.isHidden) {
  1001. return commands.execute(CommandIDs.showBrowser, void 0);
  1002. }
  1003. return commands.execute(CommandIDs.hideBrowser, void 0);
  1004. }
  1005. });
  1006. commands.addCommand(CommandIDs.createLauncher, {
  1007. label: trans.__('New Launcher'),
  1008. icon: args => (args.toolbar ? addIcon : undefined),
  1009. execute: (args: JSONObject) => {
  1010. if (commands.hasCommand('launcher:create')) {
  1011. return Private.createLauncher(commands, browser, args);
  1012. }
  1013. }
  1014. });
  1015. if (settingRegistry) {
  1016. commands.addCommand(CommandIDs.toggleNavigateToCurrentDirectory, {
  1017. label: trans.__('Show Active File in File Browser'),
  1018. isToggled: () => browser.navigateToCurrentDirectory,
  1019. execute: () => {
  1020. const value = !browser.navigateToCurrentDirectory;
  1021. const key = 'navigateToCurrentDirectory';
  1022. return settingRegistry
  1023. .set('@jupyterlab/filebrowser-extension:browser', key, value)
  1024. .catch((reason: Error) => {
  1025. console.error(`Failed to set navigateToCurrentDirectory setting`);
  1026. });
  1027. }
  1028. });
  1029. }
  1030. commands.addCommand(CommandIDs.toggleLastModified, {
  1031. label: trans.__('Show Last Modified Column'),
  1032. isToggled: () => browser.showLastModifiedColumn,
  1033. execute: () => {
  1034. const value = !browser.showLastModifiedColumn;
  1035. const key = 'showLastModifiedColumn';
  1036. if (settingRegistry) {
  1037. return settingRegistry
  1038. .set('@jupyterlab/filebrowser-extension:browser', key, value)
  1039. .catch((reason: Error) => {
  1040. console.error(`Failed to set showLastModifiedColumn setting`);
  1041. });
  1042. }
  1043. }
  1044. });
  1045. commands.addCommand(CommandIDs.toggleHiddenFiles, {
  1046. label: trans.__('Show Hidden Files'),
  1047. isToggled: () => browser.showHiddenFiles,
  1048. isVisible: () => PageConfig.getOption('allow_hidden_files') === 'true',
  1049. execute: () => {
  1050. const value = !browser.showHiddenFiles;
  1051. const key = 'showHiddenFiles';
  1052. if (settingRegistry) {
  1053. return settingRegistry
  1054. .set('@jupyterlab/filebrowser-extension:browser', key, value)
  1055. .catch((reason: Error) => {
  1056. console.error(`Failed to set showHiddenFiles setting`);
  1057. });
  1058. }
  1059. }
  1060. });
  1061. commands.addCommand(CommandIDs.search, {
  1062. label: trans.__('Search on File Names'),
  1063. execute: () => alert('search')
  1064. });
  1065. if (commandPalette) {
  1066. commandPalette.addItem({
  1067. command: CommandIDs.toggleNavigateToCurrentDirectory,
  1068. category: trans.__('File Operations')
  1069. });
  1070. }
  1071. }
  1072. /**
  1073. * A namespace for private module data.
  1074. */
  1075. namespace Private {
  1076. /**
  1077. * Create a launcher for a given filebrowser widget.
  1078. */
  1079. export function createLauncher(
  1080. commands: CommandRegistry,
  1081. browser: FileBrowser,
  1082. args?: JSONObject
  1083. ): Promise<MainAreaWidget<Launcher>> {
  1084. const { model } = browser;
  1085. return commands
  1086. .execute('launcher:create', { cwd: model.path, ...args })
  1087. .then((launcher: MainAreaWidget<Launcher>) => {
  1088. model.pathChanged.connect(() => {
  1089. if (launcher.content) {
  1090. launcher.content.cwd = model.path;
  1091. }
  1092. }, launcher);
  1093. return launcher;
  1094. });
  1095. }
  1096. /**
  1097. * Get browser object given file path.
  1098. */
  1099. export function getBrowserForPath(
  1100. path: string,
  1101. factory: IFileBrowserFactory
  1102. ): FileBrowser | undefined {
  1103. const { defaultBrowser: browser, tracker } = factory;
  1104. const driveName = browser.model.manager.services.contents.driveName(path);
  1105. if (driveName) {
  1106. const browserForPath = tracker.find(
  1107. _path => _path.model.driveName === driveName
  1108. );
  1109. if (!browserForPath) {
  1110. // warn that no filebrowser could be found for this driveName
  1111. console.warn(
  1112. `${CommandIDs.goToPath} failed to find filebrowser for path: ${path}`
  1113. );
  1114. return;
  1115. }
  1116. return browserForPath;
  1117. }
  1118. // if driveName is empty, assume the main filebrowser
  1119. return browser;
  1120. }
  1121. /**
  1122. * Navigate to a path or the path containing a file.
  1123. */
  1124. export async function navigateToPath(
  1125. path: string,
  1126. factory: IFileBrowserFactory,
  1127. translator: ITranslator
  1128. ): Promise<Contents.IModel> {
  1129. const trans = translator.load('jupyterlab');
  1130. const browserForPath = Private.getBrowserForPath(path, factory);
  1131. if (!browserForPath) {
  1132. throw new Error(trans.__('No browser for path'));
  1133. }
  1134. const { services } = browserForPath.model.manager;
  1135. const localPath = services.contents.localPath(path);
  1136. await services.ready;
  1137. const item = await services.contents.get(path, { content: false });
  1138. const { model } = browserForPath;
  1139. await model.restored;
  1140. if (item.type === 'directory') {
  1141. await model.cd(`/${localPath}`);
  1142. } else {
  1143. await model.cd(`/${PathExt.dirname(localPath)}`);
  1144. }
  1145. return item;
  1146. }
  1147. /**
  1148. * Restores file browser state and overrides state if tree resolver resolves.
  1149. */
  1150. export async function restoreBrowser(
  1151. browser: FileBrowser,
  1152. commands: CommandRegistry,
  1153. router: IRouter | null,
  1154. tree: JupyterFrontEnd.ITreeResolver | null
  1155. ): Promise<void> {
  1156. const restoring = 'jp-mod-restoring';
  1157. browser.addClass(restoring);
  1158. if (!router) {
  1159. await browser.model.restore(browser.id);
  1160. await browser.model.refresh();
  1161. browser.removeClass(restoring);
  1162. return;
  1163. }
  1164. const listener = async () => {
  1165. router.routed.disconnect(listener);
  1166. const paths = await tree?.paths;
  1167. if (paths?.file || paths?.browser) {
  1168. // Restore the model without populating it.
  1169. await browser.model.restore(browser.id, false);
  1170. if (paths.file) {
  1171. await commands.execute(CommandIDs.openPath, {
  1172. path: paths.file,
  1173. dontShowBrowser: true
  1174. });
  1175. }
  1176. if (paths.browser) {
  1177. await commands.execute(CommandIDs.openPath, {
  1178. path: paths.browser,
  1179. dontShowBrowser: true
  1180. });
  1181. }
  1182. } else {
  1183. await browser.model.restore(browser.id);
  1184. await browser.model.refresh();
  1185. }
  1186. browser.removeClass(restoring);
  1187. };
  1188. router.routed.connect(listener);
  1189. }
  1190. }
  1191. /**
  1192. * Export the plugins as default.
  1193. */
  1194. const plugins: JupyterFrontEndPlugin<any>[] = [
  1195. factory,
  1196. browser,
  1197. shareFile,
  1198. fileUploadStatus,
  1199. downloadPlugin,
  1200. browserWidget,
  1201. openWithPlugin,
  1202. openBrowserTabPlugin,
  1203. openUrlPlugin
  1204. ];
  1205. export default plugins;
  1206. namespace Private {
  1207. export namespace OpenWith {
  1208. /**
  1209. * Get the factories for the selected item
  1210. *
  1211. * @param docRegistry Application document registry
  1212. * @param item Selected item model
  1213. * @returns Available factories for the model
  1214. */
  1215. export function getFactories(
  1216. docRegistry: DocumentRegistry,
  1217. item: Contents.IModel
  1218. ): Array<string> {
  1219. const factories = docRegistry
  1220. .preferredWidgetFactories(item.path)
  1221. .map(f => f.name);
  1222. const notebookFactory = docRegistry.getWidgetFactory('notebook')?.name;
  1223. if (
  1224. notebookFactory &&
  1225. item.type === 'notebook' &&
  1226. factories.indexOf(notebookFactory) === -1
  1227. ) {
  1228. factories.unshift(notebookFactory);
  1229. }
  1230. return factories;
  1231. }
  1232. /**
  1233. * Return the intersection of multiple arrays.
  1234. *
  1235. * @param iter Iterator of arrays
  1236. * @returns Set of common elements to all arrays
  1237. */
  1238. export function intersection<T>(iter: IIterator<Array<T>>): Set<T> {
  1239. // pop the first element of iter
  1240. const first = iter.next();
  1241. // first will be undefined if iter is empty
  1242. if (!first) {
  1243. return new Set<T>();
  1244. }
  1245. // "initialize" the intersection from first
  1246. const isect = new Set(first);
  1247. // reduce over the remaining elements of iter
  1248. return reduce(
  1249. iter,
  1250. (isect, subarr) => {
  1251. // filter out all elements not present in both isect and subarr,
  1252. // accumulate result in new set
  1253. return new Set(subarr.filter(x => isect.has(x)));
  1254. },
  1255. isect
  1256. );
  1257. }
  1258. }
  1259. }