123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { ISessionContext, sessionContextDialogs } from '@jupyterlab/apputils';
- import { PathExt } from '@jupyterlab/coreutils';
- import { IDocumentProviderFactory } from '@jupyterlab/docprovider';
- import {
- Context,
- DocumentRegistry,
- IDocumentWidget
- } from '@jupyterlab/docregistry';
- import { Contents, Kernel, ServiceManager } from '@jupyterlab/services';
- import { ITranslator, nullTranslator } from '@jupyterlab/translation';
- import { ArrayExt, find } from '@lumino/algorithm';
- import { UUID } from '@lumino/coreutils';
- import { IDisposable } from '@lumino/disposable';
- import { AttachedProperty } from '@lumino/properties';
- import { ISignal, Signal } from '@lumino/signaling';
- import { Widget } from '@lumino/widgets';
- import { SaveHandler } from './savehandler';
- import { IDocumentManager } from './tokens';
- import { DocumentWidgetManager } from './widgetmanager';
- /**
- * The document manager.
- *
- * #### Notes
- * The document manager is used to register model and widget creators,
- * and the file browser uses the document manager to create widgets. The
- * document manager maintains a context for each path and model type that is
- * open, and a list of widgets for each context. The document manager is in
- * control of the proper closing and disposal of the widgets and contexts.
- */
- export class DocumentManager implements IDocumentManager {
- /**
- * Construct a new document manager.
- */
- constructor(options: DocumentManager.IOptions) {
- this.translator = options.translator || nullTranslator;
- this.registry = options.registry;
- this.services = options.manager;
- this._collaborative = !!options.collaborative;
- this._dialogs = options.sessionDialogs || sessionContextDialogs;
- this._docProviderFactory = options.docProviderFactory;
- this._opener = options.opener;
- this._when = options.when || options.manager.ready;
- const widgetManager = new DocumentWidgetManager({
- registry: this.registry,
- translator: this.translator
- });
- widgetManager.activateRequested.connect(this._onActivateRequested, this);
- this._widgetManager = widgetManager;
- this._setBusy = options.setBusy;
- }
- /**
- * The registry used by the manager.
- */
- readonly registry: DocumentRegistry;
- /**
- * The service manager used by the manager.
- */
- readonly services: ServiceManager.IManager;
- /**
- * A signal emitted when one of the documents is activated.
- */
- get activateRequested(): ISignal<this, string> {
- return this._activateRequested;
- }
- /**
- * Whether to autosave documents.
- */
- get autosave(): boolean {
- return this._autosave;
- }
- set autosave(value: boolean) {
- this._autosave = value;
- // For each existing context, start/stop the autosave handler as needed.
- this._contexts.forEach(context => {
- const handler = Private.saveHandlerProperty.get(context);
- if (!handler) {
- return;
- }
- if (value === true && !handler.isActive) {
- handler.start();
- } else if (value === false && handler.isActive) {
- handler.stop();
- }
- });
- }
- /**
- * Determines the time interval for autosave in seconds.
- */
- get autosaveInterval(): number {
- return this._autosaveInterval;
- }
- set autosaveInterval(value: number) {
- this._autosaveInterval = value;
- // For each existing context, set the save interval as needed.
- this._contexts.forEach(context => {
- const handler = Private.saveHandlerProperty.get(context);
- if (!handler) {
- return;
- }
- handler.saveInterval = value || 120;
- });
- }
- /**
- * Defines max acceptable difference, in milliseconds, between last modified timestamps on disk and client
- */
- get lastModifiedCheckMargin(): number {
- return this._lastModifiedCheckMargin;
- }
- set lastModifiedCheckMargin(value: number) {
- this._lastModifiedCheckMargin = value;
- // For each existing context, update the margin value.
- this._contexts.forEach(context => {
- context.lastModifiedCheckMargin = value;
- });
- }
- /**
- * Get whether the document manager has been disposed.
- */
- get isDisposed(): boolean {
- return this._isDisposed;
- }
- /**
- * Dispose of the resources held by the document manager.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._isDisposed = true;
- // Clear any listeners for our signals.
- Signal.clearData(this);
- // Close all the widgets for our contexts and dispose the widget manager.
- this._contexts.forEach(context => {
- return this._widgetManager.closeWidgets(context);
- });
- this._widgetManager.dispose();
- // Clear the context list.
- this._contexts.length = 0;
- }
- /**
- * Clone a widget.
- *
- * @param widget - The source widget.
- *
- * @returns A new widget or `undefined`.
- *
- * #### Notes
- * Uses the same widget factory and context as the source, or returns
- * `undefined` if the source widget is not managed by this manager.
- */
- cloneWidget(widget: Widget): IDocumentWidget | undefined {
- return this._widgetManager.cloneWidget(widget);
- }
- /**
- * Close all of the open documents.
- *
- * @returns A promise resolving when the widgets are closed.
- */
- closeAll(): Promise<void> {
- return Promise.all(
- this._contexts.map(context => this._widgetManager.closeWidgets(context))
- ).then(() => undefined);
- }
- /**
- * Close the widgets associated with a given path.
- *
- * @param path - The target path.
- *
- * @returns A promise resolving when the widgets are closed.
- */
- closeFile(path: string): Promise<void> {
- const close = this._contextsForPath(path).map(c =>
- this._widgetManager.closeWidgets(c)
- );
- return Promise.all(close).then(x => undefined);
- }
- /**
- * Get the document context for a widget.
- *
- * @param widget - The widget of interest.
- *
- * @returns The context associated with the widget, or `undefined` if no such
- * context exists.
- */
- contextForWidget(widget: Widget): DocumentRegistry.Context | undefined {
- return this._widgetManager.contextForWidget(widget);
- }
- /**
- * Copy a file.
- *
- * @param fromFile - The full path of the original file.
- *
- * @param toDir - The full path to the target directory.
- *
- * @returns A promise which resolves to the contents of the file.
- */
- copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
- return this.services.contents.copy(fromFile, toDir);
- }
- /**
- * Create a new file and return the widget used to view it.
- *
- * @param path - The file path to create.
- *
- * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
- *
- * @param kernel - An optional kernel name/id to override the default.
- *
- * @returns The created widget, or `undefined`.
- *
- * #### Notes
- * This function will return `undefined` if a valid widget factory
- * cannot be found.
- */
- createNew(
- path: string,
- widgetName = 'default',
- kernel?: Partial<Kernel.IModel>
- ): Widget | undefined {
- return this._createOrOpenDocument('create', path, widgetName, kernel);
- }
- /**
- * Delete a file.
- *
- * @param path - The full path to the file to be deleted.
- *
- * @returns A promise which resolves when the file is deleted.
- *
- * #### Notes
- * If there is a running session associated with the file and no other
- * sessions are using the kernel, the session will be shut down.
- */
- deleteFile(path: string): Promise<void> {
- return this.services.sessions
- .stopIfNeeded(path)
- .then(() => {
- return this.services.contents.delete(path);
- })
- .then(() => {
- this._contextsForPath(path).forEach(context =>
- this._widgetManager.deleteWidgets(context)
- );
- return Promise.resolve(void 0);
- });
- }
- /**
- * See if a widget already exists for the given path and widget name.
- *
- * @param path - The file path to use.
- *
- * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
- *
- * @returns The found widget, or `undefined`.
- *
- * #### Notes
- * This can be used to find an existing widget instead of opening
- * a new widget.
- */
- findWidget(
- path: string,
- widgetName: string | null = 'default'
- ): IDocumentWidget | undefined {
- const newPath = PathExt.normalize(path);
- let widgetNames = [widgetName];
- if (widgetName === 'default') {
- const factory = this.registry.defaultWidgetFactory(newPath);
- if (!factory) {
- return undefined;
- }
- widgetNames = [factory.name];
- } else if (widgetName === null) {
- widgetNames = this.registry
- .preferredWidgetFactories(newPath)
- .map(f => f.name);
- }
- for (const context of this._contextsForPath(newPath)) {
- for (const widgetName of widgetNames) {
- if (widgetName !== null) {
- const widget = this._widgetManager.findWidget(context, widgetName);
- if (widget) {
- return widget;
- }
- }
- }
- }
- return undefined;
- }
- /**
- * Create a new untitled file.
- *
- * @param options - The file content creation options.
- */
- newUntitled(options: Contents.ICreateOptions): Promise<Contents.IModel> {
- if (options.type === 'file') {
- options.ext = options.ext || '.txt';
- }
- return this.services.contents.newUntitled(options);
- }
- /**
- * Open a file and return the widget used to view it.
- *
- * @param path - The file path to open.
- *
- * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
- *
- * @param kernel - An optional kernel name/id to override the default.
- *
- * @returns The created widget, or `undefined`.
- *
- * #### Notes
- * This function will return `undefined` if a valid widget factory
- * cannot be found.
- */
- open(
- path: string,
- widgetName = 'default',
- kernel?: Partial<Kernel.IModel>,
- options?: DocumentRegistry.IOpenOptions
- ): IDocumentWidget | undefined {
- return this._createOrOpenDocument(
- 'open',
- path,
- widgetName,
- kernel,
- options
- );
- }
- /**
- * Open a file and return the widget used to view it.
- * Reveals an already existing editor.
- *
- * @param path - The file path to open.
- *
- * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
- *
- * @param kernel - An optional kernel name/id to override the default.
- *
- * @returns The created widget, or `undefined`.
- *
- * #### Notes
- * This function will return `undefined` if a valid widget factory
- * cannot be found.
- */
- openOrReveal(
- path: string,
- widgetName = 'default',
- kernel?: Partial<Kernel.IModel>,
- options?: DocumentRegistry.IOpenOptions
- ): IDocumentWidget | undefined {
- const widget = this.findWidget(path, widgetName);
- if (widget) {
- this._opener.open(widget, options || {});
- return widget;
- }
- return this.open(path, widgetName, kernel, options || {});
- }
- /**
- * Overwrite a file.
- *
- * @param oldPath - The full path to the original file.
- *
- * @param newPath - The full path to the new file.
- *
- * @returns A promise containing the new file contents model.
- */
- overwrite(oldPath: string, newPath: string): Promise<Contents.IModel> {
- // Cleanly overwrite the file by moving it, making sure the original does
- // not exist, and then renaming to the new path.
- const tempPath = `${newPath}.${UUID.uuid4()}`;
- const cb = () => this.rename(tempPath, newPath);
- return this.rename(oldPath, tempPath)
- .then(() => {
- return this.deleteFile(newPath);
- })
- .then(cb, cb);
- }
- /**
- * Rename a file or directory.
- *
- * @param oldPath - The full path to the original file.
- *
- * @param newPath - The full path to the new file.
- *
- * @returns A promise containing the new file contents model. The promise
- * will reject if the newPath already exists. Use [[overwrite]] to overwrite
- * a file.
- */
- rename(oldPath: string, newPath: string): Promise<Contents.IModel> {
- return this.services.contents.rename(oldPath, newPath);
- }
- /**
- * Find a context for a given path and factory name.
- */
- private _findContext(
- path: string,
- factoryName: string
- ): Private.IContext | undefined {
- const normalizedPath = this.services.contents.normalize(path);
- return find(this._contexts, context => {
- return (
- context.path === normalizedPath && context.factoryName === factoryName
- );
- });
- }
- /**
- * Get the contexts for a given path.
- *
- * #### Notes
- * There may be more than one context for a given path if the path is open
- * with multiple model factories (for example, a notebook can be open with a
- * notebook model factory and a text model factory).
- */
- private _contextsForPath(path: string): Private.IContext[] {
- const normalizedPath = this.services.contents.normalize(path);
- return this._contexts.filter(context => context.path === normalizedPath);
- }
- /**
- * Create a context from a path and a model factory.
- */
- private _createContext(
- path: string,
- factory: DocumentRegistry.ModelFactory,
- kernelPreference?: ISessionContext.IKernelPreference
- ): Private.IContext {
- // TODO: Make it impossible to open two different contexts for the same
- // path. Or at least prompt the closing of all widgets associated with the
- // old context before opening the new context. This will make things much
- // more consistent for the users, at the cost of some confusion about what
- // models are and why sometimes they cannot open the same file in different
- // widgets that have different models.
- // Allow options to be passed when adding a sibling.
- const adopter = (
- widget: IDocumentWidget,
- options?: DocumentRegistry.IOpenOptions
- ) => {
- this._widgetManager.adoptWidget(context, widget);
- this._opener.open(widget, options);
- };
- const modelDBFactory =
- this.services.contents.getModelDBFactory(path) || undefined;
- const context = new Context({
- opener: adopter,
- manager: this.services,
- factory,
- path,
- kernelPreference,
- modelDBFactory,
- setBusy: this._setBusy,
- sessionDialogs: this._dialogs,
- collaborative: this._collaborative,
- docProviderFactory: this._docProviderFactory,
- lastModifiedCheckMargin: this._lastModifiedCheckMargin,
- translator: this.translator
- });
- const handler = new SaveHandler({
- context,
- saveInterval: this.autosaveInterval
- });
- Private.saveHandlerProperty.set(context, handler);
- void context.ready.then(() => {
- if (this.autosave) {
- handler.start();
- }
- });
- context.disposed.connect(this._onContextDisposed, this);
- this._contexts.push(context);
- return context;
- }
- /**
- * Handle a context disposal.
- */
- private _onContextDisposed(context: Private.IContext): void {
- ArrayExt.removeFirstOf(this._contexts, context);
- }
- /**
- * Get the widget factory for a given widget name.
- */
- private _widgetFactoryFor(
- path: string,
- widgetName: string
- ): DocumentRegistry.WidgetFactory | undefined {
- const { registry } = this;
- if (widgetName === 'default') {
- const factory = registry.defaultWidgetFactory(path);
- if (!factory) {
- return undefined;
- }
- widgetName = factory.name;
- }
- return registry.getWidgetFactory(widgetName);
- }
- /**
- * Creates a new document, or loads one from disk, depending on the `which` argument.
- * If `which==='create'`, then it creates a new document. If `which==='open'`,
- * then it loads the document from disk.
- *
- * The two cases differ in how the document context is handled, but the creation
- * of the widget and launching of the kernel are identical.
- */
- private _createOrOpenDocument(
- which: 'open' | 'create',
- path: string,
- widgetName = 'default',
- kernel?: Partial<Kernel.IModel>,
- options?: DocumentRegistry.IOpenOptions
- ): IDocumentWidget | undefined {
- const widgetFactory = this._widgetFactoryFor(path, widgetName);
- if (!widgetFactory) {
- return undefined;
- }
- const modelName = widgetFactory.modelName || 'text';
- const factory = this.registry.getModelFactory(modelName);
- if (!factory) {
- return undefined;
- }
- // Handle the kernel preference.
- const preference = this.registry.getKernelPreference(
- path,
- widgetFactory.name,
- kernel
- );
- let context: Private.IContext | null;
- let ready: Promise<void> = Promise.resolve(undefined);
- // Handle the load-from-disk case
- if (which === 'open') {
- // Use an existing context if available.
- context = this._findContext(path, factory.name) || null;
- if (!context) {
- context = this._createContext(path, factory, preference);
- // Populate the model, either from disk or a
- // model backend.
- ready = this._when.then(() => context!.initialize(false));
- }
- } else if (which === 'create') {
- context = this._createContext(path, factory, preference);
- // Immediately save the contents to disk.
- ready = this._when.then(() => context!.initialize(true));
- } else {
- throw new Error(`Invalid argument 'which': ${which}`);
- }
- const widget = this._widgetManager.createWidget(widgetFactory, context);
- this._opener.open(widget, options || {});
- // If the initial opening of the context fails, dispose of the widget.
- ready.catch(err => {
- console.error(
- `Failed to initialize the context with '${factory.name}' for ${path}`,
- err
- );
- widget.close();
- });
- return widget;
- }
- /**
- * Handle an activateRequested signal from the widget manager.
- */
- private _onActivateRequested(
- sender: DocumentWidgetManager,
- args: string
- ): void {
- this._activateRequested.emit(args);
- }
- protected translator: ITranslator;
- private _activateRequested = new Signal<this, string>(this);
- private _contexts: Private.IContext[] = [];
- private _opener: DocumentManager.IWidgetOpener;
- private _widgetManager: DocumentWidgetManager;
- private _isDisposed = false;
- private _autosave = true;
- private _autosaveInterval = 120;
- private _lastModifiedCheckMargin = 500;
- private _when: Promise<void>;
- private _setBusy: (() => IDisposable) | undefined;
- private _dialogs: ISessionContext.IDialogs;
- private _docProviderFactory: IDocumentProviderFactory | undefined;
- private _collaborative: boolean;
- }
- /**
- * A namespace for document manager statics.
- */
- export namespace DocumentManager {
- /**
- * The options used to initialize a document manager.
- */
- export interface IOptions {
- /**
- * A document registry instance.
- */
- registry: DocumentRegistry;
- /**
- * A service manager instance.
- */
- manager: ServiceManager.IManager;
- /**
- * A widget opener for sibling widgets.
- */
- opener: IWidgetOpener;
- /**
- * A promise for when to start using the manager.
- */
- when?: Promise<void>;
- /**
- * A function called when a kernel is busy.
- */
- setBusy?: () => IDisposable;
- /**
- * The provider for session dialogs.
- */
- sessionDialogs?: ISessionContext.IDialogs;
- /**
- * The application language translator.
- */
- translator?: ITranslator;
- /**
- * A factory method for the document provider.
- */
- docProviderFactory?: IDocumentProviderFactory;
- /**
- * Whether the context should be collaborative.
- * If true, the context will connect through yjs_ws_server to share information if possible.
- */
- collaborative?: boolean;
- }
- /**
- * An interface for a widget opener.
- */
- export interface IWidgetOpener {
- /**
- * Open the given widget.
- */
- open(
- widget: IDocumentWidget,
- options?: DocumentRegistry.IOpenOptions
- ): void;
- }
- }
- /**
- * A namespace for private data.
- */
- namespace Private {
- /**
- * An attached property for a context save handler.
- */
- export const saveHandlerProperty = new AttachedProperty<
- DocumentRegistry.Context,
- SaveHandler | undefined
- >({
- name: 'saveHandler',
- create: () => undefined
- });
- /**
- * A type alias for a standard context.
- *
- * #### Notes
- * We define this as an interface of a specific implementation so that we can
- * use the implementation-specific functions.
- */
- export interface IContext extends Context<DocumentRegistry.IModel> {
- /* no op */
- }
- }
|