diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index e4eba9aefb3..0de2917cf3f 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -212,7 +212,7 @@ suite('notebook dirty state', () => { await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); - // await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); @@ -272,3 +272,51 @@ suite('notebook undo redo', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); }); + +suite('notebook working copy', () => { + test('notebook revert on close', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + // close active editor from command will revert the file + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test'); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + test('notebook revert', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + await vscode.commands.executeCommand('workbench.action.files.revert'); + + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 1); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test'); + + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); +}); diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index b27f6760475..e5402feb298 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -6,12 +6,22 @@ import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext): any { - context.subscriptions.push(vscode.notebook.registerNotebookProvider('notebookCoreTest', { - resolveNotebook: async (editor: vscode.NotebookEditor) => { - await editor.edit(eb => { - eb.insert(0, 'test', 'typescript', vscode.CellKind.Code, [], {}); - }); - return; + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { + onDidChangeNotebook: new vscode.EventEmitter().event, + openNotebook: async (_resource: vscode.Uri) => { + return { + languages: ['typescript'], + metadata: {}, + cells: [ + { + source: 'test', + language: 'typescript', + cellKind: vscode.CellKind.Code, + outputs: [], + metadata: {} + } + ] + }; }, executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { if (!_cell) { @@ -26,8 +36,11 @@ export function activate(context: vscode.ExtensionContext): any { }]; return; }, - save: async (_document: vscode.NotebookDocument) => { - return true; + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; } })); } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index da705cb2bb8..e44809aaa68 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1640,12 +1640,6 @@ declare module 'vscode' { edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; } - export interface NotebookProvider { - resolveNotebook(editor: NotebookEditor): Promise; - executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise; - save(document: NotebookDocument): Promise; - } - export interface NotebookOutputSelector { type: string; subTypes?: string[]; @@ -1708,11 +1702,6 @@ declare module 'vscode' { provider: NotebookContentProvider ): Disposable; - export function registerNotebookProvider( - notebookType: string, - provider: NotebookProvider - ): Disposable; - export function registerNotebookOutputRenderer( type: string, outputSelector: NotebookOutputSelector, diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 4653ef3ffec..efdef129e3f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,7 +84,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo registerListeners() { this._register(this._notebookService.onDidChangeActiveEditor(e => { - this._proxy.$updateActiveEditor(e.viewType, e.uri); + this._proxy.$acceptDocumentAndEditorsDelta({ + newActiveEditor: e.uri + }); })); const updateOrder = () => { @@ -129,16 +131,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.createNotebookDocument(handle, viewType, resource); - } - - return; - } - async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { let controller = this._notebookProviders.get(viewType); @@ -163,11 +155,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } - async resolveNotebook(viewType: string, uri: URI): Promise { - let handle = await this._proxy.$resolveNotebook(viewType, uri); - return handle; - } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { let controller = this._notebookProviders.get(viewType); controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); @@ -195,6 +182,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo export class MainThreadNotebookController implements IMainNotebookController { private _mapping: Map = new Map(); + static documentHandle: number = 0; constructor( private readonly _proxy: ExtHostNotebookShape, @@ -203,26 +191,44 @@ export class MainThreadNotebookController implements IMainNotebookController { ) { } - async resolveNotebook(viewType: string, uri: URI): Promise { - // TODO: resolve notebook should wait for all notebook document destory operations to finish. + async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook) { + if (forceReload) { + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return; + } + + mainthreadNotebook.textModel.languages = data.languages; + mainthreadNotebook.textModel.metadata = data.metadata; + mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [ + { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, + { editType: CellEditType.Insert, index: 0, cells: data.cells } + ]); + } return mainthreadNotebook.textModel; } - let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); - if (notebookHandle !== undefined) { - mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - if (mainthreadNotebook && mainthreadNotebook.textModel.cells.length === 0) { - // it's empty, we should create an empty template one - const mainCell = mainthreadNotebook.textModel.createCellTextModel([''], mainthreadNotebook.textModel.languages.length ? mainthreadNotebook.textModel.languages[0] : '', CellKind.Code, [], undefined); - mainthreadNotebook.textModel.insertTemplateCell(mainCell); - } - return mainthreadNotebook?.textModel; + let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri); + await this.createNotebookDocument(document); + + if (forBackup) { + return document.textModel; } - return undefined; + // open notebook document + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return; + } + + document.textModel.languages = data.languages; + document.textModel.metadata = data.metadata; + document.textModel.initialize(data!.cells); + + return document.textModel; } async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { @@ -250,12 +256,32 @@ export class MainThreadNotebookController implements IMainNotebookController { this._proxy.$onDidReceiveMessage(uri, message); } - // Methods for ExtHost - async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); - this._mapping.set(URI.revive(resource).toString(), document); + async createNotebookDocument(document: MainThreadNotebookDocument): Promise { + this._mapping.set(document.uri.toString(), document); + + await this._proxy.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + viewType: document.viewType, + handle: document.handle, + uri: document.uri + }] + }); } + async removeNotebookDocument(notebook: INotebookTextModel): Promise { + let document = this._mapping.get(URI.from(notebook.uri).toString()); + + if (!document) { + return; + } + + await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); + document.dispose(); + this._mapping.delete(URI.from(notebook.uri).toString()); + } + + // Methods for ExtHost + updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateLanguages(languages); @@ -280,21 +306,12 @@ export class MainThreadNotebookController implements IMainNotebookController { return this._proxy.$executeNotebook(this._viewType, uri, handle, token); } - async destoryNotebookDocument(notebook: INotebookTextModel): Promise { - let document = this._mapping.get(URI.from(notebook.uri).toString()); - - if (!document) { - return; - } - - let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri); - if (removeFromExtHost) { - document.dispose(); - this._mapping.delete(URI.from(notebook.uri).toString()); - } - } - async save(uri: URI, token: CancellationToken): Promise { return this._proxy.$saveNotebook(this._viewType, uri, token); } + + async saveAs(uri: URI, target: URI, token: CancellationToken): Promise { + return this._proxy.$saveNotebookAs(this._viewType, uri, target, token); + + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a4736ede416..eed6c1f9d12 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -910,10 +910,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidCloseNotebookDocument; }, - registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookProvider(extension, viewType, provider); - }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c6b3c31999d..9340648be72 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -691,7 +691,6 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookProvider(viewType: string): Promise; $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; $unregisterNotebookRenderer(handle: number): Promise; - $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; @@ -1538,16 +1537,31 @@ export interface INotebookEditorPropertiesChangeData { selections: INotebookSelectionChangeEvent | null; } +export interface INotebookModelAddedData { + uri: UriComponents; + handle: number; + // versionId: number; + viewType: string; +} + +export interface INotebookDocumentsAndEditorsDelta { + removedDocuments?: UriComponents[]; + addedDocuments?: INotebookModelAddedData[]; + // removedEditors?: string[]; + // addedEditors?: ITextEditorAddData[]; + newActiveEditor?: UriComponents | null; +} + export interface ExtHostNotebookShape { - $resolveNotebook(viewType: string, uri: UriComponents): Promise; + $resolveNotebookData(viewType: string, uri: UriComponents): Promise; $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise; - $updateActiveEditor(viewType: string, uri: UriComponents): Promise; - $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; + $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $onDidReceiveMessage(uri: UriComponents, message: any): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 1b1d7db1f80..66dba087614 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,10 +10,10 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Disposable as VSCodeDisposable } from './extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; @@ -345,8 +345,6 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo transformMimeTypes(output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { let mimeTypes = Object.keys(output.data); - - // TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side. let coreDisplayOrder = this.renderingHandler.outputDisplayOrder; const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []); @@ -415,7 +413,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } } -export class NotebookEditorCellEdit { +export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { private _finalized: boolean = false; private readonly _documentVersionId: number; private _collectedEdits: ICellEditOperation[] = []; @@ -526,13 +524,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable { - const edit = new NotebookEditorCellEdit(this); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this); callback(edit); return this._applyEdit(edit); } - private _applyEdit(editBuilder: NotebookEditorCellEdit): Promise { + private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { const editData = editBuilder.finalize(); // return when there is nothing to do @@ -625,7 +623,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookProviders = new Map(); private readonly _notebookContentProviders = new Map(); private readonly _documents = new Map(); private readonly _editors = new Map; }>(); @@ -706,31 +703,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return matches; } - registerNotebookProvider( - extension: IExtensionDescription, - viewType: string, - provider: vscode.NotebookProvider, - ): vscode.Disposable { - - if (this._notebookProviders.has(viewType)) { - throw new Error(`Notebook provider for '${viewType}' already registered`); - } - - this._notebookProviders.set(viewType, { extension, provider }); - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); - return new VSCodeDisposable(() => { - this._notebookProviders.delete(viewType); - this._proxy.$unregisterNotebookProvider(viewType); - }); - } - registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, ): vscode.Disposable { - if (this._notebookProviders.has(viewType)) { + if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } @@ -742,97 +721,99 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - async _resolveNotebookFromContentProvider(viewType: string, uri: UriComponents): Promise { + async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { let provider = this._notebookContentProviders.get(viewType); + let document = this._documents.get(URI.revive(uri).toString()); - if (provider) { - const revivedUri = URI.revive(uri); - if (!this._documents.has(revivedUri.toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (provider && document) { + const rawCells = await provider.provider.openNotebook(URI.revive(uri)); + const renderers = new Set(); + const dto = { + metadata: { + ...notebookDocumentMetadataDefaults, + ...rawCells.metadata + }, + languages: rawCells.languages, + cells: rawCells.cells.map(cell => { + let transformedOutputs = cell.outputs.map(output => { + if (output.outputKind === CellOutputKind.Rich) { + // TODO display string[] + const ret = this._transformMimeTypes(document!, (rawCells.metadata.displayOrder as string[]) || [], output); - this._documents.set(revivedUri.toString(), document); - } + if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { + renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); + } + return ret; + } else { + return output as IStreamOutput | IErrorOutput; + } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - revivedUri, - this._proxy, - onDidReceiveMessage, - this._documents.get(revivedUri.toString())!, - this._documentsAndEditors - ); - - this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); - - const data = await provider.provider.openNotebook(revivedUri); - editor.document.languages = data.languages; - editor.document.metadata = { - ...notebookDocumentMetadataDefaults, - ...data.metadata + return { + language: cell.language, + cellKind: cell.cellKind, + metadata: cell.metadata, + source: cell.source, + outputs: transformedOutputs + }; + }) }; - await editor.edit(editBuilder => { - for (let i = 0; i < data.cells.length; i++) { - const cell = data.cells[i]; - editBuilder.insert(0, cell.source, cell.language, cell.cellKind, cell.outputs, cell.metadata); - } - }); - - this._onDidOpenNotebookDocument.fire(editor.document); - return editor.document.handle; - } else { - return Promise.resolve(undefined); + return dto; } + + return; } - async $resolveNotebook(viewType: string, uri: UriComponents): Promise { - let notebookFromNotebookContentProvider = await this._resolveNotebookFromContentProvider(viewType, uri); + private _transformMimeTypes(document: ExtHostNotebookDocument, displayOrder: string[], output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { + let mimeTypes = Object.keys(output.data); + let coreDisplayOrder = this.outputDisplayOrder; + const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], displayOrder, coreDisplayOrder?.defaultOrder || []); - if (notebookFromNotebookContentProvider !== undefined) { - return notebookFromNotebookContentProvider; - } + let orderMimeTypes: IOrderedMimeType[] = []; - let provider = this._notebookProviders.get(viewType); + sorted.forEach(mimeType => { + let handlers = this.findBestMatchedRenderer(mimeType); - if (provider) { - if (!this._documents.has(URI.revive(uri).toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, URI.revive(uri), this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (handlers.length) { + let renderedOutput = handlers[0].render(document, output, mimeType); - this._documents.set(URI.revive(uri).toString(), document); + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: true, + rendererId: handlers[0].handle, + output: renderedOutput + }); + + for (let i = 1; i < handlers.length; i++) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: handlers[i].handle + }); + } + + if (mimeTypeSupportedByCore(mimeType)) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: -1 + }); + } + } else { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false + }); } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - URI.revive(uri), - this._proxy, - onDidReceiveMessage, - this._documents.get(URI.revive(uri).toString())!, - this._documentsAndEditors - ); - - this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage }); - await provider.provider.resolveNotebook(editor); - // await editor.document.$updateCells(); - return editor.document.handle; - } - - return Promise.resolve(undefined); + return { + outputKind: output.outputKind, + data: output.data, + orderedMimeTypes: orderMimeTypes, + pickedMimeTypeIndex: 0 + }; } async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { @@ -847,15 +828,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._notebookContentProviders.get(viewType)!.provider.executeCell(document, cell, token); } - - let provider = this._notebookProviders.get(viewType); - - if (!provider) { - return; - } - - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - return provider.provider.executeCell(document!, cell, token); } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { @@ -874,44 +846,26 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return true; } - let provider = this._notebookProviders.get(viewType); - - if (provider && document) { - return await provider.provider.save(document); - } - return false; } - async $updateActiveEditor(viewType: string, uri: UriComponents): Promise { - this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString()); - this._activeNotebookEditor = this._editors.get(URI.revive(uri).toString())?.editor; - } - - async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise { - let provider = this._notebookProviders.get(viewType); - - if (!provider) { + async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { + let document = this._documents.get(URI.revive(uri).toString()); + if (!document) { return false; } - let document = this._documents.get(URI.revive(uri).toString()); + if (this._notebookContentProviders.has(viewType)) { + try { + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + } catch (e) { + return false; + } - if (document) { - document.dispose(); - this._documents.delete(URI.revive(uri).toString()); - this._onDidCloseNotebookDocument.fire(document); + return true; } - let editor = this._editors.get(URI.revive(uri).toString()); - - if (editor) { - editor.editor.dispose(); - editor.onDidReceiveMessage.dispose(); - this._editors.delete(URI.revive(uri).toString()); - } - - return true; + return false; } $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { @@ -957,4 +911,60 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } } + + async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) { + if (delta.removedDocuments) { + delta.removedDocuments.forEach((uri) => { + let document = this._documents.get(URI.revive(uri).toString()); + + if (document) { + document.dispose(); + this._documents.delete(URI.revive(uri).toString()); + this._onDidCloseNotebookDocument.fire(document); + } + + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.editor.dispose(); + editor.onDidReceiveMessage.dispose(); + this._editors.delete(URI.revive(uri).toString()); + } + }); + } + + if (delta.addedDocuments) { + delta.addedDocuments.forEach(modelData => { + const revivedUri = URI.revive(modelData.uri); + const viewType = modelData.viewType; + if (!this._documents.has(revivedUri.toString())) { + let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); + this._documents.set(revivedUri.toString(), document); + } + + const onDidReceiveMessage = new Emitter(); + const document = this._documents.get(revivedUri.toString())!; + + let editor = new ExtHostNotebookEditor( + viewType, + `${ExtHostNotebookController._handlePool++}`, + revivedUri, + this._proxy, + onDidReceiveMessage, + document, + this._documentsAndEditors + ); + + this._onDidOpenNotebookDocument.fire(document); + + // TODO, does it already exist? + this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); + }); + } + + if (delta.newActiveEditor) { + this._activeNotebookDocument = this._documents.get(URI.revive(delta.newActiveEditor).toString()); + this._activeNotebookEditor = this._editors.get(URI.revive(delta.newActiveEditor).toString())?.editor; + } + } } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 1a7895cefd8..e17833c1ff5 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -21,6 +21,10 @@ overflow: visible !important; } */ +.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible { + z-index: 100; +} + .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 088143de4db..fd7dff392af 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; export class NotebookEditorInput extends EditorInput { @@ -39,7 +40,10 @@ export class NotebookEditorInput extends EditorInput { public name: string, public readonly viewType: string | undefined, @INotebookService private readonly notebookService: INotebookService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + // @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); } @@ -85,6 +89,48 @@ export class NotebookEditorInput extends EditorInput { return undefined; } + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this.textModel) { + return undefined; + } + + const dialogPath = this.textModel.resource; + const target = await this.fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!await this.textModel.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + move(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this.textModel) { + const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this.textModel!.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType); + return { editor: editorInput }; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this.textModel) { + await this.textModel.revert(options); + } + + return; + } + async resolve(): Promise { if (!await this.notebookService.canResolve(this.viewType!)) { throw new Error(`Cannot open notebook of type '${this.viewType}'`); @@ -96,6 +142,10 @@ export class NotebookEditorInput extends EditorInput { this._onDidChangeDirty.fire(); })); + if (this.textModel.isDirty()) { + this._onDidChangeDirty.fire(); + } + return this.textModel; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 8e8a3d0c741..ca530378bbc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -11,7 +11,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from ' import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { Iterable } from 'vs/base/common/iterator'; @@ -202,13 +202,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu return; } - async resolveNotebook(viewType: string, uri: URI): Promise { + async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.resolveNotebook(viewType, uri); + const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false); if (!notebookModel) { return undefined; } @@ -219,6 +219,39 @@ export class NotebookService extends Disposable implements INotebookService, ICu notebookModel, (model) => this._onWillDispose(model), ); + this._models[modelId] = modelData; + + notebookModel.metadata = metadata; + notebookModel.languages = languages; + + notebookModel.applyEdit(notebookModel.versionId, [ + { + editType: CellEditType.Insert, + index: 0, + cells: cells + } + ]); + + return modelData.model; + } + + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise { + const provider = this._notebookProviders.get(viewType); + if (!provider) { + return undefined; + } + + let notebookModel: NotebookTextModel | undefined; + + notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload); + + // new notebook model created + const modelId = MODEL_ID(uri); + const modelData = new ModelData( + notebookModel!, + (model) => this._onWillDispose(model), + ); + this._models[modelId] = modelData; return modelData.model; } @@ -265,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu let provider = this._notebookProviders.get(viewType); if (provider) { - provider.controller.destoryNotebookDocument(notebook); + provider.controller.removeNotebookDocument(notebook); } } @@ -291,6 +324,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } + async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.saveAs(resource, target, token); + } + + return false; + } + onDidReceiveMessage(viewType: string, uri: URI, message: any): void { let provider = this._notebookProviders.get(viewType); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 5f7766fd2e4..f82bf348e93 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -157,7 +157,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -177,7 +177,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -299,7 +299,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -316,6 +316,18 @@ export class NotebookCellList extends WorkbenchList implements ID splice2(start: number, deleteCount: number, elements: CellViewModel[] = []): void { // we need to convert start and delete count based on hidden ranges super.splice(start, deleteCount, elements); + + const selectionsLeft = []; + this._viewModel!.selectionHandles.forEach(handle => { + if (this._viewModel!.hasCell(handle)) { + selectionsLeft.push(handle); + } + }); + + if (!selectionsLeft.length && this._viewModel!.viewCells) { + // after splice, the selected cells are deleted + this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle]; + } } getViewIndex(cell: ICellViewModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index e8016684b27..5b56bce0939 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -268,7 +268,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); diffs.reverse().forEach(diff => { - this._viewCells.splice(diff[0], diff[1], ...diff[2]); + const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]); + + deletedCells.forEach(cell => { + this._handleToViewCellMapping.delete(cell.handle); + }); + diff[2].forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); this._localStore.add(cell); @@ -456,8 +461,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return index + 1; } - hasCell(cell: ICellViewModel) { - return this._handleToViewCellMapping.has(cell.handle); + hasCell(handle: number) { + return this._handleToViewCellMapping.has(handle); } getVersionId() { @@ -586,7 +591,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(deleteIndex, 1); this._handleToViewCellMapping.delete(deleteCell.handle); - this._notebook.removeCell(deleteIndex); + this._notebook.removeCell(deleteIndex, 1); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -638,7 +643,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(index, 1); this._handleToViewCellMapping.delete(viewCell.handle); - this._notebook.removeCell(index); + this._notebook.removeCell(index, 1); let endSelections: number[] = []; if (this.selectionHandles.length) { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 4c5ca6ecf6b..f768bc5d1ce 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ITextSnapshot } from 'vs/editor/common/model'; function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { if (a[1] === b[1]) { @@ -17,6 +18,49 @@ function compareRangesUsingEnds(a: [number, number], b: [number, number]): numbe return a[1] - b[1]; } +export class NotebookTextModelSnapshot implements ITextSnapshot { + // private readonly _pieces: Ce[] = []; + private _index: number = -1; + + constructor(private _model: NotebookTextModel) { + // for (let i = 0; i < this._model.cells.length; i++) { + // const cell = this._model.cells[i]; + // this._pieces.push(this._model.cells[i].textBuffer.createSnapshot(true)); + // } + } + + read(): string | null { + + if (this._index === -1) { + this._index++; + return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "languages": ${JSON.stringify(this._model.languages)}, "cells": [`; + } + + if (this._index < this._model.cells.length) { + const cell = this._model.cells[this._index]; + + const data = { + source: cell.getValue(), + metadata: cell.metadata, + cellKind: cell.cellKind, + language: cell.language + }; + + const rawStr = JSON.stringify(data); + const isLastCell = this._index === this._model.cells.length - 1; + + this._index++; + return isLastCell ? rawStr : (rawStr + ','); + } else if (this._index === this._model.cells.length) { + this._index++; + return `]}`; + } else { + return null; + } + } + +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private static _cellhandlePool: number = 0; @@ -77,6 +121,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata); } + initialize(cells: ICellDto2[]) { + this.cells = []; + this._versionId = 0; + + const mainCells = cells.map(cell => { + const cellHandle = NotebookTextModel._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata); + }); + this.insertNewCell(0, mainCells); + } + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[]): boolean { if (modelVersionId !== this._versionId) { return false; @@ -127,7 +183,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.insertNewCell(insertEdit.index, mainCells); break; case CellEditType.Delete: - this.removeCell(operations[i].index); + this.removeCell(operations[i].index, operations[i].end - operations[i].start); break; } } @@ -142,6 +198,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } + createSnapshot(preserveBOM?: boolean): ITextSnapshot { + return new NotebookTextModelSnapshot(this); + } + private _increaseVersionId(): void { this._versionId = this._versionId + 1; } @@ -250,17 +310,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return; } - removeCell(index: number) { + removeCell(index: number, count: number) { this._isUntitled = false; - let cell = this.cells[index]; - this._cellListeners.get(cell.handle)?.dispose(); - this._cellListeners.delete(cell.handle); - this.cells.splice(index, 1); + for (let i = index; i < index + count; i++) { + let cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + this.cells.splice(index, count); this._onDidChangeContent.fire(); this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, 1, []]] }); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] }); } moveCellToIdx(index: number, newIdx: number) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index b2a349dd607..90006f52f2a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -273,7 +273,7 @@ export enum CellEditType { } export interface ICellDto2 { - source: string[]; + source: string | string[]; language: string; cellKind: CellKind; outputs: IOutput[]; @@ -300,6 +300,13 @@ export interface INotebookEditData { renderers: number[]; } +export interface NotebookDataDto { + readonly cells: ICellDto2[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; +} + + export namespace CellUri { export const scheme = 'vscode-notebook'; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index fd66b9a06b7..823c790efbb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; export interface INotebookEditorModelManager { models: NotebookEditorModel[]; @@ -24,6 +26,13 @@ export interface INotebookEditorModelManager { get(resource: URI): NotebookEditorModel | undefined; } +export interface INotebookRevertOptions { + /** + * Go to disk bypassing any cache of the model if any. + */ + forceReadFromDisk?: boolean; +} + export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { private _dirty = false; @@ -47,7 +56,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN public readonly resource: URI, public readonly viewType: string, @INotebookService private readonly notebookService: INotebookService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IBackupFileService private readonly backupFileService: IBackupFileService ) { super(); this._register(this.workingCopyService.registerWorkingCopy(this)); @@ -56,28 +66,91 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN capabilities = 0; async backup(): Promise { - return {}; + return { content: this._notebook.createSnapshot(true) }; } async revert(options?: IRevertOptions | undefined): Promise { + if (options?.soft) { + await this.backupFileService.discardBackup(this.resource); + return; + } + + await this.load({ forceReadFromDisk: true }); + this._dirty = false; + this._onDidChangeDirty.fire(); return; } - async load(): Promise { - const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource); + async load(options?: INotebookRevertOptions): Promise { + if (options?.forceReadFromDisk) { + return this.loadFromProvider(true); + } + if (this.isResolved()) { + return this; + } + + const backup = await this.backupFileService.resolve(this.resource); + + if (this.isResolved()) { + return this; // Make sure meanwhile someone else did not succeed in loading + } + + if (backup) { + try { + return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF)); + } catch (error) { + // this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below + } + } + + return this.loadFromProvider(false); + } + + private async loadFromBackup(content: ITextBuffer): Promise { + const fullRange = content.getRangeAt(0, content.getLength()); + const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF)); + + const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells); this._notebook = notebook!; this._name = basename(this._notebook!.uri); this._register(this._notebook.onDidChangeContent(() => { - this._dirty = true; - this._onDidChangeDirty.fire(); + this.setDirty(true); + this._onDidChangeContent.fire(); + })); + + await this.backupFileService.discardBackup(this.resource); + this.setDirty(true); + + return this; + } + + private async loadFromProvider(forceReloadFromDisk: boolean) { + const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk); + this._notebook = notebook!; + + this._name = basename(this._notebook!.uri); + + this._register(this._notebook.onDidChangeContent(() => { + this.setDirty(true); this._onDidChangeContent.fire(); })); return this; } + isResolved(): boolean { + return !!this._notebook; + } + + setDirty(newState: boolean) { + if (this._dirty !== newState) { + this._dirty = newState; + this._onDidChangeDirty.fire(); + } + } + isDirty() { return this._dirty; } @@ -89,6 +162,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._onDidChangeDirty.fire(); return true; } + + async saveAs(targetResource: URI): Promise { + const tokenSource = new CancellationTokenSource(); + await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); + this._dirty = false; + this._onDidChangeDirty.fire(); + return true; + } } export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index f846f30ffe8..6a808bafa99 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -17,12 +17,13 @@ import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/commo export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - resolveNotebook(viewType: string, uri: URI): Promise; + createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise; executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; onDidReceiveMessage(uri: URI, message: any): void; executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise; - destoryNotebookDocument(notebook: INotebookTextModel): Promise; + removeNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI, token: CancellationToken): Promise; + saveAs(uri: URI, target: URI, token: CancellationToken): Promise; } export interface INotebookService { @@ -35,7 +36,8 @@ export interface INotebookService { registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; unregisterNotebookRenderer(handle: number): void; getRendererInfo(handle: number): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI): Promise; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise; + createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise; executeNotebook(viewType: string, uri: URI): Promise; executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; @@ -45,6 +47,7 @@ export interface INotebookService { destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; save(viewType: string, resource: URI, token: CancellationToken): Promise; + saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise; onDidReceiveMessage(viewType: string, uri: URI, message: any): void; setToCopy(items: NotebookCellTextModel[]): void; getToCopy(): NotebookCellTextModel[] | undefined; diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 0b0827adb8a..7695ee2df5b 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -41,15 +41,20 @@ suite('NotebookConcatDocument', function () { rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { async $registerNotebookProvider() { } async $unregisterNotebookProvider() { } - async $createNotebookDocument() { } }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors); - let reg = extHostNotebooks.registerNotebookProvider(nullExtensionDescription, 'test', new class extends mock() { - async resolveNotebook() { } + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { + // async openNotebook() { } + }); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + handle: 0, + uri: notebookUri, + viewType: 'test' + }] }); - await extHostNotebooks.$resolveNotebook('test', notebookUri); extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, versionId: 0, @@ -62,7 +67,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }]]] }); - await extHostNotebooks.$updateActiveEditor('test', notebookUri); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: notebookUri }); notebook = extHostNotebooks.activeNotebookDocument!;