diff --git a/src/docprovider/filebrowser.ts b/src/docprovider/filebrowser.ts index 386e9b3..222cc13 100644 --- a/src/docprovider/filebrowser.ts +++ b/src/docprovider/filebrowser.ts @@ -31,12 +31,9 @@ import { ICollaborativeContentProvider, IGlobalAwareness } from '@jupyter/collaborative-drive'; -import { - RtcContentProvider -} from './ydrive'; +import { RtcContentProvider } from './ydrive'; import { Awareness } from 'y-protocols/awareness'; - const TWO_SESSIONS_WARNING = 'The file %1 has been opened with two different views. ' + 'This is not supported. Please close this view; otherwise, ' + @@ -132,7 +129,7 @@ export const ynotebook: JupyterFrontEndPlugin = { const enableDocWideUndo = settings?.get( 'experimentalEnableDocumentWideUndoRedo' ).composite as boolean; - + // @ts-ignore disableDocumentWideUndoRedo = !enableDocWideUndo ?? true; }; diff --git a/src/docprovider/index.ts b/src/docprovider/index.ts index 997793f..10d3fe3 100644 --- a/src/docprovider/index.ts +++ b/src/docprovider/index.ts @@ -1,4 +1,4 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -export * from './filebrowser' +export * from './filebrowser'; diff --git a/src/docprovider/requests.ts b/src/docprovider/requests.ts index 31b9fd4..b19d6ff 100644 --- a/src/docprovider/requests.ts +++ b/src/docprovider/requests.ts @@ -71,4 +71,4 @@ export async function requestAPI( } return data; -} \ No newline at end of file +} diff --git a/src/handler.ts b/src/handler.ts index 3fecdff..b65ad60 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -17,7 +17,7 @@ export async function requestAPI( const settings = ServerConnection.makeSettings(); const requestUrl = URLExt.join( settings.baseUrl, - 'jupyter-rtc-core', // API Namespace + endPoint.startsWith('/') ? '' : 'jupyter-rtc-core', // API Namespace endPoint ); diff --git a/src/index.ts b/src/index.ts index 5772653..8a4378a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,14 +30,12 @@ import { import { AwarenessExecutionIndicator } from './executionindicator'; + +import { IEditorServices } from '@jupyterlab/codeeditor'; import { requestAPI } from './handler'; +import { YNotebookContentFactory } from './notebook'; -import { - rtcContentProvider, - yfile, - ynotebook, - logger -} from './docprovider'; +import { rtcContentProvider, yfile, ynotebook, logger } from './docprovider'; import { IStateDB, StateDB } from '@jupyterlab/statedb'; import { IGlobalAwareness } from '@jupyter/collaborative-drive'; @@ -284,6 +282,21 @@ export const kernelStatus: JupyterFrontEndPlugin = { }; +/** + * The notebook cell factory provider. + */ +const factory: JupyterFrontEndPlugin = { + id: '@jupyter/rtc-core/notebook-extension:factory', + description: 'Provides the notebook cell factory.', + provides: NotebookPanel.IContentFactory, + requires: [IEditorServices], + autoStart: true, + activate: (app: JupyterFrontEnd, editorServices: IEditorServices) => { + const editorFactory = editorServices.factoryService.newInlineEditor; + return new YNotebookContentFactory({ editorFactory }); + } +}; + const plugins: JupyterFrontEndPlugin[] = [ rtcContentProvider, yfile, @@ -292,7 +305,8 @@ const plugins: JupyterFrontEndPlugin[] = [ rtcGlobalAwarenessPlugin, plugin, executionIndicator, - kernelStatus + kernelStatus, + factory ]; export default plugins; diff --git a/src/notebook.ts b/src/notebook.ts new file mode 100644 index 0000000..411c5ff --- /dev/null +++ b/src/notebook.ts @@ -0,0 +1,222 @@ +// @ts-nocheck +import { + Cell, + CodeCell, + CellModel, + CodeCellModel, + MarkdownCellModel, + RawCellModel +} from '@jupyterlab/cells'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { KernelMessage } from '@jupyterlab/services'; +import { + CellChange, + createMutex, + ISharedCodeCell, + ISharedMarkdownCell, + ISharedRawCell, + YCodeCell +} from '@jupyter/ydoc'; +import { IOutputAreaModel, OutputAreaModel } from '@jupyterlab/outputarea'; +import { requestAPI } from './handler'; +import { CellList } from '@jupyterlab/notebook'; + +import { ObservableList } from '@jupyterlab/observables'; + +const globalModelDBMutex = createMutex(); + + +// @ts-ignore +CodeCellModel.prototype._onSharedModelChanged = function ( + slot: ISharedCodeCell, + change: CellChange +) { + if (change.streamOutputChange) { + globalModelDBMutex(() => { + for (const streamOutputChange of change.streamOutputChange!) { + if ('delete' in streamOutputChange) { + // @ts-ignore + this._outputs.removeStreamOutput(streamOutputChange.delete!); + } + if ('insert' in streamOutputChange) { + // @ts-ignore + this._outputs.appendStreamOutput( + streamOutputChange.insert!.toString() + ); + } + } + }); + } + + if (change.outputsChange) { + globalModelDBMutex(() => { + let retain = 0; + for (const outputsChange of change.outputsChange!) { + if ('retain' in outputsChange) { + retain += outputsChange.retain!; + } + if ('delete' in outputsChange) { + for (let i = 0; i < outputsChange.delete!; i++) { + // @ts-ignore + this._outputs.remove(retain); + } + } + if ('insert' in outputsChange) { + // Inserting an output always results in appending it. + for (const output of outputsChange.insert!) { + // For compatibility with older ydoc where a plain object, + // (rather than a Map instance) could be provided. + // In a future major release the use of Map will be required. + //@ts-ignore + if ('toJSON' in output) { + // @ts-ignore + const parsed = output.toJSON(); + const metadata = parsed.metadata; + if (metadata && metadata.url) { + // fetch the real output + requestAPI(metadata.url).then(data => { + // @ts-ignore + this._outputs.add(data); + }); + } else { + // @ts-ignore + this._outputs.add(parsed); + } + } else { + console.debug('output from doc: ', output); + // @ts-ignore + this._outputs.add(output); + } + } + } + } + }); + } + if (change.executionCountChange) { + if ( + change.executionCountChange.newValue && + // @ts-ignore + (this.isDirty || !change.executionCountChange.oldValue) + ) { + // @ts-ignore + this._setDirty(false); + } + // @ts-ignore + this.stateChanged.emit({ + name: 'executionCount', + oldValue: change.executionCountChange.oldValue, + newValue: change.executionCountChange.newValue + }); + } + + if (change.executionStateChange) { + // @ts-ignore + this.stateChanged.emit({ + name: 'executionState', + oldValue: change.executionStateChange.oldValue, + newValue: change.executionStateChange.newValue + }); + } + // @ts-ignore + if (change.sourceChange && this.executionCount !== null) { + // @ts-ignore + this._setDirty(this._executedCode !== this.sharedModel.getSource().trim()); + } +}; + +// @ts-ignore +CodeCellModel.prototype.onOutputsChange = function ( + sender: IOutputAreaModel, + event: IOutputAreaModel.ChangedArgs +) { + console.debug('Inside onOutputsChange, called with event: ', event); + return + // @ts-ignore + const codeCell = this.sharedModel as YCodeCell; + globalModelDBMutex(() => { + if (event.type == 'remove') { + codeCell.updateOutputs(event.oldIndex, event.oldValues.length, []); + } + }); +}; + +class RtcOutputAreaModel extends OutputAreaModel implements IOutputAreaModel{ + /** + * Construct a new observable outputs instance. + */ + constructor(options: IOutputAreaModel.IOptions = {}) { + super({...options, values: []}) + this._trusted = !!options.trusted; + this.contentFactory = + options.contentFactory || OutputAreaModel.defaultContentFactory; + this.list = new ObservableList(); + if (options.values) { + // Create an array to store promises for each value + const valuePromises = options.values.map((value, originalIndex) => { + console.log("originalIndex: ", originalIndex, ", value: ", value); + // If value has a URL, fetch the data, otherwise just use the value directly + if (value.metadata?.url) { + return requestAPI(value.metadata.url) + .then(data => { + console.log("data from outputs service: " , data) + return {data, originalIndex} + }) + .catch(error => { + console.error('Error fetching output:', error); + // If fetch fails, return original value to maintain order + return { data: null, originalIndex }; + }); + } else { + // For values without url, return immediately with original value + return Promise.resolve({ data: value, originalIndex }); + } + }); + + // Wait for all promises to resolve and add values in original order + Promise.all(valuePromises) + .then(results => { + // Sort by original index to maintain order + results.sort((a, b) => a.originalIndex - b.originalIndex); + + console.log("After fetching outputs...") + // Add each value in order + results.forEach((result) => { + console.log("originalIndex: ", result.originalIndex, ", data: ", result.data) + if(result.data && !this.isDisposed){ + const index = this._add(result.data) - 1; + const item = this.list.get(index); + item.changed.connect(this._onGenericChange, this); + } + }); + + // Connect the list changed handler after all items are added + //this.list.changed.connect(this._onListChanged, this); + })/* + .catch(error => { + console.error('Error processing values:', error); + // If something goes wrong, fall back to original behavior + options.values.forEach(value => { + const index = this._add(value) - 1; + const item = this.list.get(index); + item.changed.connect(this._onGenericChange, this); + }); + this.list.changed.connect(this._onListChanged, this); + });*/ + } else { + // If no values, just connect the list changed handler + //this.list.changed.connect(this._onListChanged, this); + } + + this.list.changed.connect(this._onListChanged, this); + } +} + +CodeCellModel.ContentFactory.prototype.createOutputArea = function(options: IOutputAreaModel.IOptions): IOutputAreaModel { + return new RtcOutputAreaModel(options); +} + +export class YNotebookContentFactory extends NotebookPanel.ContentFactory implements NotebookPanel.IContentFactory{ + createCodeCell(options: CodeCell.IOptions): CodeCell { + return new CodeCell(options).initializeState(); + } +}