index.ts 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ILabShell,
  5. ILayoutRestorer,
  6. IRouter,
  7. JupyterFrontEnd,
  8. JupyterFrontEndPlugin
  9. } from '@jupyterlab/application';
  10. import {
  11. Clipboard,
  12. MainAreaWidget,
  13. ToolbarButton,
  14. WidgetTracker,
  15. ICommandPalette,
  16. InputDialog,
  17. showErrorMessage
  18. } from '@jupyterlab/apputils';
  19. import { PageConfig, PathExt, URLExt } from '@jupyterlab/coreutils';
  20. import { IDocumentManager } from '@jupyterlab/docmanager';
  21. import {
  22. FileBrowserModel,
  23. FileBrowser,
  24. FileUploadStatus,
  25. IFileBrowserFactory
  26. } from '@jupyterlab/filebrowser';
  27. import { Launcher } from '@jupyterlab/launcher';
  28. import { IMainMenu } from '@jupyterlab/mainmenu';
  29. import { Contents } from '@jupyterlab/services';
  30. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  31. import { IStateDB } from '@jupyterlab/statedb';
  32. import { IStatusBar } from '@jupyterlab/statusbar';
  33. import { addIcon, folderIcon } from '@jupyterlab/ui-components';
  34. import { IIterator, map, reduce, toArray, find } from '@lumino/algorithm';
  35. import { CommandRegistry } from '@lumino/commands';
  36. import { Message } from '@lumino/messaging';
  37. import { Menu } from '@lumino/widgets';
  38. /**
  39. * The command IDs used by the file browser plugin.
  40. */
  41. namespace CommandIDs {
  42. export const copy = 'filebrowser:copy';
  43. export const copyDownloadLink = 'filebrowser:copy-download-link';
  44. // For main browser only.
  45. export const createLauncher = 'filebrowser:create-main-launcher';
  46. export const cut = 'filebrowser:cut';
  47. export const del = 'filebrowser:delete';
  48. export const download = 'filebrowser:download';
  49. export const duplicate = 'filebrowser:duplicate';
  50. // For main browser only.
  51. export const hideBrowser = 'filebrowser:hide-main';
  52. export const goToPath = 'filebrowser:go-to-path';
  53. export const openPath = 'filebrowser:open-path';
  54. export const open = 'filebrowser:open';
  55. export const openBrowserTab = 'filebrowser:open-browser-tab';
  56. export const paste = 'filebrowser:paste';
  57. export const createNewDirectory = 'filebrowser:create-new-directory';
  58. export const createNewFile = 'filebrowser:create-new-file';
  59. export const createNewMarkdownFile = 'filebrowser:create-new-markdown-file';
  60. export const rename = 'filebrowser:rename';
  61. // For main browser only.
  62. export const share = 'filebrowser:share-main';
  63. // For main browser only.
  64. export const copyPath = 'filebrowser:copy-path';
  65. export const showBrowser = 'filebrowser:activate';
  66. export const shutdown = 'filebrowser:shutdown';
  67. // For main browser only.
  68. export const toggleBrowser = 'filebrowser:toggle-main';
  69. export const toggleNavigateToCurrentDirectory =
  70. 'filebrowser:toggle-navigate-to-current-directory';
  71. }
  72. /**
  73. * The default file browser extension.
  74. */
  75. const browser: JupyterFrontEndPlugin<void> = {
  76. activate: activateBrowser,
  77. id: '@jupyterlab/filebrowser-extension:browser',
  78. requires: [
  79. IFileBrowserFactory,
  80. IDocumentManager,
  81. ILabShell,
  82. ILayoutRestorer,
  83. ISettingRegistry
  84. ],
  85. optional: [ICommandPalette, IMainMenu],
  86. autoStart: true
  87. };
  88. /**
  89. * The default file browser factory provider.
  90. */
  91. const factory: JupyterFrontEndPlugin<IFileBrowserFactory> = {
  92. activate: activateFactory,
  93. id: '@jupyterlab/filebrowser-extension:factory',
  94. provides: IFileBrowserFactory,
  95. requires: [IDocumentManager],
  96. optional: [IStateDB, IRouter, JupyterFrontEnd.ITreeResolver]
  97. };
  98. /**
  99. * The default file browser share-file plugin
  100. *
  101. * This extension adds a "Copy Shareable Link" command that generates a copy-
  102. * pastable URL. This url can be used to open a particular file in JupyterLab,
  103. * handy for emailing links or bookmarking for reference.
  104. *
  105. * If you need to change how this link is generated (for instance, to copy a
  106. * /user-redirect URL for JupyterHub), disable this plugin and replace it
  107. * with another implementation.
  108. */
  109. const shareFile: JupyterFrontEndPlugin<void> = {
  110. activate: activateShareFile,
  111. id: '@jupyterlab/filebrowser-extension:share-file',
  112. requires: [IFileBrowserFactory],
  113. autoStart: true
  114. };
  115. /**
  116. * A plugin providing file upload status.
  117. */
  118. export const fileUploadStatus: JupyterFrontEndPlugin<void> = {
  119. id: '@jupyterlab/filebrowser-extension:file-upload-status',
  120. autoStart: true,
  121. requires: [IFileBrowserFactory],
  122. optional: [IStatusBar],
  123. activate: (
  124. app: JupyterFrontEnd,
  125. browser: IFileBrowserFactory,
  126. statusBar: IStatusBar | null
  127. ) => {
  128. if (!statusBar) {
  129. // Automatically disable if statusbar missing
  130. return;
  131. }
  132. const item = new FileUploadStatus({
  133. tracker: browser.tracker
  134. });
  135. statusBar.registerStatusItem(
  136. '@jupyterlab/filebrowser-extension:file-upload-status',
  137. {
  138. item,
  139. align: 'middle',
  140. isActive: () => {
  141. return !!item.model && item.model.items.length > 0;
  142. },
  143. activeStateChanged: item.model.stateChanged
  144. }
  145. );
  146. }
  147. };
  148. /**
  149. * The file browser namespace token.
  150. */
  151. const namespace = 'filebrowser';
  152. /**
  153. * Export the plugins as default.
  154. */
  155. const plugins: JupyterFrontEndPlugin<any>[] = [
  156. factory,
  157. browser,
  158. shareFile,
  159. fileUploadStatus
  160. ];
  161. export default plugins;
  162. /**
  163. * Activate the file browser factory provider.
  164. */
  165. async function activateFactory(
  166. app: JupyterFrontEnd,
  167. docManager: IDocumentManager,
  168. state: IStateDB | null,
  169. router: IRouter | null,
  170. tree: JupyterFrontEnd.ITreeResolver | null
  171. ): Promise<IFileBrowserFactory> {
  172. const { commands } = app;
  173. const tracker = new WidgetTracker<FileBrowser>({ namespace });
  174. const createFileBrowser = (
  175. id: string,
  176. options: IFileBrowserFactory.IOptions = {}
  177. ) => {
  178. const model = new FileBrowserModel({
  179. auto: options.auto ?? true,
  180. manager: docManager,
  181. driveName: options.driveName || '',
  182. refreshInterval: options.refreshInterval,
  183. state:
  184. options.state === null ? undefined : options.state || state || undefined
  185. });
  186. const restore = options.restore;
  187. const widget = new FileBrowser({ id, model, restore });
  188. // Add a launcher toolbar item.
  189. let launcher = new ToolbarButton({
  190. icon: addIcon,
  191. onClick: () => {
  192. return Private.createLauncher(commands, widget);
  193. },
  194. tooltip: 'New Launcher'
  195. });
  196. widget.toolbar.insertItem(0, 'launch', launcher);
  197. // Track the newly created file browser.
  198. void tracker.add(widget);
  199. return widget;
  200. };
  201. // Manually restore and load the default file browser.
  202. const defaultBrowser = createFileBrowser('filebrowser', {
  203. auto: false,
  204. restore: false
  205. });
  206. void Private.restoreBrowser(defaultBrowser, commands, router, tree);
  207. return { createFileBrowser, defaultBrowser, tracker };
  208. }
  209. /**
  210. * Activate the default file browser in the sidebar.
  211. */
  212. function activateBrowser(
  213. app: JupyterFrontEnd,
  214. factory: IFileBrowserFactory,
  215. docManager: IDocumentManager,
  216. labShell: ILabShell,
  217. restorer: ILayoutRestorer,
  218. settingRegistry: ISettingRegistry,
  219. commandPalette: ICommandPalette | null,
  220. mainMenu: IMainMenu | null
  221. ): void {
  222. const browser = factory.defaultBrowser;
  223. const { commands } = app;
  224. // Let the application restorer track the primary file browser (that is
  225. // automatically created) for restoration of application state (e.g. setting
  226. // the file browser as the current side bar widget).
  227. //
  228. // All other file browsers created by using the factory function are
  229. // responsible for their own restoration behavior, if any.
  230. restorer.add(browser, namespace);
  231. addCommands(
  232. app,
  233. factory,
  234. labShell,
  235. docManager,
  236. settingRegistry,
  237. commandPalette,
  238. mainMenu
  239. );
  240. browser.title.iconRenderer = folderIcon;
  241. // Show the current file browser shortcut in its title.
  242. const updateBrowserTitle = () => {
  243. const binding = find(
  244. app.commands.keyBindings,
  245. b => b.command === CommandIDs.toggleBrowser
  246. );
  247. if (binding) {
  248. const ks = CommandRegistry.formatKeystroke(binding.keys.join(' '));
  249. browser.title.caption = `File Browser (${ks})`;
  250. } else {
  251. browser.title.caption = 'File Browser';
  252. }
  253. };
  254. updateBrowserTitle();
  255. app.commands.keyBindingChanged.connect(() => {
  256. updateBrowserTitle();
  257. });
  258. labShell.add(browser, 'left', { rank: 100 });
  259. // If the layout is a fresh session without saved data, open file browser.
  260. void labShell.restored.then(layout => {
  261. if (layout.fresh) {
  262. void commands.execute(CommandIDs.showBrowser, void 0);
  263. }
  264. });
  265. void Promise.all([app.restored, browser.model.restored]).then(() => {
  266. function maybeCreate() {
  267. // Create a launcher if there are no open items.
  268. if (labShell.isEmpty('main')) {
  269. void Private.createLauncher(commands, browser);
  270. }
  271. }
  272. // When layout is modified, create a launcher if there are no open items.
  273. labShell.layoutModified.connect(() => {
  274. maybeCreate();
  275. });
  276. let navigateToCurrentDirectory: boolean = false;
  277. void settingRegistry
  278. .load('@jupyterlab/filebrowser-extension:browser')
  279. .then(settings => {
  280. settings.changed.connect(settings => {
  281. navigateToCurrentDirectory = settings.get(
  282. 'navigateToCurrentDirectory'
  283. ).composite as boolean;
  284. browser.navigateToCurrentDirectory = navigateToCurrentDirectory;
  285. });
  286. navigateToCurrentDirectory = settings.get('navigateToCurrentDirectory')
  287. .composite as boolean;
  288. browser.navigateToCurrentDirectory = navigateToCurrentDirectory;
  289. });
  290. // Whether to automatically navigate to a document's current directory
  291. labShell.currentChanged.connect(async (_, change) => {
  292. if (navigateToCurrentDirectory && change.newValue) {
  293. const { newValue } = change;
  294. const context = docManager.contextForWidget(newValue);
  295. if (context) {
  296. const { path } = context;
  297. try {
  298. await Private.navigateToPath(path, factory);
  299. labShell.currentWidget?.activate();
  300. } catch (reason) {
  301. console.warn(
  302. `${CommandIDs.goToPath} failed to open: ${path}`,
  303. reason
  304. );
  305. }
  306. }
  307. }
  308. });
  309. maybeCreate();
  310. });
  311. }
  312. function activateShareFile(
  313. app: JupyterFrontEnd,
  314. factory: IFileBrowserFactory
  315. ): void {
  316. const { commands } = app;
  317. const { tracker } = factory;
  318. commands.addCommand(CommandIDs.share, {
  319. execute: () => {
  320. const widget = tracker.currentWidget;
  321. const model = widget?.selectedItems().next();
  322. if (!model) {
  323. return;
  324. }
  325. const path = encodeURI(model.path);
  326. Clipboard.copyToSystem(URLExt.join(PageConfig.getTreeUrl(), path));
  327. },
  328. isVisible: () =>
  329. !!tracker.currentWidget &&
  330. toArray(tracker.currentWidget.selectedItems()).length === 1,
  331. iconClass: 'jp-MaterialIcon jp-LinkIcon',
  332. label: 'Copy Shareable Link'
  333. });
  334. }
  335. /**
  336. * Add the main file browser commands to the application's command registry.
  337. */
  338. function addCommands(
  339. app: JupyterFrontEnd,
  340. factory: IFileBrowserFactory,
  341. labShell: ILabShell,
  342. docManager: IDocumentManager,
  343. settingRegistry: ISettingRegistry,
  344. commandPalette: ICommandPalette | null,
  345. mainMenu: IMainMenu | null
  346. ): void {
  347. const { docRegistry: registry, commands } = app;
  348. const { defaultBrowser: browser, tracker } = factory;
  349. commands.addCommand(CommandIDs.del, {
  350. execute: () => {
  351. const widget = tracker.currentWidget;
  352. if (widget) {
  353. return widget.delete();
  354. }
  355. },
  356. iconClass: 'jp-MaterialIcon jp-CloseIcon',
  357. label: 'Delete',
  358. mnemonic: 0
  359. });
  360. commands.addCommand(CommandIDs.copy, {
  361. execute: () => {
  362. const widget = tracker.currentWidget;
  363. if (widget) {
  364. return widget.copy();
  365. }
  366. },
  367. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  368. label: 'Copy',
  369. mnemonic: 0
  370. });
  371. commands.addCommand(CommandIDs.cut, {
  372. execute: () => {
  373. const widget = tracker.currentWidget;
  374. if (widget) {
  375. return widget.cut();
  376. }
  377. },
  378. iconClass: 'jp-MaterialIcon jp-CutIcon',
  379. label: 'Cut'
  380. });
  381. commands.addCommand(CommandIDs.download, {
  382. execute: () => {
  383. const widget = tracker.currentWidget;
  384. if (widget) {
  385. return widget.download();
  386. }
  387. },
  388. iconClass: 'jp-MaterialIcon jp-DownloadIcon',
  389. label: 'Download'
  390. });
  391. commands.addCommand(CommandIDs.duplicate, {
  392. execute: () => {
  393. const widget = tracker.currentWidget;
  394. if (widget) {
  395. return widget.duplicate();
  396. }
  397. },
  398. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  399. label: 'Duplicate'
  400. });
  401. commands.addCommand(CommandIDs.hideBrowser, {
  402. execute: () => {
  403. const widget = tracker.currentWidget;
  404. if (widget && !widget.isHidden) {
  405. labShell.collapseLeft();
  406. }
  407. }
  408. });
  409. commands.addCommand(CommandIDs.goToPath, {
  410. execute: async args => {
  411. const path = (args.path as string) || '';
  412. try {
  413. const item = await Private.navigateToPath(path, factory);
  414. if (item.type !== 'directory') {
  415. const browserForPath = Private.getBrowserForPath(path, factory);
  416. if (browserForPath) {
  417. browserForPath.clearSelectedItems();
  418. const parts = path.split('/');
  419. const name = parts[parts.length - 1];
  420. if (name) {
  421. await browserForPath.selectItemByName(name);
  422. }
  423. }
  424. }
  425. } catch (reason) {
  426. console.warn(`${CommandIDs.goToPath} failed to go to: ${path}`, reason);
  427. }
  428. return commands.execute(CommandIDs.showBrowser, { path });
  429. }
  430. });
  431. commands.addCommand(CommandIDs.openPath, {
  432. label: args => (args.path ? `Open ${args.path}` : 'Open from Path…'),
  433. caption: args => (args.path ? `Open ${args.path}` : 'Open from path'),
  434. execute: async ({ path }: { path?: string }) => {
  435. if (!path) {
  436. path =
  437. (
  438. await InputDialog.getText({
  439. label: 'Path',
  440. placeholder: '/path/relative/to/jlab/root',
  441. title: 'Open Path',
  442. okLabel: 'Open'
  443. })
  444. ).value ?? undefined;
  445. }
  446. if (!path) {
  447. return;
  448. }
  449. try {
  450. let trailingSlash = path !== '/' && path.endsWith('/');
  451. if (trailingSlash) {
  452. // The normal contents service errors on paths ending in slash
  453. path = path.slice(0, path.length - 1);
  454. }
  455. const browserForPath = Private.getBrowserForPath(path, factory)!;
  456. const { services } = browserForPath.model.manager;
  457. const item = await services.contents.get(path, {
  458. content: false
  459. });
  460. if (trailingSlash && item.type !== 'directory') {
  461. throw new Error(`Path ${path}/ is not a directory`);
  462. }
  463. await commands.execute(CommandIDs.goToPath, { path });
  464. if (item.type === 'directory') {
  465. return;
  466. }
  467. return commands.execute('docmanager:open', { path });
  468. } catch (reason) {
  469. if (reason.response && reason.response.status === 404) {
  470. reason.message = `Could not find path: ${path}`;
  471. }
  472. return showErrorMessage('Cannot open', reason);
  473. }
  474. }
  475. });
  476. // Add the openPath command to the command palette
  477. if (commandPalette) {
  478. commandPalette.addItem({
  479. command: CommandIDs.openPath,
  480. category: 'File Operations'
  481. });
  482. }
  483. commands.addCommand(CommandIDs.open, {
  484. execute: args => {
  485. const factory = (args['factory'] as string) || void 0;
  486. const widget = tracker.currentWidget;
  487. if (!widget) {
  488. return;
  489. }
  490. const { contents } = widget.model.manager.services;
  491. return Promise.all(
  492. toArray(
  493. map(widget.selectedItems(), item => {
  494. if (item.type === 'directory') {
  495. const localPath = contents.localPath(item.path);
  496. return widget.model.cd(`/${localPath}`);
  497. }
  498. return commands.execute('docmanager:open', {
  499. factory: factory,
  500. path: item.path
  501. });
  502. })
  503. )
  504. );
  505. },
  506. iconClass: args => {
  507. const factory = (args['factory'] as string) || void 0;
  508. if (factory) {
  509. // if an explicit factory is passed...
  510. const ft = registry.getFileType(factory);
  511. // ...set an icon if the factory name corresponds to a file type name...
  512. // ...or leave the icon blank
  513. return ft?.iconClass ?? '';
  514. } else {
  515. return 'jp-MaterialIcon jp-FolderIcon';
  516. }
  517. },
  518. label: args => (args['label'] || args['factory'] || 'Open') as string,
  519. mnemonic: 0
  520. });
  521. commands.addCommand(CommandIDs.openBrowserTab, {
  522. execute: () => {
  523. const widget = tracker.currentWidget;
  524. if (!widget) {
  525. return;
  526. }
  527. return Promise.all(
  528. toArray(
  529. map(widget.selectedItems(), item => {
  530. return commands.execute('docmanager:open-browser-tab', {
  531. path: item.path
  532. });
  533. })
  534. )
  535. );
  536. },
  537. iconClass: 'jp-MaterialIcon jp-AddIcon',
  538. label: 'Open in New Browser Tab',
  539. mnemonic: 0
  540. });
  541. commands.addCommand(CommandIDs.copyDownloadLink, {
  542. execute: () => {
  543. const widget = tracker.currentWidget;
  544. if (!widget) {
  545. return;
  546. }
  547. return widget.model.manager.services.contents
  548. .getDownloadUrl(widget.selectedItems().next()!.path)
  549. .then(url => {
  550. Clipboard.copyToSystem(url);
  551. });
  552. },
  553. iconClass: 'jp-MaterialIcon jp-CopyIcon',
  554. label: 'Copy Download Link',
  555. mnemonic: 0
  556. });
  557. commands.addCommand(CommandIDs.paste, {
  558. execute: () => {
  559. const widget = tracker.currentWidget;
  560. if (widget) {
  561. return widget.paste();
  562. }
  563. },
  564. iconClass: 'jp-MaterialIcon jp-PasteIcon',
  565. label: 'Paste',
  566. mnemonic: 0
  567. });
  568. commands.addCommand(CommandIDs.createNewDirectory, {
  569. execute: () => {
  570. const widget = tracker.currentWidget;
  571. if (widget) {
  572. return widget.createNewDirectory();
  573. }
  574. },
  575. iconClass: 'jp-MaterialIcon jp-NewFolderIcon',
  576. label: 'New Folder'
  577. });
  578. commands.addCommand(CommandIDs.createNewFile, {
  579. execute: () => {
  580. const {
  581. model: { path }
  582. } = browser;
  583. void commands.execute('docmanager:new-untitled', {
  584. path,
  585. type: 'file',
  586. ext: 'txt'
  587. });
  588. },
  589. iconClass: 'jp-MaterialIcon jp-TextEditorIcon',
  590. label: 'New File'
  591. });
  592. commands.addCommand(CommandIDs.createNewMarkdownFile, {
  593. execute: () => {
  594. const {
  595. model: { path }
  596. } = browser;
  597. void commands.execute('docmanager:new-untitled', {
  598. path,
  599. type: 'file',
  600. ext: 'md'
  601. });
  602. },
  603. iconClass: 'jp-MaterialIcon jp-MarkdownIcon',
  604. label: 'New Markdown File'
  605. });
  606. commands.addCommand(CommandIDs.rename, {
  607. execute: args => {
  608. const widget = tracker.currentWidget;
  609. if (widget) {
  610. return widget.rename();
  611. }
  612. },
  613. iconClass: 'jp-MaterialIcon jp-EditIcon',
  614. label: 'Rename',
  615. mnemonic: 0
  616. });
  617. commands.addCommand(CommandIDs.copyPath, {
  618. execute: () => {
  619. const widget = tracker.currentWidget;
  620. if (!widget) {
  621. return;
  622. }
  623. const item = widget.selectedItems().next();
  624. if (!item) {
  625. return;
  626. }
  627. Clipboard.copyToSystem(item.path);
  628. },
  629. isVisible: () =>
  630. !!tracker.currentWidget &&
  631. tracker.currentWidget.selectedItems().next !== undefined,
  632. iconClass: 'jp-MaterialIcon jp-FileIcon',
  633. label: 'Copy Path'
  634. });
  635. commands.addCommand(CommandIDs.showBrowser, {
  636. execute: args => {
  637. const path = (args.path as string) || '';
  638. const browserForPath = Private.getBrowserForPath(path, factory);
  639. // Check for browser not found
  640. if (!browserForPath) {
  641. return;
  642. }
  643. // Shortcut if we are using the main file browser
  644. if (browser === browserForPath) {
  645. labShell.activateById(browser.id);
  646. return;
  647. } else {
  648. const areas: ILabShell.Area[] = ['left', 'right'];
  649. for (let area of areas) {
  650. const it = labShell.widgets(area);
  651. let widget = it.next();
  652. while (widget) {
  653. if (widget.contains(browserForPath)) {
  654. labShell.activateById(widget.id);
  655. return;
  656. }
  657. widget = it.next();
  658. }
  659. }
  660. }
  661. }
  662. });
  663. commands.addCommand(CommandIDs.shutdown, {
  664. execute: () => {
  665. const widget = tracker.currentWidget;
  666. if (widget) {
  667. return widget.shutdownKernels();
  668. }
  669. },
  670. iconClass: 'jp-MaterialIcon jp-StopIcon',
  671. label: 'Shut Down Kernel'
  672. });
  673. commands.addCommand(CommandIDs.toggleBrowser, {
  674. execute: () => {
  675. if (browser.isHidden) {
  676. return commands.execute(CommandIDs.showBrowser, void 0);
  677. }
  678. return commands.execute(CommandIDs.hideBrowser, void 0);
  679. }
  680. });
  681. commands.addCommand(CommandIDs.createLauncher, {
  682. label: 'New Launcher',
  683. execute: () => Private.createLauncher(commands, browser)
  684. });
  685. commands.addCommand(CommandIDs.toggleNavigateToCurrentDirectory, {
  686. label: 'Show Active File in File Browser',
  687. isToggled: () => browser.navigateToCurrentDirectory,
  688. execute: () => {
  689. const value = !browser.navigateToCurrentDirectory;
  690. const key = 'navigateToCurrentDirectory';
  691. return settingRegistry
  692. .set('@jupyterlab/filebrowser-extension:browser', key, value)
  693. .catch((reason: Error) => {
  694. console.error(`Failed to set navigateToCurrentDirectory setting`);
  695. });
  696. }
  697. });
  698. if (mainMenu) {
  699. mainMenu.settingsMenu.addGroup(
  700. [{ command: CommandIDs.toggleNavigateToCurrentDirectory }],
  701. 5
  702. );
  703. }
  704. if (commandPalette) {
  705. commandPalette.addItem({
  706. command: CommandIDs.toggleNavigateToCurrentDirectory,
  707. category: 'File Operations'
  708. });
  709. }
  710. /**
  711. * A menu widget that dynamically populates with different widget factories
  712. * based on current filebrowser selection.
  713. */
  714. class OpenWithMenu extends Menu {
  715. protected onBeforeAttach(msg: Message): void {
  716. // clear the current menu items
  717. this.clearItems();
  718. // get the widget factories that could be used to open all of the items
  719. // in the current filebrowser selection
  720. let factories = tracker.currentWidget
  721. ? OpenWithMenu._intersection(
  722. map(tracker.currentWidget.selectedItems(), i => {
  723. return OpenWithMenu._getFactories(i);
  724. })
  725. )
  726. : undefined;
  727. if (factories) {
  728. // make new menu items from the widget factories
  729. factories.forEach(factory => {
  730. this.addItem({
  731. args: { factory: factory },
  732. command: CommandIDs.open
  733. });
  734. });
  735. }
  736. super.onBeforeAttach(msg);
  737. }
  738. static _getFactories(item: Contents.IModel): Array<string> {
  739. let factories = registry
  740. .preferredWidgetFactories(item.path)
  741. .map(f => f.name);
  742. const notebookFactory = registry.getWidgetFactory('notebook')?.name;
  743. if (
  744. notebookFactory &&
  745. item.type === 'notebook' &&
  746. factories.indexOf(notebookFactory) === -1
  747. ) {
  748. factories.unshift(notebookFactory);
  749. }
  750. return factories;
  751. }
  752. static _intersection<T>(iter: IIterator<Array<T>>): Set<T> | void {
  753. // pop the first element of iter
  754. let first = iter.next();
  755. // first will be undefined if iter is empty
  756. if (!first) {
  757. return;
  758. }
  759. // "initialize" the intersection from first
  760. let isect = new Set(first);
  761. // reduce over the remaining elements of iter
  762. return reduce(
  763. iter,
  764. (isect, subarr) => {
  765. // filter out all elements not present in both isect and subarr,
  766. // accumulate result in new set
  767. return new Set(subarr.filter(x => isect.has(x)));
  768. },
  769. isect
  770. );
  771. }
  772. }
  773. // matches anywhere on filebrowser
  774. const selectorContent = '.jp-DirListing-content';
  775. // matches all filebrowser items
  776. const selectorItem = '.jp-DirListing-item[data-isdir]';
  777. // matches only non-directory items
  778. const selectorNotDir = '.jp-DirListing-item[data-isdir="false"]';
  779. // If the user did not click on any file, we still want to show paste and new folder,
  780. // so target the content rather than an item.
  781. app.contextMenu.addItem({
  782. command: CommandIDs.createNewDirectory,
  783. selector: selectorContent,
  784. rank: 1
  785. });
  786. app.contextMenu.addItem({
  787. command: CommandIDs.createNewFile,
  788. selector: selectorContent,
  789. rank: 2
  790. });
  791. app.contextMenu.addItem({
  792. command: CommandIDs.createNewMarkdownFile,
  793. selector: selectorContent,
  794. rank: 3
  795. });
  796. app.contextMenu.addItem({
  797. command: CommandIDs.paste,
  798. selector: selectorContent,
  799. rank: 4
  800. });
  801. app.contextMenu.addItem({
  802. command: CommandIDs.open,
  803. selector: selectorItem,
  804. rank: 1
  805. });
  806. const openWith = new OpenWithMenu({ commands });
  807. openWith.title.label = 'Open With';
  808. app.contextMenu.addItem({
  809. type: 'submenu',
  810. submenu: openWith,
  811. selector: selectorNotDir,
  812. rank: 2
  813. });
  814. app.contextMenu.addItem({
  815. command: CommandIDs.openBrowserTab,
  816. selector: selectorNotDir,
  817. rank: 3
  818. });
  819. app.contextMenu.addItem({
  820. command: CommandIDs.rename,
  821. selector: selectorItem,
  822. rank: 4
  823. });
  824. app.contextMenu.addItem({
  825. command: CommandIDs.del,
  826. selector: selectorItem,
  827. rank: 5
  828. });
  829. app.contextMenu.addItem({
  830. command: CommandIDs.cut,
  831. selector: selectorItem,
  832. rank: 6
  833. });
  834. app.contextMenu.addItem({
  835. command: CommandIDs.copy,
  836. selector: selectorNotDir,
  837. rank: 7
  838. });
  839. app.contextMenu.addItem({
  840. command: CommandIDs.duplicate,
  841. selector: selectorNotDir,
  842. rank: 8
  843. });
  844. app.contextMenu.addItem({
  845. command: CommandIDs.download,
  846. selector: selectorNotDir,
  847. rank: 9
  848. });
  849. app.contextMenu.addItem({
  850. command: CommandIDs.shutdown,
  851. selector: selectorNotDir,
  852. rank: 10
  853. });
  854. app.contextMenu.addItem({
  855. command: CommandIDs.share,
  856. selector: selectorItem,
  857. rank: 11
  858. });
  859. app.contextMenu.addItem({
  860. command: CommandIDs.copyPath,
  861. selector: selectorItem,
  862. rank: 12
  863. });
  864. app.contextMenu.addItem({
  865. command: CommandIDs.copyDownloadLink,
  866. selector: selectorNotDir,
  867. rank: 13
  868. });
  869. }
  870. /**
  871. * A namespace for private module data.
  872. */
  873. namespace Private {
  874. /**
  875. * Create a launcher for a given filebrowser widget.
  876. */
  877. export function createLauncher(
  878. commands: CommandRegistry,
  879. browser: FileBrowser
  880. ): Promise<MainAreaWidget<Launcher>> {
  881. const { model } = browser;
  882. return commands
  883. .execute('launcher:create', { cwd: model.path })
  884. .then((launcher: MainAreaWidget<Launcher>) => {
  885. model.pathChanged.connect(() => {
  886. if (launcher.content) {
  887. launcher.content.cwd = model.path;
  888. }
  889. }, launcher);
  890. return launcher;
  891. });
  892. }
  893. /**
  894. * Get browser object given file path.
  895. */
  896. export function getBrowserForPath(
  897. path: string,
  898. factory: IFileBrowserFactory
  899. ): FileBrowser | undefined {
  900. const { defaultBrowser: browser, tracker } = factory;
  901. const driveName = browser.model.manager.services.contents.driveName(path);
  902. if (driveName) {
  903. let browserForPath = tracker.find(
  904. _path => _path.model.driveName === driveName
  905. );
  906. if (!browserForPath) {
  907. // warn that no filebrowser could be found for this driveName
  908. console.warn(
  909. `${CommandIDs.goToPath} failed to find filebrowser for path: ${path}`
  910. );
  911. return;
  912. }
  913. return browserForPath;
  914. }
  915. // if driveName is empty, assume the main filebrowser
  916. return browser;
  917. }
  918. /**
  919. * Navigate to a path or the path containing a file.
  920. */
  921. export async function navigateToPath(
  922. path: string,
  923. factory: IFileBrowserFactory
  924. ): Promise<Contents.IModel> {
  925. const browserForPath = Private.getBrowserForPath(path, factory);
  926. if (!browserForPath) {
  927. throw new Error('No browser for path');
  928. }
  929. const { services } = browserForPath.model.manager;
  930. const localPath = services.contents.localPath(path);
  931. await services.ready;
  932. let item = await services.contents.get(path, { content: false });
  933. const { model } = browserForPath;
  934. await model.restored;
  935. if (item.type === 'directory') {
  936. await model.cd(`/${localPath}`);
  937. } else {
  938. await model.cd(`/${PathExt.dirname(localPath)}`);
  939. }
  940. return item;
  941. }
  942. /**
  943. * Restores file browser state and overrides state if tree resolver resolves.
  944. */
  945. export async function restoreBrowser(
  946. browser: FileBrowser,
  947. commands: CommandRegistry,
  948. router: IRouter | null,
  949. tree: JupyterFrontEnd.ITreeResolver | null
  950. ): Promise<void> {
  951. const restoring = 'jp-mod-restoring';
  952. browser.addClass(restoring);
  953. if (!router) {
  954. await browser.model.restore(browser.id);
  955. await browser.model.refresh();
  956. browser.removeClass(restoring);
  957. return;
  958. }
  959. const listener = async () => {
  960. router.routed.disconnect(listener);
  961. const paths = await tree?.paths;
  962. if (paths?.file || paths?.browser) {
  963. // Restore the model without populating it.
  964. await browser.model.restore(browser.id, false);
  965. if (paths.file) {
  966. await commands.execute(CommandIDs.openPath, { path: paths.file });
  967. }
  968. if (paths.browser) {
  969. await commands.execute(CommandIDs.openPath, { path: paths.browser });
  970. }
  971. } else {
  972. await browser.model.restore(browser.id);
  973. await browser.model.refresh();
  974. }
  975. browser.removeClass(restoring);
  976. };
  977. router.routed.connect(listener);
  978. }
  979. }