manager.ts 15 KB

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