manager.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IClientSession
  5. } from '@jupyterlab/apputils';
  6. import {
  7. uuid
  8. } from '@jupyterlab/coreutils';
  9. import {
  10. DocumentRegistry, Context
  11. } from '@jupyterlab/docregistry';
  12. import {
  13. Contents, Kernel, ServiceManager
  14. } from '@jupyterlab/services';
  15. import {
  16. ArrayExt, each, find, map, toArray
  17. } from '@phosphor/algorithm';
  18. import {
  19. Token
  20. } from '@phosphor/coreutils';
  21. import {
  22. IDisposable
  23. } from '@phosphor/disposable';
  24. import {
  25. AttachedProperty
  26. } from '@phosphor/properties';
  27. import {
  28. ISignal, Signal
  29. } from '@phosphor/signaling';
  30. import {
  31. Widget
  32. } from '@phosphor/widgets';
  33. import {
  34. SaveHandler
  35. } from './savehandler';
  36. import {
  37. DocumentWidgetManager
  38. } from './widgetmanager';
  39. /* tslint:disable */
  40. /**
  41. * The document registry token.
  42. */
  43. export
  44. const IDocumentManager = new Token<IDocumentManager>('@jupyterlab/docmanager:IDocumentManager');
  45. /* tslint:enable */
  46. /**
  47. * The interface for a document manager.
  48. */
  49. export
  50. interface IDocumentManager extends DocumentManager {}
  51. /**
  52. * The document manager.
  53. *
  54. * #### Notes
  55. * The document manager is used to register model and widget creators,
  56. * and the file browser uses the document manager to create widgets. The
  57. * document manager maintains a context for each path and model type that is
  58. * open, and a list of widgets for each context. The document manager is in
  59. * control of the proper closing and disposal of the widgets and contexts.
  60. */
  61. export
  62. class DocumentManager implements IDisposable {
  63. /**
  64. * Construct a new document manager.
  65. */
  66. constructor(options: DocumentManager.IOptions) {
  67. this.registry = options.registry;
  68. this.services = options.manager;
  69. this._opener = options.opener;
  70. let widgetManager = new DocumentWidgetManager({ registry: this.registry });
  71. widgetManager.activateRequested.connect(this._onActivateRequested, this);
  72. this._widgetManager = widgetManager;
  73. }
  74. /**
  75. * The registry used by the manager.
  76. */
  77. readonly registry: DocumentRegistry;
  78. /**
  79. * The service manager used by the manager.
  80. */
  81. readonly services: ServiceManager.IManager;
  82. /**
  83. * A signal emitted when one of the documents is activated.
  84. */
  85. get activateRequested(): ISignal<this, string> {
  86. return this._activateRequested;
  87. }
  88. /**
  89. * Get whether the document manager has been disposed.
  90. */
  91. get isDisposed(): boolean {
  92. return this._isDisposed;
  93. }
  94. /**
  95. * Dispose of the resources held by the document manager.
  96. */
  97. dispose(): void {
  98. if (this.isDisposed) {
  99. return;
  100. }
  101. this._isDisposed = true;
  102. Signal.clearData(this);
  103. each(toArray(this._contexts), context => {
  104. this._widgetManager.closeWidgets(context);
  105. });
  106. this._widgetManager.dispose();
  107. this._contexts.length = 0;
  108. }
  109. /**
  110. * Clone a widget.
  111. *
  112. * @param widget - The source widget.
  113. *
  114. * @returns A new widget or `undefined`.
  115. *
  116. * #### Notes
  117. * Uses the same widget factory and context as the source, or returns
  118. * `undefined` if the source widget is not managed by this manager.
  119. */
  120. cloneWidget(widget: Widget): DocumentRegistry.IReadyWidget | undefined {
  121. return this._widgetManager.cloneWidget(widget);
  122. }
  123. /**
  124. * Close all of the open documents.
  125. */
  126. closeAll(): Promise<void> {
  127. return Promise.all(
  128. toArray(map(this._contexts, context => {
  129. return this._widgetManager.closeWidgets(context);
  130. }))
  131. ).then(() => undefined);
  132. }
  133. /**
  134. * Close the widgets associated with a given path.
  135. *
  136. * @param path - The target path.
  137. */
  138. closeFile(path: string): Promise<void> {
  139. let context = this._contextForPath(path);
  140. if (context) {
  141. return this._widgetManager.closeWidgets(context);
  142. }
  143. return Promise.resolve(void 0);
  144. }
  145. /**
  146. * Get the document context for a widget.
  147. *
  148. * @param widget - The widget of interest.
  149. *
  150. * @returns The context associated with the widget, or `undefined`.
  151. */
  152. contextForWidget(widget: Widget): DocumentRegistry.Context | undefined {
  153. return this._widgetManager.contextForWidget(widget);
  154. }
  155. /**
  156. * Copy a file.
  157. *
  158. * @param fromFile - The full path of the original file.
  159. *
  160. * @param toDir - The full path to the target directory.
  161. *
  162. * @returns A promise which resolves to the contents of the file.
  163. */
  164. copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
  165. return this.services.contents.copy(fromFile, toDir);
  166. }
  167. /**
  168. * Create a new file and return the widget used to view it.
  169. *
  170. * @param path - The file path to create.
  171. *
  172. * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
  173. *
  174. * @param kernel - An optional kernel name/id to override the default.
  175. *
  176. * @returns The created widget, or `undefined`.
  177. *
  178. * #### Notes
  179. * This function will return `undefined` if a valid widget factory
  180. * cannot be found.
  181. */
  182. createNew(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): Widget {
  183. return this._createOrOpenDocument('create', path, widgetName, kernel);
  184. }
  185. /**
  186. * Delete a file.
  187. *
  188. * @param path - The full path to the file to be deleted.
  189. *
  190. * @returns A promise which resolves when the file is deleted.
  191. *
  192. * #### Notes
  193. * If there is a running session associated with the file and no other
  194. * sessions are using the kernel, the session will be shut down.
  195. */
  196. deleteFile(path: string): Promise<void> {
  197. return this.services.sessions.stopIfNeeded(path).then(() => {
  198. return this.services.contents.delete(path);
  199. })
  200. .then(() => {
  201. let context = this._contextForPath(path);
  202. if (context) {
  203. return this._widgetManager.deleteWidgets(context);
  204. }
  205. return Promise.resolve(void 0);
  206. });
  207. }
  208. /**
  209. * See if a widget already exists for the given path and widget name.
  210. *
  211. * @param path - The file path to use.
  212. *
  213. * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
  214. *
  215. * @returns The found widget, or `undefined`.
  216. *
  217. * #### Notes
  218. * This can be used to use an existing widget instead of opening
  219. * a new widget.
  220. */
  221. findWidget(path: string, widgetName='default'): DocumentRegistry.IReadyWidget | undefined {
  222. if (widgetName === 'default') {
  223. let factory = this.registry.defaultWidgetFactory(path);
  224. if (!factory) {
  225. return undefined;
  226. }
  227. widgetName = factory.name;
  228. }
  229. let context = this._contextForPath(path);
  230. if (context) {
  231. return this._widgetManager.findWidget(context, widgetName);
  232. }
  233. return undefined;
  234. }
  235. /**
  236. * Create a new untitled file.
  237. *
  238. * @param options - The file content creation options.
  239. */
  240. newUntitled(options: Contents.ICreateOptions): Promise<Contents.IModel> {
  241. if (options.type === 'file') {
  242. options.ext = options.ext || '.txt';
  243. }
  244. return this.services.contents.newUntitled(options);
  245. }
  246. /**
  247. * Open a file and return the widget used to view it.
  248. *
  249. * @param path - The file path to open.
  250. *
  251. * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
  252. *
  253. * @param kernel - An optional kernel name/id to override the default.
  254. *
  255. * @returns The created widget, or `undefined`.
  256. *
  257. * #### Notes
  258. * This function will return `undefined` if a valid widget factory
  259. * cannot be found.
  260. */
  261. open(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>, options?: DocumentRegistry.IOpenOptions ): DocumentRegistry.IReadyWidget | undefined {
  262. return this._createOrOpenDocument('open', path, widgetName, kernel, options);
  263. }
  264. /**
  265. * Open a file and return the widget used to view it.
  266. * Reveals an already existing editor.
  267. *
  268. * @param path - The file path to open.
  269. *
  270. * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
  271. *
  272. * @param kernel - An optional kernel name/id to override the default.
  273. *
  274. * @returns The created widget, or `undefined`.
  275. *
  276. * #### Notes
  277. * This function will return `undefined` if a valid widget factory
  278. * cannot be found.
  279. */
  280. openOrReveal(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>, options?: DocumentRegistry.IOpenOptions ): DocumentRegistry.IReadyWidget | undefined {
  281. let widget = this.findWidget(path, widgetName);
  282. if (widget) {
  283. this._opener.open(widget, options || {});
  284. return widget;
  285. }
  286. return this.open(path, widgetName, kernel, options || {});
  287. }
  288. /**
  289. * Overwrite a file.
  290. *
  291. * @param oldPath - The full path to the original file.
  292. *
  293. * @param newPath - The full path to the new file.
  294. *
  295. * @returns A promise containing the new file contents model.
  296. */
  297. overwrite(oldPath: string, newPath: string): Promise<Contents.IModel> {
  298. // Cleanly overwrite the file by moving it, making sure the original does
  299. // not exist, and then renaming to the new path.
  300. const tempPath = `${newPath}.${uuid()}`;
  301. const cb = () => this.rename(tempPath, newPath);
  302. return this.rename(oldPath, tempPath).then(() => {
  303. return this.deleteFile(newPath);
  304. }).then(cb, cb);
  305. }
  306. /**
  307. * Rename a file or directory.
  308. *
  309. * @param oldPath - The full path to the original file.
  310. *
  311. * @param newPath - The full path to the new file.
  312. *
  313. * @returns A promise containing the new file contents model. The promise
  314. * will reject if the newPath already exists. Use [[overwrite]] to overwrite
  315. * a file.
  316. */
  317. rename(oldPath: string, newPath: string): Promise<Contents.IModel> {
  318. return this.services.contents.rename(oldPath, newPath);
  319. }
  320. /**
  321. * Find a context for a given path and factory name.
  322. */
  323. private _findContext(path: string, factoryName: string): Private.IContext | undefined {
  324. return find(this._contexts, context => {
  325. return context.factoryName === factoryName && context.path === path;
  326. });
  327. }
  328. /**
  329. * Get a context for a given path.
  330. */
  331. private _contextForPath(path: string): Private.IContext | undefined {
  332. return find(this._contexts, context => context.path === path);
  333. }
  334. /**
  335. * Create a context from a path and a model factory.
  336. */
  337. private _createContext(path: string, factory: DocumentRegistry.ModelFactory, kernelPreference: IClientSession.IKernelPreference): Private.IContext {
  338. // Allow options to be passed when adding a sibling.
  339. let adopter = (widget: Widget, options?: DocumentRegistry.IOpenOptions) => {
  340. this._widgetManager.adoptWidget(context, widget);
  341. this._opener.open(widget, options);
  342. };
  343. let modelDBFactory = this.services.contents.getModelDBFactory(path) || undefined;
  344. let context = new Context({
  345. opener: adopter,
  346. manager: this.services,
  347. factory,
  348. path,
  349. kernelPreference,
  350. modelDBFactory
  351. });
  352. let handler = new SaveHandler({ context });
  353. Private.saveHandlerProperty.set(context, handler);
  354. context.ready.then(() => {
  355. handler.start();
  356. });
  357. context.disposed.connect(this._onContextDisposed, this);
  358. this._contexts.push(context);
  359. return context;
  360. }
  361. /**
  362. * Handle a context disposal.
  363. */
  364. private _onContextDisposed(context: Private.IContext): void {
  365. ArrayExt.removeFirstOf(this._contexts, context);
  366. }
  367. /**
  368. * Get the widget factory for a given widget name.
  369. */
  370. private _widgetFactoryFor(path: string, widgetName: string): DocumentRegistry.WidgetFactory | undefined {
  371. let { registry } = this;
  372. if (widgetName === 'default') {
  373. let factory = registry.defaultWidgetFactory(path);
  374. if (!factory) {
  375. return undefined;
  376. }
  377. widgetName = factory.name;
  378. }
  379. return registry.getWidgetFactory(widgetName);
  380. }
  381. /**
  382. * Creates a new document, or loads one from disk, depending on the `which` argument.
  383. * If `which==='create'`, then it creates a new document. If `which==='open'`,
  384. * then it loads the document from disk.
  385. *
  386. * The two cases differ in how the document context is handled, but the creation
  387. * of the widget and launching of the kernel are identical.
  388. */
  389. private _createOrOpenDocument(which: 'open'|'create', path: string, widgetName='default', kernel?: Partial<Kernel.IModel>, options?: DocumentRegistry.IOpenOptions): DocumentRegistry.IReadyWidget | undefined {
  390. let widgetFactory = this._widgetFactoryFor(path, widgetName);
  391. if (!widgetFactory) {
  392. return undefined;
  393. }
  394. let modelName = widgetFactory.modelName || 'text';
  395. let factory = this.registry.getModelFactory(modelName);
  396. if (!factory) {
  397. return undefined;
  398. }
  399. // Handle the kernel pereference.
  400. let preference = this.registry.getKernelPreference(
  401. path, widgetFactory.name, kernel
  402. );
  403. let context: Private.IContext | null = null;
  404. // Handle the load-from-disk case
  405. if (which === 'open') {
  406. // Use an existing context if available.
  407. context = this._findContext(path, factory.name) || null;
  408. if (!context) {
  409. context = this._createContext(path, factory, preference);
  410. // Populate the model, either from disk or a
  411. // model backend.
  412. context.fromStore();
  413. }
  414. } else if (which === 'create') {
  415. context = this._createContext(path, factory, preference);
  416. // Immediately save the contents to disk.
  417. context.save();
  418. }
  419. let widget = this._widgetManager.createWidget(widgetFactory, context!);
  420. this._opener.open(widget, options || {});
  421. return widget;
  422. }
  423. /**
  424. * Handle an activateRequested signal from the widget manager.
  425. */
  426. private _onActivateRequested(sender: DocumentWidgetManager, args: string): void {
  427. this._activateRequested.emit(args);
  428. }
  429. private _activateRequested = new Signal<this, string>(this);
  430. private _contexts: Private.IContext[] = [];
  431. private _opener: DocumentManager.IWidgetOpener;
  432. private _widgetManager: DocumentWidgetManager;
  433. private _isDisposed = false;
  434. }
  435. /**
  436. * A namespace for document manager statics.
  437. */
  438. export
  439. namespace DocumentManager {
  440. /**
  441. * The options used to initialize a document manager.
  442. */
  443. export
  444. interface IOptions {
  445. /**
  446. * A document registry instance.
  447. */
  448. registry: DocumentRegistry;
  449. /**
  450. * A service manager instance.
  451. */
  452. manager: ServiceManager.IManager;
  453. /**
  454. * A widget opener for sibling widgets.
  455. */
  456. opener: IWidgetOpener;
  457. }
  458. /**
  459. * An interface for a widget opener.
  460. */
  461. export
  462. interface IWidgetOpener {
  463. /**
  464. * Open the given widget.
  465. */
  466. open(widget: Widget, options?: DocumentRegistry.IOpenOptions): void;
  467. }
  468. }
  469. /**
  470. * A namespace for private data.
  471. */
  472. namespace Private {
  473. /**
  474. * An attached property for a context save handler.
  475. */
  476. export
  477. const saveHandlerProperty = new AttachedProperty<DocumentRegistry.Context, SaveHandler | undefined>({
  478. name: 'saveHandler',
  479. create: () => undefined
  480. });
  481. /**
  482. * A type alias for a standard context.
  483. */
  484. export
  485. interface IContext extends Context<DocumentRegistry.IModel> { /* no op */ }
  486. }