123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- 'use strict';
- import {
- IContentsModel, IContentsManager, IContentsOpts
- } from 'jupyter-js-services';
- import {
- IMessageFilter, IMessageHandler, Message, installMessageFilter
- } from 'phosphor-messaging';
- import {
- Property
- } from 'phosphor-properties';
- import {
- ISignal, Signal
- } from 'phosphor-signaling';
- import {
- Widget
- } from 'phosphor-widget';
- import {
- showDialog
- } from '../dialog';
- /**
- * The class name added to a dirty documents.
- */
- const DIRTY_CLASS = 'jp-mod-dirty';
- /**
- * An implementation of a file handler.
- */
- export
- abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
- /**
- * Construct a new source file handler.
- *
- * @param manager - The contents manager used to save/load files.
- */
- constructor(manager: IContentsManager) {
- this._manager = manager;
- }
- /**
- * A signal emitted when a file opens.
- */
- get opened(): ISignal<AbstractFileHandler<T>, T> {
- return Private.openedSignal.bind(this);
- }
- /**
- * Get the list of file extensions explicitly supported by the handler.
- */
- get fileExtensions(): string[] {
- return [];
- }
- /**
- * Get the list of mime types explicitly supported by the handler.
- */
- get mimeTypes(): string[] {
- return [];
- }
- /**
- * Get the contents manager used by the handler.
- */
- get manager(): IContentsManager {
- return this._manager;
- }
- /**
- * Find a widget given a path.
- */
- findWidget(path: string): T {
- for (let w of this._widgets) {
- let model = this._getModel(w);
- if (model.path === path) {
- return w;
- }
- }
- }
- /**
- * Find a model given a widget. The model itself will have a
- * null `content` field.
- */
- findModel(widget: T): IContentsModel {
- return Private.modelProperty.get(widget);
- }
- /**
- * Open a contents model and return a widget.
- */
- open(model: IContentsModel): T {
- let widget = this.findWidget(model.path);
- if (!widget) {
- widget = this.createWidget(model);
- widget.title.closable = true;
- this._setModel(widget, model);
- this._widgets.push(widget);
- installMessageFilter(widget, this);
- }
- // Fetch the contents and populate the widget asynchronously.
- let opts = this.getFetchOptions(model);
- this.manager.get(model.path, opts).then(contents => {
- widget.title.text = this.getTitleText(model);
- return this.populateWidget(widget, contents);
- }).then(contents => {
- this.clearDirty(model.path);
- });
- this.opened.emit(widget);
- return widget;
- }
- /**
- * Rename a file.
- */
- rename(oldPath: string, newPath: string): boolean {
- let widget = this.findWidget(oldPath);
- if (widget === void 0) {
- return false;
- }
- if (newPath === void 0) {
- this.clearDirty(oldPath);
- widget.close();
- return true;
- }
- let model = this._getModel(widget);
- model.path = newPath;
- let parts = newPath.split('/');
- model.name = parts[parts.length - 1];
- widget.title.text = this.getTitleText(model);
- return true;
- }
- /**
- * Save contents.
- *
- * @param path - The path of the file to save.
- *
- * returns A promise that resolves to the contents of the path.
- *
- * #### Notes
- * This clears the dirty state of the file after a successful save.
- */
- save(path: string): Promise<IContentsModel> {
- let widget = this.findWidget(path);
- if (!widget) {
- return Promise.resolve(void 0);
- }
- let model = this._getModel(widget);
- return this.getSaveOptions(widget, model).then(opts => {
- return this.manager.save(model.path, opts);
- }).then(contents => {
- this.clearDirty(path);
- return contents;
- });
- }
- /**
- * Revert contents.
- *
- * @param path - The path of the file to revert.
- *
- * returns A promise that resolves to the new contents of the path.
- *
- * #### Notes
- * This clears the dirty state of the file after a successful revert.
- */
- revert(path: string): Promise<IContentsModel> {
- let widget = this.findWidget(path);
- if (!widget) {
- return Promise.resolve(void 0);
- }
- let model = this._getModel(widget);
- let opts = this.getFetchOptions(model);
- return this.manager.get(model.path, opts).then(contents => {
- return this.populateWidget(widget, contents);
- }).then(contents => {
- this.clearDirty(path);
- return contents;
- });
- }
- /**
- * Close a file.
- *
- * @param path - The path of the file to close.
- *
- * returns A boolean indicating whether the file was closed.
- */
- close(path: string): Promise<boolean> {
- let widget = this.findWidget(path);
- if (!widget) {
- return Promise.resolve(false);
- }
- if (this.isDirty(path)) {
- return this._maybeClose(widget);
- }
- this._close(widget);
- return Promise.resolve(true);
- }
- /**
- * Close all files.
- */
- closeAll(): void {
- for (let w of this._widgets) {
- w.close();
- }
- }
- /**
- * Get whether a file is dirty.
- */
- isDirty(path: string): boolean {
- let widget = this.findWidget(path);
- return Private.dirtyProperty.get(widget);
- }
- /**
- * Set the dirty state of a widget (defaults to current active widget).
- */
- setDirty(path: string): void {
- let widget = this.findWidget(path);
- Private.dirtyProperty.set(widget, true);
- }
- /**
- * Clear the dirty state of a widget (defaults to current active widget).
- */
- clearDirty(path: string): void {
- let widget = this.findWidget(path);
- Private.dirtyProperty.set(widget, false);
- }
- /**
- * Filter messages on the widget.
- */
- filterMessage(handler: IMessageHandler, msg: Message): boolean {
- let widget = handler as T;
- if (msg.type === 'close-request' && widget) {
- let path = this.findModel(widget).path;
- this.close(path);
- return true;
- }
- return false;
- }
- /**
- * Get options use to fetch the model contents from disk.
- *
- * #### Notes
- * Subclasses are free to use any or none of the information in
- * the model.
- */
- protected getFetchOptions(model: IContentsModel): IContentsOpts {
- return { type: 'file', format: 'text' };
- }
- /**
- * Get the options used to save the widget content.
- */
- protected abstract getSaveOptions(widget: T, model: IContentsModel): Promise<IContentsOpts>;
- /**
- * Create the widget from a model.
- */
- protected abstract createWidget(model: IContentsModel): T;
- /**
- * Populate a widget from an `IContentsModel`.
- *
- * #### Notes
- * Subclasses are free to use any or none of the information in
- * the model. It is up to subclasses to handle setting dirty state when
- * the widget contents change. See [[AbstractFileHandler.dirtyProperty]].
- */
- protected abstract populateWidget(widget: T, model: IContentsModel): Promise<IContentsModel>;
- /**
- * Set the appropriate title text based on a model.
- */
- protected getTitleText(model: IContentsModel): string {
- return model.name;
- }
- /**
- * Get the model for a given widget.
- */
- private _getModel(widget: T): IContentsModel {
- return Private.modelProperty.get(widget);
- }
- /**
- * Set the model for a widget.
- */
- private _setModel(widget: T, model: IContentsModel) {
- Private.modelProperty.set(widget, model);
- }
- /**
- * Ask the user whether to close an unsaved file.
- */
- private _maybeClose(widget: T): Promise<boolean> {
- return showDialog({
- title: 'Close without saving?',
- body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
- host: widget.node
- }).then(value => {
- if (value.text === 'OK') {
- this._close(widget);
- return true;
- }
- return false;
- });
- }
- /**
- * Actually close the file.
- */
- private _close(widget: T): void {
- let model = Private.modelProperty.get(widget);
- widget.dispose();
- let index = this._widgets.indexOf(widget);
- this._widgets.splice(index, 1);
- }
- private _manager: IContentsManager = null;
- private _widgets: T[] = [];
- private _cb: (widget: T) => void = null;
- }
- /**
- * A private namespace for AbstractFileHandler data.
- */
- namespace Private {
- /**
- * A signal emitted when a model is opened.
- */
- export
- const openedSignal = new Signal<AbstractFileHandler<Widget>, Widget>();
- /**
- * An attached property with the widget model.
- */
- export
- const modelProperty = new Property<Widget, IContentsModel>({
- name: 'model',
- value: null
- });
- /**
- * An attached property with the widget dirty state.
- */
- export
- const dirtyProperty = new Property<Widget, boolean>({
- name: 'dirty',
- value: false,
- changed: (widget: Widget, oldValue: boolean, newValue: boolean) => {
- if (newValue) {
- widget.title.className += ` ${DIRTY_CLASS}`;
- } else {
- widget.title.className = widget.title.className.replace(DIRTY_CLASS, '');
- }
- }
- });
- }
|