diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 7e143e679d2..f76bd9e1f4b 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1502,6 +1502,7 @@ export interface ICell { cell_type: 'markdown' | 'code'; outputs: IOutput[]; onDidChangeOutputs?: Event; + isDirty: boolean; } /** @@ -1525,9 +1526,12 @@ export interface INotebook { handle: number; // metadata: IMetadata; readonly uri: URI; + languages: string[]; cells: ICell[]; onDidChangeCells?: Event; + onDidChangeDirtyState: Event; onWillDispose(listener: () => void): IDisposable; + save(): Promise; } export interface CodeLens { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 922083d151b..4268abf98b7 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1324,7 +1324,8 @@ declare module 'vscode' { export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; export interface NotebookCell { - handle: number, + handle: number; + language: string; cell_type: 'markdown' | 'code'; outputs: CellOutput[]; getContent(): string; @@ -1334,6 +1335,7 @@ declare module 'vscode' { readonly uri: Uri; readonly fileName: string; readonly isDirty: boolean; + languages: string[]; cells: NotebookCell[]; } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index d418f1043be..fa90ef9819c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -6,15 +6,18 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { INotebook, IMetadata, ICell, IOutput } from 'vs/editor/common/modes'; +import { INotebook, ICell, IOutput } from 'vs/editor/common/modes'; import { Emitter, Event } from 'vs/base/common/event'; export class MainThreadCell implements ICell { private _onDidChangeOutputs = new Emitter(); onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + private _onDidChangeDirtyState = new Emitter(); + onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; + private _outputs: IOutput[]; public get outputs(): IOutput[] { @@ -26,6 +29,17 @@ export class MainThreadCell implements ICell { this._onDidChangeOutputs.fire(); } + private _isDirty: boolean = false; + + get isDirty() { + return this._isDirty; + } + + set isDirty(newState: boolean) { + this._isDirty = newState; + this._onDidChangeDirtyState.fire(newState); + } + constructor( public handle: number, public source: string[], @@ -35,6 +49,10 @@ export class MainThreadCell implements ICell { ) { this._outputs = outputs; } + + save() { + this._isDirty = false; + } } export class MainThreadNotebookDocument extends Disposable implements INotebook { @@ -42,19 +60,39 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook public readonly onWillDispose: Event = this._onWillDispose.event; private readonly _onDidChangeCells = new Emitter(); get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + private _onDidChangeDirtyState = new Emitter(); + onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; private _mapping: Map = new Map(); + private _cellListeners: Map = new Map(); public cells: MainThreadCell[]; public activeCell: MainThreadCell | undefined; + public languages: string[] = []; + + private _isDirty: boolean = false; + + get isDirty() { + return this._isDirty; + } + + set isDirty(newState: boolean) { + this._isDirty = newState; + this._onDidChangeDirtyState.fire(newState); + } constructor( private readonly _proxy: ExtHostNotebookShape, public handle: number, + public viewType: string, public uri: URI ) { super(); this.cells = []; } + updateLanguages(languages: string[]) { + this.languages = languages; + } + updateCell(cell: ICell) { let mcell = this._mapping.get(cell.handle); @@ -71,6 +109,10 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook let mainCell = new MainThreadCell(cell.handle, cell.source, cell.language, cell.cell_type, cell.outputs); this._mapping.set(cell.handle, mainCell); this.cells.push(mainCell); + let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => { + this.isDirty = this.isDirty || cellState; + }); + this._cellListeners.set(cell.handle, dirtyStateListener); }); } else { newCells.forEach(newCell => { @@ -88,6 +130,40 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook this.activeCell = this._mapping.get(handle); } + async createRawCell(viewType: string, uri: URI, index: number, language: string, type: 'markdown' | 'code'): Promise { + let cell = await this._proxy.$createRawCell(viewType, uri, index, language, type); + if (cell) { + let mainCell = new MainThreadCell(cell.handle, cell.source, cell.language, cell.cell_type, cell.outputs); + this._mapping.set(cell.handle, mainCell); + this.cells.splice(index, 0, mainCell); + + let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => { + this.isDirty = this.isDirty || cellState; + }); + + this._cellListeners.set(cell.handle, dirtyStateListener); + return mainCell; + } + + return; + } + + async removeCell(viewType: string, uri: URI, index: number): Promise { + return true; + } + + async save(): Promise { + let ret = await this._proxy.$saveNotebook(this.viewType, this.uri); + + if (ret) { + this.cells.forEach((cell) => { + cell.save(); + }); + } + + return ret; + } + dispose() { this._onWillDispose.fire(); super.dispose(); @@ -99,7 +175,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo private readonly _notebookProviders = new Map(); private readonly _proxy: ExtHostNotebookShape; - private readonly _documents: Map = new Map(); constructor( extHostContext: IExtHostContext, @INotebookService private _notebookService: INotebookService @@ -121,50 +196,54 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $createNotebookDocument(handle: number, resource: URI): Promise { - let document = new MainThreadNotebookDocument(this._proxy, handle, resource); - this._documents.set(handle, document); - return; - } - - async $updateNotebookCells(handle: number, cells: ICell[]): Promise { - let document = this._documents.get(handle); - - if (document) { - document.updateCells(cells); - } - } - - async $updateNotebookCell(handle: number, cell: ICell): Promise { - let document = this._documents.get(handle); - - if (document) { - document.updateCell(cell); - } - } - - - async $updateNotebook(viewType: string, uri: URI, notebook: INotebook): Promise { + async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { let controller = this._notebookProviders.get(viewType); if (controller) { - controller.updateNotebook(uri, notebook); + controller.createNotebookDocument(handle, viewType, resource); + } + + // let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); + // this._documents.set(handle, document); + return; + } + + async $updateNotebookCells(viewType: string, resource: UriComponents, cells: ICell[]): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebookCells(resource, cells); } } - async resolveNotebook(viewType: string, uri: URI): Promise { - let handle = await this._proxy.$resolveNotebook(viewType, uri); - if (handle !== undefined) { - const doc = this._documents.get(handle); + async $updateNotebookCell(viewType: string, resource: UriComponents, cell: ICell): Promise { + let controller = this._notebookProviders.get(viewType); - if (doc === undefined) { - console.log('resolve notebook from main but undefined'); - } - - return doc; + if (controller) { + controller.updateNotebookCell(resource, cell); } - return; + } + + async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateLanguages(resource, languages); + } + } + + async $updateNotebook(viewType: string, resource: UriComponents, notebook: INotebook): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebook(resource, notebook); + } + } + + async resolveNotebook(viewType: string, uri: URI): Promise { + let handle = await this._proxy.$resolveNotebook(viewType, uri); + return handle; } executeNotebook(viewType: string, uri: URI): Promise { @@ -183,11 +262,18 @@ export class MainThreadNotebookController implements IMainNotebookController { } async resolveNotebook(viewType: string, uri: URI): Promise { - let notebook = await this._mainThreadNotebook.resolveNotebook(viewType, uri); - if (notebook) { - this._mapping.set(uri.toString(), notebook); - return notebook; + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook) { + return mainthreadNotebook; } + + let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); + if (notebookHandle !== undefined) { + mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + return mainthreadNotebook; + } + return undefined; } @@ -195,14 +281,44 @@ export class MainThreadNotebookController implements IMainNotebookController { this._mainThreadNotebook.executeNotebook(viewType, uri); } - updateNotebook(uri: URI, notebook: INotebook): void { - let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + // 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); + } + + updateNotebook(resource: UriComponents, notebook: INotebook): void { + let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); if (mainthreadNotebook) { mainthreadNotebook.updateCells(notebook.cells); } } + updateLanguages(resource: UriComponents, languages: string[]) { + let document = this._mapping.get(URI.from(resource).toString()); + + if (document) { + document.updateLanguages(languages); + } + } + + updateNotebookCells(resource: UriComponents, cells: ICell[]): void { + let document = this._mapping.get(URI.from(resource).toString()); + + if (document) { + document.updateCells(cells); + } + } + + updateNotebookCell(resource: UriComponents, cell: ICell): void { + let document = this._mapping.get(URI.from(resource).toString()); + + if (document) { + document.updateCell(cell); + } + } + updateNotebookActiveCell(uri: URI, cellHandle: number): void { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); @@ -211,6 +327,16 @@ export class MainThreadNotebookController implements IMainNotebookController { } } + async createRawCell(uri: URI, index: number, language: string, type: 'markdown' | 'code'): Promise { + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook) { + return mainthreadNotebook.createRawCell(this._viewType, uri, index, language, type); + } + + return; + } + executeNotebookActiveCell(uri: URI): void { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); @@ -218,4 +344,13 @@ export class MainThreadNotebookController implements IMainNotebookController { this._proxy.$executeNotebookCell(this._viewType, uri, mainthreadNotebook.activeCell.handle); } } + + destoryNotebookDocument(notebook: INotebook): void { + let mainthreadNotebook = this._mapping.get(URI.from(notebook.uri).toString()); + + if (mainthreadNotebook) { + mainthreadNotebook.dispose(); + this._mapping.delete(URI.from(notebook.uri).toString()); + } + } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4a6a833c2a6..b3896102ad1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -611,10 +611,11 @@ export interface ExtHostWebviewsShape { export interface MainThreadNotebookShape extends IDisposable { $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $createNotebookDocument(handle: number, resource: UriComponents): Promise; + $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $updateNotebook(viewType: string, resource: UriComponents, notebook: modes.INotebook): Promise; - $updateNotebookCells(handle: number, cells: modes.ICell[]): Promise; - $updateNotebookCell(handle: number, cell: modes.ICell): Promise; + $updateNotebookCells(viewType: string, resource: UriComponents, cells: modes.ICell[]): Promise; + $updateNotebookCell(viewType: string, resource: UriComponents, cell: modes.ICell): Promise; + $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1409,6 +1410,8 @@ export interface ExtHostNotebookShape { $resolveNotebook(viewType: string, uri: URI): Promise; $executeNotebook(viewType: string, uri: URI): Promise; $executeNotebookCell(viewType: string, uri: URI, cellHandle: number): Promise; + $createRawCell(viewType: string, uri: URI, index: number, language: string, type: 'markdown' | 'code'): Promise; + $saveNotebook(viewType: string, uri: URI): Promise; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 28fa316d06a..d10276ecc6b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -12,6 +12,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { readonly } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ICell } from 'vs/editor/common/modes'; // import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; export class ExtHostCell implements vscode.NotebookCell { @@ -81,20 +82,32 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { this._cells = newCells; this._cells.forEach(cell => { cell.onDidChangeOutputs(() => { - this._proxy.$updateNotebookCell(this.handle, { + this._proxy.$updateNotebookCell(this.viewType, this.uri, { handle: cell.handle, source: cell.source, language: cell.language, cell_type: cell.cell_type, - outputs: cell.outputs + outputs: cell.outputs, + isDirty: false }); }); }); + } + private _languages: string[] = []; + + get languages() { + return this._languages = []; + } + + set languages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } constructor( private readonly _proxy: MainThreadNotebookShape, + public viewType: string, public uri: URI ) { @@ -105,15 +118,30 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { get isDirty() { return false; } async $updateCells(): Promise { - return await this._proxy.$updateNotebookCells(this.handle, this.cells.map(cell => ({ + return await this._proxy.$updateNotebookCells(this.viewType, this.uri, this.cells.map(cell => ({ handle: cell.handle, source: cell.source, language: cell.language, cell_type: cell.cell_type, - outputs: cell.outputs + outputs: cell.outputs, + isDirty: false }))); } + insertRawCell(index: number, cell: ExtHostCell) { + this.cells.splice(index, 0, cell); + cell.onDidChangeOutputs(() => { + this._proxy.$updateNotebookCell(this.viewType, this.uri, { + handle: cell.handle, + source: cell.source, + language: cell.language, + cell_type: cell.cell_type, + outputs: cell.outputs, + isDirty: false + }); + }); + } + getActiveCell(cellHandle: number) { return this.cells.find(cell => cell.handle === cellHandle); } @@ -209,6 +237,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private readonly _notebookProviders = new Map(); private readonly _localStore = new DisposableStore(); private readonly _documents = new Map(); + private readonly _editors = new Map(); constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) { @@ -240,9 +269,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (provider) { if (!this._documents.has(URI.revive(uri).toString())) { - let document = new ExtHostNotebookDocument(this._proxy, uri); + let document = new ExtHostNotebookDocument(this._proxy, viewType, uri); await this._proxy.$createNotebookDocument( document.handle, + viewType, uri ); @@ -258,6 +288,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._documentsProxy, this._documentsAndEditors ); + + this._editors.set(URI.revive(uri).toString(), editor); await provider.provider.resolveNotebook(editor); await editor.document.$updateCells(); return editor.document.handle; @@ -288,4 +320,37 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } + async $createRawCell(viewType: string, uri: URI, index: number, language: string, type: 'markdown' | 'code'): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + let editor = this._editors.get(URI.revive(uri).toString()); + let document = this._documents.get(URI.revive(uri).toString()); + + let rawCell = editor?.createCell('', language, type, []) as ExtHostCell; + document?.insertRawCell(index, rawCell!); + return { + handle: rawCell.handle, + source: rawCell.source, + language: rawCell.language, + cell_type: rawCell.cell_type, + outputs: rawCell.outputs, + isDirty: false + }; + } + + return; + } + + async $saveNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + let document = this._documents.get(URI.revive(uri).toString()); + + if (provider && document) { + return await provider.provider.save(document); + } + + return false; + } + } diff --git a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts index 03bec14009e..fa8ef7284e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts @@ -32,7 +32,7 @@ import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/sizeOb export const CELL_MARGIN = 24; -export class ViewCell { +export class ViewCell extends Disposable { private _textModel: ITextModel | null = null; private _mdRenderer: marked.Renderer | null = null; private _html: string | null = null; @@ -78,6 +78,8 @@ export class ViewCell { private readonly modelService: IModelService, private readonly modeService: IModeService ) { + super(); + this.id = UUID.generateUuid(); if (this.cell.onDidChangeOutputs) { this.cell.onDidChangeOutputs(() => { @@ -134,7 +136,7 @@ export class ViewCell { } save() { - if (this._textModel && this.isEditing) { + if (this._textModel && (this.cell.isDirty || this.isEditing)) { let cnt = this._textModel.getLineCount(); this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); } @@ -160,12 +162,16 @@ export class ViewCell { getTextModel(): ITextModel { if (!this._textModel) { - let mode = this.modeService.create(this.cell.language); + let mode = this.modeService.create(this.cellType === 'markdown' ? 'markdown' : this.cell.language); let ext = this.cellType === 'markdown' ? 'md' : 'py'; let resource = URI.parse(`notebookcell-${Date.now()}.${ext}`); resource = resource.with({ authority: `notebook+${this.viewType}-${this.notebookHandle}-${this.cell.handle}` }); let content = this.cell.source.join('\n'); this._textModel = this.modelService.createModel(content, mode, resource, false); + this._register(this._textModel); + this._register(this._textModel.onDidChangeContent(() => { + this.cell.isDirty = true; + })); } return this._textModel; @@ -182,7 +188,7 @@ export class ViewCell { } export interface NotebookHandler { - insertEmptyNotebookCell(listIndex: number | undefined, cell: ViewCell, type: 'markdown' | 'code', direction: 'above' | 'below'): void; + insertEmptyNotebookCell(listIndex: number | undefined, cell: ViewCell, type: 'markdown' | 'code', direction: 'above' | 'below'): Promise; deleteNotebookCell(listIndex: number | undefined, cell: ViewCell): void; editNotebookCell(listIndex: number | undefined, cell: ViewCell): void; saveNotebookCell(listIndex: number | undefined, cell: ViewCell): void; @@ -271,7 +277,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.handler.insertEmptyNotebookCell(listIndex, element, 'code', 'above'); + await this.handler.insertEmptyNotebookCell(listIndex, element, 'code', 'above'); } ); actions.push(insertAbove); @@ -282,7 +288,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.handler.insertEmptyNotebookCell(listIndex, element, 'code', 'below'); + await this.handler.insertEmptyNotebookCell(listIndex, element, 'code', 'below'); } ); actions.push(insertBelow); @@ -293,7 +299,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.handler.insertEmptyNotebookCell(listIndex, element, 'markdown', 'above'); + await this.handler.insertEmptyNotebookCell(listIndex, element, 'markdown', 'above'); } ); actions.push(insertMarkdownAbove); @@ -304,7 +310,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.handler.insertEmptyNotebookCell(listIndex, element, 'markdown', 'below'); + await this.handler.insertEmptyNotebookCell(listIndex, element, 'markdown', 'below'); } ); actions.push(insertMarkdownBelow); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 8784a0edfc1..86011451859 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -218,6 +218,14 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { } this.list?.splice(0, this.list?.length); + + if (this.model && !this.model.isDirty()) { + this.notebookService.destoryNotebookDocument(this.viewType!, this.notebook!); + this.model = undefined; + this.notebook = undefined; + this.viewType = undefined; + } + super.onHide(); } @@ -264,11 +272,11 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { })); let viewState = this.loadTextEditorViewState(input); - let notebook = model.getNotebook(); + this.notebook = model.getNotebook(); this.viewType = input.viewType; - this.viewCells = notebook.cells.map(cell => { + this.viewCells = this.notebook.cells.map(cell => { const isEditing = viewState && viewState.editingCells[cell.handle]; - return new ViewCell(input.viewType!, notebook.handle, cell, !!isEditing, this.modelService, this.modeService); + return new ViewCell(input.viewType!, this.notebook!.handle, cell, !!isEditing, this.modelService, this.modeService); }); const updateScrollPosition = () => { @@ -352,18 +360,19 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { } } - insertEmptyNotebookCell(listIndex: number | undefined, cell: ViewCell, type: 'code' | 'markdown', direction: 'above' | 'below') { - let newCell = new ViewCell(this.viewType!, this.notebook!.handle, { - handle: -1, - cell_type: type, - language: '', - source: [], - outputs: [] - }, false, this.modelService, this.modeService); + async insertEmptyNotebookCell(listIndex: number | undefined, cell: ViewCell, type: 'code' | 'markdown', direction: 'above' | 'below'): Promise { + let newLanguages = this.notebook!.languages; + let language = 'markdown'; + if (newLanguages && newLanguages.length) { + language = newLanguages[0]; + } let index = listIndex ? listIndex : this.model!.getNotebook().cells.indexOf(cell.cell); const insertIndex = direction === 'above' ? index : index + 1; + let newModeCell = await this.notebookService.createNotebookCell(this.viewType!, this.notebook!.uri, insertIndex, language, type); + let newCell = new ViewCell(this.viewType!, this.notebook!.handle, newModeCell!, false, this.modelService, this.modeService); + this.viewCells!.splice(insertIndex, 0, newCell); this.model!.insertCell(newCell.cell, insertIndex); this.list?.splice(insertIndex, 0, [newCell]); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 3747ea0cc41..bdaf84d58b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -6,9 +6,6 @@ import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; -import { format } from 'vs/base/common/jsonFormatter'; -import { applyEdits } from 'vs/base/common/jsonEdit'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebook, ICell } from 'vs/editor/common/modes'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; @@ -29,6 +26,10 @@ export class NotebookEditorModel extends EditorModel { super(); if (_notebook && _notebook.onDidChangeCells) { + this._register(_notebook.onDidChangeDirtyState((newState) => { + this._dirty = newState; + this._onDidChangeDirty.fire(); + })); this._register(_notebook.onDidChangeCells(() => { this._onDidChangeCells.fire(); })); @@ -70,12 +71,19 @@ export class NotebookEditorModel extends EditorModel { } } - save() { - let content = JSON.stringify(this._notebook); - let edits = format(content, undefined, {}); - this.textModel.setValue(applyEdits(content, edits)); - this._dirty = false; - this._onDidChangeDirty.fire(); + async save(): Promise { + if (this._notebook) { + let ret = await this._notebook.save(); + + if (ret) { + this._dirty = false; + this._onDidChangeDirty.fire(); + // todo, flush all states + return true; + } + } + + return false; } } @@ -88,7 +96,6 @@ export class NotebookEditorInput extends EditorInput { public readonly editorInput: IEditorInput, public readonly viewType: string | undefined, @INotebookService private readonly notebookService: INotebookService, - @ITextFileService private readonly textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService ) { super(); @@ -106,13 +113,12 @@ export class NotebookEditorInput extends EditorInput { return this.textModel?.isDirty() || false; } - save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { if (this.textModel) { - this.textModel.save(); - return this.textFileService.save(this.textModel.textModel.uri); + return await this.textModel.save(); } - return Promise.resolve(true); + return false; } getResource() { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 836a22aea24..d066027bdf4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -5,7 +5,7 @@ import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { INotebook } from 'vs/editor/common/modes'; +import { INotebook, ICell } from 'vs/editor/common/modes'; import { URI } from 'vs/base/common/uri'; import { notebookExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; @@ -22,7 +22,9 @@ export interface IMainNotebookController { executeNotebook(viewType: string, uri: URI): Promise; updateNotebook(uri: URI, notebook: INotebook): void; updateNotebookActiveCell(uri: URI, cellHandle: number): void; + createRawCell(uri: URI, index: number, language: string, type: 'markdown' | 'code'): Promise; executeNotebookActiveCell(uri: URI): void; + destoryNotebookDocument(notebook: INotebook): void; } export interface INotebookService { @@ -35,6 +37,8 @@ export interface INotebookService { getContributedNotebook(resource: URI): readonly NotebookProviderInfo[]; getNotebookProviderResourceRoots(): URI[]; updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void; + createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: 'markdown' | 'code'): Promise; + destoryNotebookDocument(viewType: string, notebook: INotebook): void; } export class NotebookInfoStore { @@ -143,6 +147,16 @@ export class NotebookService extends Disposable implements INotebookService { } } + async createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: 'markdown' | 'code'): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.createRawCell(resource, index, language, type); + } + + return; + } + async executeNotebook(viewType: string, uri: URI): Promise { let provider = this._notebookProviders.get(viewType); @@ -174,6 +188,14 @@ export class NotebookService extends Disposable implements INotebookService { return ret; } + destoryNotebookDocument(viewType: string, notebook: INotebook): void { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + provider.controller.destoryNotebookDocument(notebook); + } + } + private _onWillDispose(model: INotebook): void { let modelId = MODEL_ID(model.uri); let modelData = this._models[modelId];