123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- ContentsManager, Contents, Kernel, ServiceManager, Session, utils
- } from '@jupyterlab/services';
- import {
- JSONObject
- } from 'phosphor/lib/algorithm/json';
- import {
- findIndex
- } from 'phosphor/lib/algorithm/searching';
- import {
- IDisposable, DisposableDelegate
- } from 'phosphor/lib/core/disposable';
- import {
- clearSignalData, defineSignal, ISignal
- } from 'phosphor/lib/core/signaling';
- import {
- Widget
- } from 'phosphor/lib/ui/widget';
- import {
- showDialog, okButton
- } from '../common/dialog';
- import {
- findKernel
- } from '../docregistry';
- import {
- DocumentRegistry
- } from '../docregistry';
- /**
- * An implementation of a document context.
- *
- * This class is typically instantiated by the document manger.
- */
- export
- class Context<T extends DocumentRegistry.IModel> implements DocumentRegistry.IContext<T> {
- /**
- * Construct a new document context.
- */
- constructor(options: Context.IOptions<T>) {
- let manager = this._manager = options.manager;
- this._factory = options.factory;
- this._opener = options.opener;
- this._path = options.path;
- let ext = DocumentRegistry.extname(this._path);
- let lang = this._factory.preferredLanguage(ext);
- this._model = this._factory.createNew(lang);
- manager.sessions.runningChanged.connect(this._onSessionsChanged, this);
- manager.contents.fileChanged.connect(this._onFileChanged, this);
- this._readyPromise = manager.ready.then(() => {
- return this._populatedPromise.promise;
- });
- }
- /**
- * A signal emitted when the kernel changes.
- */
- kernelChanged: ISignal<this, Kernel.IKernel>;
- /**
- * A signal emitted when the path changes.
- */
- pathChanged: ISignal<this, string>;
- /**
- * A signal emitted when the model is saved or reverted.
- */
- fileChanged: ISignal<this, Contents.IModel>;
- /**
- * A signal emitted when the context is disposed.
- */
- disposed: ISignal<this, void>;
- /**
- * Get the model associated with the document.
- */
- get model(): T {
- return this._model;
- }
- /**
- * The current kernel associated with the document.
- */
- get kernel(): Kernel.IKernel {
- return this._session ? this._session.kernel : null;
- }
- /**
- * The current path associated with the document.
- */
- get path(): string {
- return this._path;
- }
- /**
- * The current contents model associated with the document
- *
- * #### Notes
- * The model will have an empty `contents` field.
- */
- get contentsModel(): Contents.IModel {
- return this._contentsModel;
- }
- /**
- * Get the model factory name.
- *
- * #### Notes
- * This is not part of the `IContext` API.
- */
- get factoryName(): string {
- return this.isDisposed ? '' : this._factory.name;
- }
- /**
- * Test whether the context is disposed.
- */
- get isDisposed(): boolean {
- return this._model === null;
- }
- /**
- * Dispose of the resources held by the context.
- */
- dispose(): void {
- if (this._model == null) {
- return;
- }
- let model = this._model;
- let session = this._session;
- this._model = null;
- this._session = null;
- this._manager = null;
- this._factory = null;
- model.dispose();
- if (session) {
- session.dispose();
- }
- this.disposed.emit(void 0);
- clearSignalData(this);
- }
- /**
- * The kernel spec models
- */
- get specs(): Kernel.ISpecModels {
- return this._manager.specs;
- }
- /**
- * Whether the context is ready.
- */
- get isReady(): boolean {
- return this._isReady;
- }
- /**
- * A promise that is fulfilled when the context is ready.
- */
- get ready(): Promise<void> {
- return this._readyPromise;
- }
- /**
- * Start the default kernel for the context.
- *
- * @returns A promise that resolves with the new kernel.
- */
- startDefaultKernel(): Promise<Kernel.IKernel> {
- return this.ready.then(() => {
- if (this.isDisposed) {
- return;
- }
- let model = this.model;
- let name = findKernel(
- model.defaultKernelName,
- model.defaultKernelLanguage,
- this._manager.specs
- );
- return this.changeKernel({ name });
- });
- }
- /**
- * Change the current kernel associated with the document.
- */
- changeKernel(options: Kernel.IModel): Promise<Kernel.IKernel> {
- let session = this._session;
- if (options) {
- if (session) {
- return session.changeKernel(options);
- } else {
- let path = this._path;
- let sOptions: Session.IOptions = {
- path,
- kernelName: options.name,
- kernelId: options.id
- };
- return this._startSession(sOptions);
- }
- } else {
- if (session) {
- this._session = null;
- return session.shutdown().then(() => {
- session.dispose();
- if (this.isDisposed) {
- return;
- }
- this.kernelChanged.emit(null);
- return void 0;
- });
- } else {
- return Promise.resolve(void 0);
- }
- }
- }
- /**
- * Save the document contents to disk.
- */
- save(): Promise<void> {
- let model = this._model;
- let path = this._path;
- if (model.readOnly) {
- return Promise.reject(new Error('Read only'));
- }
- let content: JSONObject | string;
- if (this._factory.fileFormat === 'json') {
- content = model.toJSON();
- } else {
- content = model.toString();
- }
- let options = {
- type: this._factory.contentType,
- format: this._factory.fileFormat,
- content
- };
- let promise = this._manager.contents.save(path, options);
- return promise.then(value => {
- if (this.isDisposed) {
- return;
- }
- model.dirty = false;
- this._updateContentsModel(value);
- if (!this._isPopulated) {
- return this._populate();
- }
- }).catch(err => {
- showDialog({
- title: 'File Save Error',
- body: err.xhr.responseText,
- buttons: [okButton]
- });
- });
- }
- /**
- * Save the document to a different path chosen by the user.
- */
- saveAs(): Promise<void> {
- return Private.getSavePath(this._path).then(newPath => {
- if (this.isDisposed || !newPath) {
- return;
- }
- this._path = newPath;
- let session = this._session;
- if (session) {
- let options: Session.IOptions = {
- path: newPath,
- kernelId: session.kernel.id,
- kernelName: session.kernel.name
- };
- return this._startSession(options).then(() => {
- if (this.isDisposed) {
- return;
- }
- return this.save();
- });
- }
- return this.save();
- });
- }
- /**
- * Revert the document contents to disk contents.
- */
- revert(): Promise<void> {
- let opts: Contents.IFetchOptions = {
- format: this._factory.fileFormat,
- type: this._factory.contentType,
- content: true
- };
- let path = this._path;
- let model = this._model;
- return this._manager.contents.get(path, opts).then(contents => {
- if (this.isDisposed) {
- return;
- }
- if (contents.format === 'json') {
- model.fromJSON(contents.content);
- } else {
- model.fromString(contents.content);
- }
- this._updateContentsModel(contents);
- model.dirty = false;
- if (!this._isPopulated) {
- return this._populate();
- }
- }).catch(err => {
- showDialog({
- title: 'File Load Error',
- body: err.xhr.responseText,
- buttons: [okButton]
- });
- });
- }
- /**
- * Create a checkpoint for the file.
- */
- createCheckpoint(): Promise<Contents.ICheckpointModel> {
- return this._manager.contents.createCheckpoint(this._path);
- }
- /**
- * Delete a checkpoint for the file.
- */
- deleteCheckpoint(checkpointId: string): Promise<void> {
- return this._manager.contents.deleteCheckpoint(this._path, checkpointId);
- }
- /**
- * Restore the file to a known checkpoint state.
- */
- restoreCheckpoint(checkpointId?: string): Promise<void> {
- let contents = this._manager.contents;
- let path = this._path;
- if (checkpointId) {
- return contents.restoreCheckpoint(path, checkpointId);
- }
- return this.listCheckpoints().then(checkpoints => {
- if (this.isDisposed || !checkpoints.length) {
- return;
- }
- checkpointId = checkpoints[checkpoints.length - 1].id;
- return contents.restoreCheckpoint(path, checkpointId);
- });
- }
- /**
- * List available checkpoints for a file.
- */
- listCheckpoints(): Promise<Contents.ICheckpointModel[]> {
- return this._manager.contents.listCheckpoints(this._path);
- }
- /**
- * Resolve a relative url to a correct server path.
- */
- resolveUrl(url: string): Promise<string> {
- // Ignore urls that have a protocol.
- if (utils.urlParse(url).protocol || url.indexOf('//') === 0) {
- return Promise.resolve(url);
- }
- let cwd = ContentsManager.dirname(this._path);
- let path = ContentsManager.getAbsolutePath(url, cwd);
- return Promise.resolve(path);
- }
- /**
- * Add a sibling widget to the document manager.
- */
- addSibling(widget: Widget): IDisposable {
- let opener = this._opener;
- if (opener) {
- opener(widget);
- }
- return new DisposableDelegate(() => {
- widget.close();
- });
- }
- /**
- * Handle a change on the contents manager.
- */
- private _onFileChanged(sender: Contents.IManager, change: Contents.IChangedArgs): void {
- if (change.type !== 'rename') {
- return;
- }
- if (change.oldValue.path === this._path) {
- let path = this._path = change.newValue.path;
- if (this._session) {
- this._session.rename(path);
- }
- this.pathChanged.emit(path);
- }
- }
- /**
- * Start a session and set up its signals.
- */
- private _startSession(options: Session.IOptions): Promise<Kernel.IKernel> {
- return this._manager.sessions.startNew(options).then(session => {
- if (this.isDisposed) {
- return;
- }
- if (this._session) {
- this._session.dispose();
- }
- this._session = session;
- this.kernelChanged.emit(session.kernel);
- session.pathChanged.connect(this._onSessionPathChanged, this);
- session.kernelChanged.connect(this._onKernelChanged, this);
- return session.kernel;
- }).catch(err => {
- let response = JSON.parse(err.xhr.response);
- let body = document.createElement('pre');
- body.textContent = response['traceback'];
- showDialog({
- title: 'Error Starting Kernel',
- body,
- buttons: [okButton]
- });
- return Promise.reject(err);
- });
- }
- /**
- * Handle a change to a session path.
- */
- private _onSessionPathChanged(sender: Session.ISession) {
- let path = sender.path;
- if (path !== this._path) {
- this._path = path;
- this.pathChanged.emit(path);
- }
- }
- /**
- * Handle a change to the kernel.
- */
- private _onKernelChanged(sender: Session.ISession): void {
- this.kernelChanged.emit(sender.kernel);
- }
- /**
- * Update our contents model, without the content.
- */
- private _updateContentsModel(model: Contents.IModel): void {
- let newModel: Contents.IModel = {
- path: model.path,
- name: model.name,
- type: model.type,
- writable: model.writable,
- created: model.created,
- last_modified: model.last_modified,
- mimetype: model.mimetype,
- format: model.format
- };
- let mod = this._contentsModel ? this._contentsModel.last_modified : null;
- this._contentsModel = newModel;
- if (!mod || newModel.last_modified !== mod) {
- this.fileChanged.emit(newModel);
- }
- }
- /**
- * Handle a change to the running sessions.
- */
- private _onSessionsChanged(sender: Session.IManager, models: Session.IModel[]): void {
- let session = this._session;
- if (!session) {
- return;
- }
- let index = findIndex(models, model => model.id === session.id);
- if (index === -1) {
- session.dispose();
- this._session = null;
- this.kernelChanged.emit(null);
- }
- }
- /**
- * Handle an initial population.
- */
- private _populate(): Promise<void> {
- this._isPopulated = true;
- // Add a checkpoint if none exists.
- return this.listCheckpoints().then(checkpoints => {
- if (!this.isDisposed && !checkpoints) {
- return this.createCheckpoint();
- }
- }).then(() => {
- if (this.isDisposed) {
- return;
- }
- this._isReady = true;
- this._populatedPromise.resolve(void 0);
- });
- }
- private _manager: ServiceManager.IManager = null;
- private _opener: (widget: Widget) => void = null;
- private _model: T = null;
- private _path = '';
- private _session: Session.ISession = null;
- private _factory: DocumentRegistry.IModelFactory<T> = null;
- private _contentsModel: Contents.IModel = null;
- private _readyPromise: Promise<void>;
- private _populatedPromise = new utils.PromiseDelegate<void>();
- private _isPopulated = false;
- private _isReady = false;
- }
- // Define the signals for the `Context` class.
- defineSignal(Context.prototype, 'kernelChanged');
- defineSignal(Context.prototype, 'pathChanged');
- defineSignal(Context.prototype, 'fileChanged');
- defineSignal(Context.prototype, 'disposed');
- /**
- * A namespace for `Context` statics.
- */
- export namespace Context {
- /**
- * The options used to initialize a context.
- */
- export
- interface IOptions<T extends DocumentRegistry.IModel> {
- /**
- * A service manager instance.
- */
- manager: ServiceManager.IManager;
- /**
- * The model factory used to create the model.
- */
- factory: DocumentRegistry.IModelFactory<T>;
- /**
- * The initial path of the file.
- */
- path: string;
- /**
- * An optional callback for opening sibling widgets.
- */
- opener?: (widget: Widget) => void;
- }
- }
- /**
- * A namespace for private data.
- */
- namespace Private {
- /**
- * Get a new file path from the user.
- */
- export
- function getSavePath(path: string): Promise<string> {
- let input = document.createElement('input');
- input.value = path;
- return showDialog({
- title: 'Save File As..',
- body: input,
- okText: 'SAVE'
- }).then(result => {
- if (result.text === 'SAVE') {
- return input.value;
- }
- });
- }
- }
|