index.ts 31 KB

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