// ***************************************************************************** // Copyright (C) 2023 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { DisposableCollection, Event } from '@theia/core'; import { URI, UriComponents } from '@theia/core/lib/common/uri'; import { interfaces } from '@theia/core/shared/inversify'; import { NotebookModelResolverService } from '@theia/notebook/lib/browser'; import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; import { NotebookCellsChangeType } from '@theia/notebook/lib/common'; import { NotebookMonacoTextModelService } from '@theia/notebook/lib/browser/service/notebook-monaco-text-model-service'; import { MAIN_RPC_CONTEXT, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain, NotebookRawContentEventDto } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { NotebookDto } from './notebook-dto'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { NotebookOpenHandler } from '@theia/notebook/lib/browser/notebook-open-handler'; export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { protected readonly disposables = new DisposableCollection(); protected readonly proxy: NotebookDocumentsExt; protected readonly documentEventListenersMapping = new Map(); protected readonly notebookModelResolverService: NotebookModelResolverService; protected readonly notebookMonacoTextModelService: NotebookMonacoTextModelService; protected readonly notebookOpenHandler: NotebookOpenHandler; constructor( rpc: RPCProtocol, container: interfaces.Container ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT); this.notebookModelResolverService = container.get(NotebookModelResolverService); this.notebookOpenHandler = container.get(NotebookOpenHandler); // forward dirty and save events this.disposables.push(this.notebookModelResolverService.onDidChangeDirty(model => this.proxy.$acceptDirtyStateChanged(model.uri.toComponents(), model.isDirty()))); this.disposables.push(this.notebookModelResolverService.onDidSaveNotebook(e => this.proxy.$acceptModelSaved(e))); this.notebookMonacoTextModelService = container.get(NotebookMonacoTextModelService) as NotebookMonacoTextModelService; } get onDidAddNotebookCellModel(): Event { return this.notebookMonacoTextModelService.onDidCreateNotebookCellModel; } dispose(): void { this.disposables.dispose(); // this.modelReferenceCollection.dispose(); this.documentEventListenersMapping.forEach(value => value.dispose()); } handleNotebooksAdded(notebooks: readonly NotebookModel[]): void { for (const notebook of notebooks) { const listener = notebook.onDidChangeContent(events => { const eventDto: NotebookCellsChangedEventDto = { versionId: 1, // TODO implement version ID support rawEvents: [] }; for (const e of events) { switch (e.kind) { case NotebookCellsChangeType.ModelChange: eventDto.rawEvents.push({ kind: e.kind, changes: e.changes.map(diff => ({ ...diff, newItems: diff.newItems.map(NotebookDto.toNotebookCellDto) })) }); break; case NotebookCellsChangeType.Move: eventDto.rawEvents.push({ kind: e.kind, index: e.index, length: e.length, newIdx: e.newIdx, }); break; case NotebookCellsChangeType.Output: eventDto.rawEvents.push({ kind: e.kind, index: e.index, outputs: e.outputs.map(NotebookDto.toNotebookOutputDto) }); break; case NotebookCellsChangeType.OutputItem: eventDto.rawEvents.push({ kind: e.kind, index: e.index, outputId: e.outputId, outputItems: e.outputItems.map(NotebookDto.toNotebookOutputItemDto), append: e.append }); break; case NotebookCellsChangeType.ChangeCellLanguage: case NotebookCellsChangeType.ChangeCellContent: case NotebookCellsChangeType.ChangeCellMetadata: case NotebookCellsChangeType.ChangeCellInternalMetadata: eventDto.rawEvents.push(e); break; case NotebookCellsChangeType.ChangeDocumentMetadata: eventDto.rawEvents.push({ kind: e.kind, metadata: e.metadata }); break; } } const hasDocumentMetadataChangeEvent = events.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); // using the model resolver service to know if the model is dirty or not. // assuming this is the first listener it can mean that at first the model // is marked as dirty and that another event is fired this.proxy.$acceptModelChanged( notebook.uri.toComponents(), eventDto, notebook.isDirty(), hasDocumentMetadataChangeEvent ? notebook.metadata : undefined ); }); this.documentEventListenersMapping.set(notebook.uri.toString(), new DisposableCollection(listener)); } } handleNotebooksRemoved(uris: UriComponents[]): void { for (const uri of uris) { this.documentEventListenersMapping.get(uri.toString())?.dispose(); this.documentEventListenersMapping.delete(uri.toString()); } } async $tryCreateNotebook(options: { viewType: string; content?: NotebookDataDto }): Promise { const ref = await this.notebookModelResolverService.resolveUntitledResource({ untitledResource: undefined }, options.viewType); // untitled notebooks are disposed when they get saved. we should not hold a reference // to such a disposed notebook and therefore dispose the reference as well // ref.onWillDispose(() => { // ref.dispose(); // }); const uriComponents = ref.uri.toComponents(); // untitled notebooks are dirty by default this.proxy.$acceptDirtyStateChanged(uriComponents, true); // apply content changes... if (options.content) { const data = NotebookDto.fromNotebookDataDto(options.content); ref.setData(data); // Create and send a change events const rawEvents: NotebookRawContentEventDto[] = []; if (options.content.cells && options.content.cells.length > 0) { rawEvents.push({ kind: NotebookCellsChangeType.ModelChange, changes: [{ start: 0, startHandle: 0, deleteCount: 0, newItems: ref.cells.map(NotebookDto.toNotebookCellDto) }] }); } if (options.content.metadata) { rawEvents.push({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: options.content.metadata }); } if (rawEvents.length > 0) { this.proxy.$acceptModelChanged(uriComponents, { versionId: 1, rawEvents }, true); } } return uriComponents; } async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.fromComponents(uriComponents); await this.notebookModelResolverService.resolve(uri); return uri.toComponents(); } async $trySaveNotebook(uriComponents: UriComponents): Promise { const uri = URI.fromComponents(uriComponents); const ref = await this.notebookModelResolverService.resolve(uri); await ref.save(); ref.dispose(); return true; } }