diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index 2c5aef5479a..e4eba9aefb3 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -112,6 +112,7 @@ suite('notebook workflow', () => { // ---- ---- // + await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); @@ -161,6 +162,7 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('notebook.cell.execute'); assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); @@ -183,6 +185,90 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('notebook.execute'); assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); +}); + +suite('notebook dirty state', () => { + test('notebook open', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + const activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.notEqual(vscode.notebook.activeNotebookEditor!.selection, undefined); + assert.equal(activeCell!.source, ''); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); + + + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + // await vscode.commands.executeCommand('workbench.action.files.save'); + 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[1], vscode.notebook.activeNotebookEditor?.selection); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'var abc = 0;'); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); +}); + +suite('notebook undo redo', () => { + test('notebook open', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + const activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.notEqual(vscode.notebook.activeNotebookEditor!.selection, undefined); + assert.equal(activeCell!.source, ''); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); + + + // modify the second cell, delete it + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + await vscode.commands.executeCommand('notebook.cell.delete'); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); + + + // undo should bring back the deleted cell, and revert to previous content and selection + await vscode.commands.executeCommand('notebook.undo'); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'var abc = 0;'); + + // redo + // await vscode.commands.executeCommand('notebook.redo'); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); + // assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test'); + + await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); }); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 1f24a1e45b3..c4cadbb7181 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -1276,7 +1277,8 @@ export class ValidAnnotatedEditOperation implements IIdentifiedSingleEditOperati /** * @internal */ -export interface ITextBuffer { +export interface IReadonlyTextBuffer { + onDidChangeContent: Event; equals(other: ITextBuffer): boolean; mightContainRTL(): boolean; mightContainNonBasicASCII(): boolean; @@ -1299,10 +1301,15 @@ export interface ITextBuffer { getLineLength(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; + findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; +} +/** + * @internal + */ +export interface ITextBuffer extends IReadonlyTextBuffer { setEOL(newEOL: '\r\n' | '\n'): void; applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult; - findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; } /** diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 3dd4acda9d6..63ca1517de7 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -11,6 +12,7 @@ import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTex import { SearchData } from 'vs/editor/common/model/textModelSearch'; import { countEOL, StringEOL } from 'vs/editor/common/model/tokensStore'; import { TextChange } from 'vs/editor/common/model/textChange'; +import { IDisposable } from 'vs/base/common/lifecycle'; export interface IValidatedEditOperation { sortIndex: number; @@ -30,18 +32,24 @@ export interface IReverseSingleEditOperation extends IValidEditOperation { sortIndex: number; } -export class PieceTreeTextBuffer implements ITextBuffer { +export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { private readonly _pieceTree: PieceTreeBase; private readonly _BOM: string; private _mightContainRTL: boolean; private _mightContainNonBasicASCII: boolean; + private readonly _onDidChangeContent: Emitter = new Emitter(); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, isBasicASCII: boolean, eolNormalized: boolean) { this._BOM = BOM; this._mightContainNonBasicASCII = !isBasicASCII; this._mightContainRTL = containsRTL; this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized); } + dispose(): void { + this._onDidChangeContent.dispose(); + } // #region TextBuffer public equals(other: ITextBuffer): boolean { @@ -360,6 +368,8 @@ export class PieceTreeTextBuffer implements ITextBuffer { } } + this._onDidChangeContent.fire(); + return new ApplyEditsResult( reverseOperations, contentChanges, diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 52b353c6358..e6b470a236c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -7,7 +7,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; +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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index fbd01fd7e92..b4d5ce34e00 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -16,7 +16,7 @@ import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, IN import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; @@ -1403,7 +1403,7 @@ registerAction2(class extends Action2 { async function splitCell(context: INotebookCellActionContext): Promise { if (context.cell.cellKind === CellKind.Code) { - const newCells = context.notebookEditor.splitNotebookCell(context.cell); + const newCells = await context.notebookEditor.splitNotebookCell(context.cell); if (newCells) { context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], true); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 9e8f0d4c185..a726ac0d66a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -9,7 +9,7 @@ import { parse } from 'vs/base/common/marshalling'; import { basename, isEqual } from 'vs/base/common/resources'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ITextModel } from 'vs/editor/common/model'; +import { ITextModel, ITextBufferFactory, DefaultEndOfLine, ITextBuffer } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -26,7 +26,8 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { INotebookService, NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -247,14 +248,25 @@ class CellContentProvider implements ITextModelContentProvider { if (!info) { return null; } - const notebook = await this._notebookService.resolveNotebook(info.id, data.notebook); - if (!notebook) { + + const editorModel = await this._notebookService.modelManager.get(data.notebook); + if (!editorModel) { return null; } - for (let cell of notebook.cells) { + + for (let cell of editorModel.notebook.cells) { if (cell.uri.toString() === resource.toString()) { - const bufferFactory = cell.resolveTextBufferFactory(); - const language = cell.cellKind === CellKind.Markdown ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.source[0])); + const bufferFactory: ITextBufferFactory = { + create: (defaultEOL) => { + const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n'); + (cell.textBuffer as ITextBuffer).setEOL(newEOL); + return cell.textBuffer as ITextBuffer; + }, + getFirstLineText: (limit: number) => { + return cell.textBuffer.getLineContent(1).substr(0, limit); + } + }; + const language = cell.cellKind === CellKind.Markdown ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1))); return this._modelService.createModel( bufferFactory, language, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index c4e437ec0d8..dc9b678e95b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -17,14 +17,14 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; -import { FindMatch } from 'vs/editor/common/model'; -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, IOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -93,6 +93,7 @@ export interface MarkdownCellLayoutChangeEvent { export interface ICellViewModel { readonly model: NotebookCellTextModel; readonly id: string; + readonly textBuffer: IReadonlyTextBuffer; dragging: boolean; handle: number; uri: URI; @@ -103,14 +104,19 @@ export interface ICellViewModel { currentTokenSource: CancellationTokenSource | undefined; focusMode: CellFocusMode; getText(): string; - save(): void; metadata: NotebookCellMetadata | undefined; + textModel: ITextModel | undefined; + hasModel(): this is IEditableCellViewModel; + resolveTextModel(): Promise; getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata; getSelectionsStartPosition(): IPosition[] | undefined; - setLinesContent(value: string[]): void; getLinesContent(): string[]; } +export interface IEditableCellViewModel extends ICellViewModel { + textModel: ITextModel; +} + export interface INotebookEditorMouseEvent { readonly event: MouseEvent; readonly target: CellViewModel; @@ -175,7 +181,7 @@ export interface INotebookEditor { /** * Split a given cell into multiple cells of the same type using the selection start positions. */ - splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null; + splitNotebookCell(cell: ICellViewModel): Promise; /** * Joins the given cell either with the cell above or the one below depending on the given direction. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 53e8583e2b8..5f6b6ac2c60 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -29,9 +29,10 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorOptions, IEditorCloseEvent, IEditorMemento } from 'vs/workbench/common/editor'; import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, INotebookEditorContribution, NOTEBOOK_EDITOR_RUNNABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, INotebookEditorContribution, NOTEBOOK_EDITOR_RUNNABLE, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; @@ -417,7 +418,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { await super.setInput(input, options, token); const model = await input.resolve(); - if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model) || this.webview === null) { + if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { this.detachModel(); await this.attachModel(input, model); } @@ -477,7 +478,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { await this.webview.waitForInitialization(); this.eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model, this.eventDispatcher, this.getLayoutInfo()); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model.notebook, this.eventDispatcher, this.getLayoutInfo()); this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this.updateForMetadata(); @@ -637,8 +638,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { state.contributionsState = contributionsState; this.editorMemento.saveEditorState(this.group, input.resource, state); - - this.notebookViewModel.viewCells.forEach(cell => cell.save()); } } @@ -828,60 +827,43 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return boundaries.length > 2 ? boundaries : null; } - private computeCellLinesContents(cell: ICellViewModel, splitPoints: IPosition[]): string[][] | null { + private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { const lines = cell.getLinesContent(); const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, lines); if (!rangeBoundaries) { return null; } - const newLineModels: string[][] = []; + const newLineModels: string[] = []; for (let i = 1; i < rangeBoundaries.length; i++) { const start = rangeBoundaries[i - 1]; const end = rangeBoundaries[i]; - // get the right lines - const newLines = lines.slice(start.lineNumber - 1, end.lineNumber); - if (start.lineNumber === end.lineNumber) { - // cut the line at the beginning and the end - let line = newLines[0]; - line = line.slice(start.column - 1, end.column - 1); - newLines[0] = line; - } - else { - // cut last line at the end - let lastLine = newLines[newLines.length - 1]; - lastLine = lastLine.slice(0, end.column - 1); - if (lastLine) { - newLines[newLines.length - 1] = lastLine; - } else { - newLines.pop(); - } - // cut first line at the beginning - let firstLine = newLines[0]; - firstLine = firstLine.slice(start.column - 1); - if (firstLine) { - newLines[0] = firstLine; - } else { - newLines.shift(); - } - } - newLineModels.push(newLines); + newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); } + return newLineModels; } - splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null { + async splitNotebookCell(cell: ICellViewModel): Promise { if (!this.notebookViewModel!.metadata.editable) { return null; } let splitPoints = cell.getSelectionsStartPosition(); if (splitPoints && splitPoints.length > 0) { + await cell.resolveTextModel(); + + if (!cell.hasModel()) { + return null; + } + let newLinesContents = this.computeCellLinesContents(cell, splitPoints); if (newLinesContents) { // update the contents of the first cell - cell.setLinesContent(newLinesContents[0]); + cell.textModel.applyEdits([ + { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } + ], true); // create new cells based on the new text models const language = cell.model.language; @@ -921,8 +903,19 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { if (constraint && above.cellKind !== constraint) { return null; } - const newContent = above.getLinesContent().concat(cell.getLinesContent()); - above.setLinesContent(newContent); + + await above.resolveTextModel(); + if (!above.hasModel()) { + return null; + } + + const insertContent = cell.getText(); + const aboveCellLineCount = above.textModel.getLineCount(); + const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); + above.textModel.applyEdits([ + { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } + ]); + await this.deleteNotebookCell(cell); return above; } else { @@ -930,8 +923,20 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { if (constraint && below.cellKind !== constraint) { return null; } - const newContent = cell.getLinesContent().concat(below.getLinesContent()); - cell.setLinesContent(newContent); + + await cell.resolveTextModel(); + if (!cell.hasModel()) { + return null; + } + + const insertContent = below.getText(); + + const cellLineCount = cell.textModel.getLineCount(); + const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); + cell.textModel.applyEdits([ + { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } + ]); + await this.deleteNotebookCell(below); return cell; } @@ -942,7 +947,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return false; } - (cell as CellViewModel).save(); const index = this.notebookViewModel!.getCellIndex(cell); this.notebookViewModel!.deleteCell(index, true); return true; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 7cfae93b735..088143de4db 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -3,94 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; -import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { ICell, NotebookCellTextModelSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { isEqual } from 'vs/base/common/resources'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; - -export class NotebookEditorModel extends EditorModel { - private _dirty = false; - - protected readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - private readonly _onDidChangeCells = new Emitter(); - get onDidChangeCells(): Event { return this._onDidChangeCells.event; } - - private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent: Event = this._onDidChangeContent.event; - - - get notebook() { - return this._notebook; - } - - constructor( - private _notebook: NotebookTextModel - ) { - super(); - - if (_notebook && _notebook.onDidChangeCells) { - this._register(_notebook.onDidChangeContent(() => { - this._dirty = true; - this._onDidChangeDirty.fire(); - this._onDidChangeContent.fire(); - })); - this._register(_notebook.onDidChangeCells((e) => { - this._onDidChangeCells.fire(e); - })); - } - } - - isDirty() { - return this._dirty; - } - - getNotebook(): NotebookTextModel { - return this._notebook; - } - - insertCell(cell: ICell, index: number) { - let notebook = this.getNotebook(); - - if (notebook) { - this.notebook.insertNewCell(index, [cell as NotebookCellTextModel]); - this._dirty = true; - this._onDidChangeDirty.fire(); - - } - } - - deleteCell(index: number) { - let notebook = this.getNotebook(); - - if (notebook) { - this.notebook.removeCell(index); - } - } - - moveCellToIdx(index: number, newIdx: number) { - this.notebook.moveCellToIdx(index, newIdx); - } - - async save(): Promise { - if (this._notebook) { - this._dirty = false; - this._onDidChangeDirty.fire(); - // todo, flush all states - return true; - } - - return false; - } -} +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; export class NotebookEditorInput extends EditorInput { @@ -113,36 +32,16 @@ export class NotebookEditorInput extends EditorInput { } static readonly ID: string = 'workbench.input.notebook'; - private promise: Promise | null = null; private textModel: NotebookEditorModel | null = null; - private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent: Event = this._onDidChangeContent.event; constructor( public resource: URI, public name: string, public readonly viewType: string | undefined, @INotebookService private readonly notebookService: INotebookService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { super(); - - const input = this; - const workingCopyAdapter = new class implements IWorkingCopy { - readonly resource = input.resource.with({ scheme: 'vscode-notebook' }); - get name() { return input.getName(); } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; - readonly onDidChangeDirty = input.onDidChangeDirty; - readonly onDidChangeContent = input.onDidChangeContent; - isDirty(): boolean { return input.isDirty(); } - backup(): Promise { return input.backup(); } - save(options?: ISaveOptions): Promise { return input.save(0, options).then(editor => !!editor); } - revert(options?: IRevertOptions): Promise { return input.revert(0, options); } - }; - - this._register(this.workingCopyService.registerWorkingCopy(workingCopyAdapter)); - } getTypeId(): string { @@ -179,7 +78,6 @@ export class NotebookEditorInput extends EditorInput { async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this.textModel) { - await this.notebookService.save(this.textModel.notebook.viewType, this.textModel.notebook.uri); await this.textModel.save(); return this; } @@ -187,38 +85,18 @@ export class NotebookEditorInput extends EditorInput { return undefined; } - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - if (this.textModel) { - // TODO@rebornix we need hashing - await this.textModel.save(); - } - } - async resolve(): Promise { - if (this.textModel) { - return this.textModel; + if (!await this.notebookService.canResolve(this.viewType!)) { + throw new Error(`Cannot open notebook of type '${this.viewType}'`); } - if (!this.promise) { - if (!await this.notebookService.canResolve(this.viewType!)) { - throw new Error(`Cannot open notebook of type '${this.viewType}'`); - } + this.textModel = await this.notebookService.modelManager.resolve(this.resource, this.viewType!); - this.promise = this.notebookService.resolveNotebook(this.viewType!, this.resource).then(notebook => { - this.textModel = new NotebookEditorModel(notebook!); - this.textModel.onDidChangeDirty(() => this._onDidChangeDirty.fire()); - this.textModel.onDidChangeContent(() => { - this._onDidChangeContent.fire(); - }); - return this.textModel; - }); - } + this._register(this.textModel.onDidChangeDirty(() => { + this._onDidChangeDirty.fire(); + })); - return this.promise; - } - - async backup(): Promise { - throw new Error(); + return this.textModel; } matches(otherInput: unknown): boolean { @@ -235,6 +113,7 @@ export class NotebookEditorInput extends EditorInput { dispose() { if (this.textModel) { this.notebookService.destoryNotebookDocument(this.textModel!.notebook.viewType, this.textModel!.notebook); + this.textModel.dispose(); } super.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts similarity index 83% rename from src/vs/workbench/contrib/notebook/browser/notebookService.ts rename to src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 5aa8c51a855..a429c7c7b6e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; @@ -19,46 +19,13 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IEditorService, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookEditorModelManager } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; function MODEL_ID(resource: URI): string { return resource.toString(); } -export const INotebookService = createDecorator('notebookService'); - -export interface IMainNotebookController { - resolveNotebook(viewType: string, uri: URI): 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; - save(uri: URI): Promise; -} - -export interface INotebookService { - _serviceBrand: undefined; - canResolve(viewType: string): Promise; - onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }>; - registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; - unregisterNotebookProvider(viewType: string): void; - 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; - executeNotebook(viewType: string, uri: URI): Promise; - executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; - - getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; - getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; - getNotebookProviderResourceRoots(): URI[]; - destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; - updateActiveNotebookDocument(viewType: string, resource: URI): void; - save(viewType: string, resource: URI): Promise; - onDidReceiveMessage(viewType: string, uri: URI, message: any): void; - setToCopy(items: NotebookCellTextModel[]): void; - getToCopy(): NotebookCellTextModel[] | undefined; -} - export class NotebookProviderInfoStore { private readonly contributedEditors = new Map(); @@ -126,8 +93,6 @@ class ModelData implements IDisposable { this._modelEventListeners.dispose(); } } - - export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { _serviceBrand: undefined; private readonly _notebookProviders = new Map(); @@ -142,13 +107,18 @@ export class NotebookService extends Disposable implements INotebookService, ICu onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; private cutItems: NotebookCellTextModel[] | undefined; + modelManager: NotebookEditorModelManager; + constructor( @IExtensionService private readonly extensionService: IExtensionService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); this._models = {}; + this.modelManager = this.instantiationService.createInstance(NotebookEditorModelManager); + notebookProviderExtensionPoint.setHandler((extensions) => { this.notebookProviderInfoStore.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index d21b9552da0..5f7766fd2e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -527,7 +527,6 @@ export class NotebookCellList extends WorkbenchList implements ID } } - // TODO@rebornix TEST & Fix potential bugs // List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done. // For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view. // To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport. diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 3e0e347568b..9cd53cc37ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 2de108d6699..2cf05182d74 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -470,7 +470,7 @@ const DRAGOVER_CLASS = 'cell-dragover'; type DragImageProvider = () => HTMLElement; export class CellDragAndDropController { - // TODO roblou - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need + // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need // to figure out how to prevent that private currentDraggedCell: ICellViewModel | undefined; @@ -490,7 +490,7 @@ export class CellDragAndDropController { }; templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { - // TODO + // TODO@roblourens (this.notebookEditor.getInnerWebview() as any)!.element.style['pointer-events'] = ''; // Note, templateData may have a different element rendered into it by now @@ -545,7 +545,7 @@ export class CellDragAndDropController { export class CellLanguageStatusBarItem extends Disposable { private labelElement: HTMLElement; - private _cell: BaseCellViewModel | undefined; + private _cell: ICellViewModel | undefined; private _editor: INotebookEditor | undefined; private cellDisposables: DisposableStore; @@ -567,7 +567,7 @@ export class CellLanguageStatusBarItem extends Disposable { this._register(this.cellDisposables = new DisposableStore()); } - update(cell: BaseCellViewModel, editor: INotebookEditor): void { + update(cell: ICellViewModel, editor: INotebookEditor): void { this.cellDisposables.clear(); this._cell = cell; this._editor = editor; @@ -648,7 +648,7 @@ class CodeCellDragImageRenderer { getDragImage(templateData: CodeCellRenderTemplate): HTMLElement { let dragImage = this._getDragImage(templateData); if (!dragImage) { - // TODO I don't think this can happen + // TODO@roblourens I don't think this can happen dragImage = document.createElement('div'); dragImage.textContent = '1 cell'; } @@ -819,7 +819,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } else if (metadata.runState === NotebookCellRunState.Error) { templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(error)'); } else if (metadata.runState === NotebookCellRunState.Running) { - // TODO should extensions be able to customize the status message while running to show progress? + // TODO@roblourens should extensions be able to customize the status message while running to show progress? templateData.cellStatusMessageContainer.textContent = nls.localize('cellRunningStatus', "Running"); templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(sync~spin)'); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index c94df65b4ec..339a1717ea2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellOutputKind, IOutput, IRenderOutput, ITransformedDisplayOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -220,7 +220,7 @@ export class CodeCell extends Disposable { this.templateData.outputContainer!.style.display = 'block'; // there are outputs, we need to calcualte their sizes and trigger relayout - // @todo, if there is no resizable output, we should not check their height individually, which hurts the performance + // @TODO@rebornix, if there is no resizable output, we should not check their height individually, which hurts the performance for (let index = 0; index < this.viewCell.outputs.length; index++) { const currOutput = this.viewCell.outputs[index]; @@ -379,7 +379,7 @@ export class CodeCell extends Disposable { } else { // static output - // @TODO, if we stop checking output height, we need to evaluate it later when checking the height of output container + // @TODO@rebornix, if we stop checking output height, we need to evaluate it later when checking the height of output container let clientHeight = outputItemDiv.clientHeight; this.viewCell.updateOutputHeight(index, clientHeight); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index a952cb7af47..b50f970899a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -241,7 +241,9 @@ export class StatefullMarkdownCell extends Disposable { bindEditorListeners(model: ITextModel, dimension?: IDimension) { this.localDisposables.add(model.onDidChangeContent(() => { - this.viewCell.setLinesContent(model.getLinesContent()); + // we don't need to update view cell text anymore as the textbuffer is shared + // this.viewCell.setText(model.getLinesContent()); + this.viewCell.clearHTML(); let clientHeight = this.markdownContainer.clientHeight; this.markdownContainer.innerHTML = ''; let renderedHTML = this.viewCell.getHTML(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 0ff66da32b9..524eb216671 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -13,7 +13,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, CellRunState, CursorAtBoundary, ICellViewModel, CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, CellRunState, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -22,7 +22,7 @@ export const NotebookCellMetadataDefaults = { runnable: true }; -export abstract class BaseCellViewModel extends Disposable implements ICellViewModel { +export abstract class BaseCellViewModel extends Disposable { protected readonly _onDidChangeEditorAttachState = new Emitter(); // Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere. readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; @@ -36,7 +36,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM return this.model.uri; } get lineCount() { - return this.model.source.length; + return this.model.textBuffer.getLineCount(); } get metadata() { return this.model.metadata; @@ -65,7 +65,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM } } - // TODO - move any "run"/"status" concept to Code-specific places + // TODO@roblourens - move any "run"/"status" concept to Code-specific places private _currentTokenSource: CancellationTokenSource | undefined; public set currentTokenSource(v: CancellationTokenSource | undefined) { this._currentTokenSource = v; @@ -106,6 +106,14 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM private _lastDecorationId: number = 0; protected _textModel?: model.ITextModel; + get textModel(): model.ITextModel | undefined { + return this._textModel; + } + + hasModel(): this is IEditableCellViewModel { + return !!this._textModel; + } + private _dragging: boolean = false; get dragging(): boolean { return this._dragging; @@ -127,6 +135,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM })); } + // abstract resolveTextModel(): Promise; abstract hasDynamicHeight(): boolean; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; @@ -193,11 +202,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM } getText(): string { - if (this._textModel) { - return this._textModel.getValue(); - } - - return this.model.source.join('\n'); + return this.model.getValue(); } getLinesContent(): string[] { @@ -205,19 +210,19 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM return this._textModel.getLinesContent(); } - return this.model.source; + return this.model.textBuffer.getLinesContent(); } - setLinesContent(value: string[]) { - if (this._textModel) { - // TODO @rebornix we should avoid creating a new string here - return this._textModel.setValue(value.join('\n')); - } else { - this.model.source = value; - } - } - - abstract save(): void; + // setLinesContent(value: string[]) { + // if (this._textModel) { + // // TODO @rebornix we should avoid creating a new string here + // return this._textModel.setValue(value.join('\n')); + // } else { + // const range = this.model.getFullModelRange(); + // this.model.textBuffer. + // this.model.source = value; + // } + // } private saveViewState(): void { if (!this._textEditor) { @@ -340,7 +345,9 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM return CursorAtBoundary.None; } - protected _buffer: model.ITextBuffer | null = null; + get textBuffer() { + return this.model.textBuffer; + } protected cellStartFind(value: string): model.FindMatch[] | null { let cellMatches: model.FindMatch[] = []; @@ -348,12 +355,8 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM if (this.assertTextModelAttached()) { cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); } else { - if (!this._buffer) { - this._buffer = this.model.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF); - } - - const lineCount = this._buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1); + const lineCount = this.textBuffer.getLineCount(); + const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); const searchParams = new SearchParams(value, false, false, null); const searchData = searchParams.parseSearchRequest(); @@ -361,7 +364,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM return null; } - cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000); + cellMatches = this.textBuffer.findMatchesLineByLine(fullRange, searchData, false, 1000); } return cellMatches; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 46c837d3c00..0803c645e71 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -67,7 +67,8 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { this._rawCell = cell.model; // save inmem text to `ICell` - this._rawCell.source = [cell.getText()]; + // no needed any more as the text buffer is transfered to `raw_cell` + // this._rawCell.source = [cell.getText()]; } undo(): void | Promise { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 992aa5bdbf9..cbcab113254 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -81,7 +81,6 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod })); this._outputCollection = new Array(this.model.outputs.length); - this._buffer = null; this._layoutInfo = { fontInfo: initialNotebookLayoutInfo?.fontInfo || null, @@ -171,21 +170,16 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } } - save() { - if (this._textModel && !this._textModel.isDisposed() && this.editState === CellEditState.Editing) { - this.model.source = this._textModel.getLinesContent(); - } - } - + /** + * Text model is used for editing. + */ async resolveTextModel(): Promise { if (!this._textModel) { const ref = await this._modelService.createModelReference(this.model.uri); this._textModel = ref.object.textEditorModel; - this._buffer = this._textModel.getTextBuffer(); this._register(ref); this._register(this._textModel.onDidChangeContent(() => { this.editState = CellEditState.Editing; - this.model.contentChange(); this._onDidChangeState.fire({ contentChanged: true }); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 2f9223a7259..05772e1ec8b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -112,18 +112,10 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie } } - setLinesContent(strs: string[]) { - this.model.source = strs; + clearHTML() { this._html = null; } - save() { - if (this._textModel && !this._textModel.isDisposed() && this.editState === CellEditState.Editing) { - let cnt = this._textModel.getLineCount(); - this.model.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); - } - } - getHTML(): HTMLElement | null { if (this.cellKind === CellKind.Markdown) { if (this._html) { @@ -140,10 +132,8 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie if (!this._textModel) { const ref = await this._modelService.createModelReference(this.model.uri); this._textModel = ref.object.textEditorModel; - this._buffer = this._textModel.getTextBuffer(); this._register(ref); this._register(this._textModel.onDidChangeContent(() => { - this.model.contentChange(); this._html = null; this._onDidChangeState.fire({ contentChanged: true }); })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 346870c5d53..e8016684b27 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -19,7 +19,6 @@ import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { DeleteCellEdit, InsertCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; @@ -28,6 +27,7 @@ import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/vie import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -172,27 +172,27 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } get notebookDocument() { - return this._model.notebook; + return this._notebook; } get renderers() { - return this._model.notebook!.renderers; + return this._notebook!.renderers; } get handle() { - return this._model.notebook.handle; + return this._notebook.handle; } get languages() { - return this._model.notebook.languages; + return this._notebook.languages; } get uri() { - return this._model.notebook.uri; + return this._notebook.uri; } get metadata() { - return this._model.notebook.metadata; + return this._notebook.metadata; } private readonly _onDidChangeViewCells = new Emitter(); @@ -227,7 +227,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } this._selections = selections; - this._model.notebook.selections = selections; + this._notebook.selections = selections; this._onDidChangeSelection.fire(); } @@ -241,7 +241,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD constructor( public viewType: string, - private _model: NotebookEditorModel, + private _notebook: NotebookTextModel, readonly eventDispatcher: NotebookEventDispatcher, private _layoutInfo: NotebookLayoutInfo | null, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -254,7 +254,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.id = '$notebookViewModel' + MODEL_ID; this._instanceId = strings.singleLetterHash(MODEL_ID); - this._register(this._model.onDidChangeCells(e => { + this._register(this._notebook.onDidChangeCells(e => { const diffs = e.map(splice => { return [splice[0], splice[1], splice[2].map(cell => { return createCellViewModel(this.instantiationService, this, cell as NotebookCellTextModel); @@ -315,7 +315,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.selectionHandles = endSelectionHandles; })); - this._register(this._model.notebook.onDidChangeMetadata(e => { + this._register(this._notebook.onDidChangeMetadata(e => { this.eventDispatcher.emit([new NotebookMetadataChangedEvent(e)]); })); @@ -335,7 +335,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); })); - this._viewCells = this._model!.notebook!.cells.map(cell => { + this._viewCells = this._notebook!.cells.map(cell => { return createCellViewModel(this.instantiationService, this, cell); }); @@ -419,10 +419,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._hiddenRanges; } - isDirty() { - return this._model.isDirty(); - } - hide() { this._viewCells.forEach(cell => { if (cell.getText() !== '') { @@ -465,7 +461,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } getVersionId() { - return this._model.notebook.versionId; + return this._notebook.versionId; } getTrackedRange(id: string): ICellRange | null { @@ -580,7 +576,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) { this._viewCells!.splice(insertIndex, 0, insertCell); this._handleToViewCellMapping.set(insertCell.handle, insertCell); - this._model.insertCell(insertCell.model, insertIndex); + this._notebook.insertNewCell(insertIndex, [insertCell.model as NotebookCellTextModel]); this._localStore.add(insertCell); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] }); } @@ -590,7 +586,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(deleteIndex, 1); this._handleToViewCellMapping.delete(deleteCell.handle); - this._model.deleteCell(deleteIndex); + this._notebook.removeCell(deleteIndex); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -598,12 +594,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.selectionHandles = selections; } - createCell(index: number, source: string[], language: string, type: CellKind, synchronous: boolean) { - const cell = this._model.notebook.createCellTextModel(source, language, type, [], undefined); + createCell(index: number, source: string | string[], language: string, type: CellKind, synchronous: boolean) { + const cell = this._notebook.createCellTextModel(source, language, type, [], undefined); let newCell: CellViewModel = createCellViewModel(this.instantiationService, this, cell); this._viewCells!.splice(index, 0, newCell); this._handleToViewCellMapping.set(newCell.handle, newCell); - this._model.insertCell(cell, index); + this._notebook.insertNewCell(index, [cell]); this._localStore.add(newCell); this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { @@ -622,7 +618,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells!.splice(index, 0, newCell); this._handleToViewCellMapping.set(newCell.handle, newCell); - this._model.insertCell(newCell.model, index); + this._notebook.insertNewCell(index, [newCell.model]); this._localStore.add(newCell); this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { insertCell: this._insertCellDelegate.bind(this), @@ -642,7 +638,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(index, 1); this._handleToViewCellMapping.delete(viewCell.handle); - this._model.deleteCell(index); + this._notebook.removeCell(index); let endSelections: number[] = []; if (this.selectionHandles.length) { @@ -686,7 +682,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.viewCells.splice(index, 1); this.viewCells!.splice(newIdx, 0, viewCell); - this._model.moveCellToIdx(index, newIdx); + this._notebook.moveCellToIdx(index, newIdx); if (pushedToUndoStack) { this.undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { @@ -869,14 +865,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.undoService.redo(this.uri); } - equal(model: NotebookEditorModel) { - return this._model === model; + equal(notebook: NotebookTextModel) { + return this._notebook === notebook; } dispose() { this._localStore.clear(); this._viewCells.forEach(cell => { - cell.save(); cell.dispose(); }); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 9e80924fae3..4ce48fcb70a 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -5,10 +5,13 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ICell, IOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; +import * as model from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class NotebookCellTextModel implements ICell { +export class NotebookCellTextModel extends Disposable implements ICell { private _onDidChangeOutputs = new Emitter(); onDidChangeOutputs: Event = this._onDidChangeOutputs.event; @@ -27,15 +30,6 @@ export class NotebookCellTextModel implements ICell { return this._outputs; } - get source() { - return this._source; - } - - set source(newValue: string[]) { - this._source = newValue; - this._buffer = null; - } - private _metadata: NotebookCellMetadata | undefined; get metadata() { @@ -56,24 +50,52 @@ export class NotebookCellTextModel implements ICell { this._onDidChangeLanguage.fire(newLanguage); } - private _buffer: PieceTreeTextBufferFactory | null = null; + private _textBuffer!: model.IReadonlyTextBuffer; + + get textBuffer() { + if (this._textBuffer) { + return this._textBuffer; + } + + let builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + const bufferFactory = builder.finish(true); + this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); + + this._register(this._textBuffer.onDidChangeContent(() => { + this._onDidChangeContent.fire(); + })); + + return this._textBuffer; + } constructor( readonly uri: URI, public handle: number, - private _source: string[], + private _source: string | string[], private _language: string, public cellKind: CellKind, outputs: IOutput[], metadata: NotebookCellMetadata | undefined ) { + super(); this._outputs = outputs; this._metadata = metadata; } - contentChange() { - this._onDidChangeContent.fire(); + getValue(): string { + const fullRange = this.getFullModelRange(); + const eol = this.textBuffer.getEOL(); + if (eol === '\n') { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.LF); + } else { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.CRLF); + } + } + getFullModelRange() { + const lineCount = this.textBuffer.getLineCount(); + return new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); } spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void { @@ -83,15 +105,4 @@ export class NotebookCellTextModel implements ICell { this._onDidChangeOutputs.fire(splices); } - - resolveTextBufferFactory(): PieceTreeTextBufferFactory { - if (this._buffer) { - return this._buffer; - } - - let builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(this.source.join('\n')); - this._buffer = builder.finish(true); - return this._buffer; - } } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index fce0a5d86a3..4c5ca6ecf6b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -66,7 +66,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } createCellTextModel( - source: string[], + source: string | string[], language: string, cellKind: CellKind, outputs: IOutput[], @@ -200,7 +200,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel [{ handle: cell.handle, uri: cell.uri, - source: cell.source, + source: cell.textBuffer.getLinesContent(), language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, @@ -237,7 +237,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells.map(cell => ({ handle: cell.handle, uri: cell.uri, - source: cell.source, + source: cell.textBuffer.getLinesContent(), language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 406a728f335..620f9df6f97 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -10,9 +10,10 @@ import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; export enum CellKind { Markdown = 1, @@ -159,7 +160,6 @@ export type IOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutpu export interface ICell { readonly uri: URI; handle: number; - source: string[]; language: string; cellKind: CellKind; outputs: IOutput[]; @@ -167,9 +167,6 @@ export interface ICell { onDidChangeOutputs?: Event; onDidChangeLanguage: Event; onDidChangeMetadata: Event; - resolveTextBufferFactory(): PieceTreeTextBufferFactory; - // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel - contentChange(): void; } export interface LanguageInfo { @@ -465,3 +462,10 @@ export interface ICellEditorViewState { } export const NOTEBOOK_EDITOR_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('notebookEditorCursorAtBoundary', 'none'); + + +export interface INotebookEditorModel extends IEditorModel { + notebook: NotebookTextModel; + isDirty(): boolean; + save(): Promise; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts new file mode 100644 index 00000000000..a692999f413 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorModel, IRevertOptions } from 'vs/workbench/common/editor'; +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +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'; + +export interface INotebookEditorModelManager { + models: NotebookEditorModel[]; + + resolve(resource: URI, viewType: string): Promise; + + get(resource: URI): NotebookEditorModel | undefined; +} + + +export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { + private _dirty = false; + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private _notebook!: NotebookTextModel; + + get notebook() { + return this._notebook; + } + + private _name!: string; + + get name() { + return this._name; + } + + constructor( + public readonly resource: URI, + public readonly viewType: string, + @INotebookService private readonly notebookService: INotebookService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + ) { + super(); + this._register(this.workingCopyService.registerWorkingCopy(this)); + } + + capabilities = 0; + + async backup(): Promise { + return {}; + } + + async revert(options?: IRevertOptions | undefined): Promise { + return; + } + + async load(): Promise { + const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource); + this._notebook = notebook!; + + this._name = basename(this._notebook!.uri); + + this._register(this._notebook.onDidChangeContent(() => { + this._dirty = true; + this._onDidChangeDirty.fire(); + this._onDidChangeContent.fire(); + })); + + return this; + } + + isDirty() { + return this._dirty; + } + + async save(): Promise { + await this.notebookService.save(this.notebook.viewType, this.notebook.uri); + this._dirty = false; + this._onDidChangeDirty.fire(); + return true; + } +} + +export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager { + + private readonly mapResourceToModel = new ResourceMap(); + private readonly mapResourceToModelListeners = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); + private readonly mapResourceToPendingModelLoaders = new ResourceMap>(); + + // private readonly modelLoadQueue = this._register(new ResourceQueue()); + + get models(): NotebookEditorModel[] { + return this.mapResourceToModel.values(); + } + constructor( + @IInstantiationService readonly instantiationService: IInstantiationService + ) { + super(); + } + + async resolve(resource: URI, viewType: string): Promise { + // Return early if model is currently being loaded + const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); + if (pendingLoad) { + return pendingLoad; + } + + let modelPromise: Promise; + let model = this.get(resource); + // let didCreateModel = false; + + // Model exists + if (model) { + // if (options?.reload) { + // } else { + modelPromise = Promise.resolve(model); + // } + } + + // Model does not exist + else { + // didCreateModel = true; + const newModel = model = this.instantiationService.createInstance(NotebookEditorModel, resource, viewType); + modelPromise = model.load(); + + this.registerModel(newModel); + } + + // Store pending loads to avoid race conditions + this.mapResourceToPendingModelLoaders.set(resource, modelPromise); + + // Make known to manager (if not already known) + this.add(resource, model); + + // dispose and bind new listeners + + try { + const resolvedModel = await modelPromise; + + // Remove from pending loads + this.mapResourceToPendingModelLoaders.delete(resource); + return resolvedModel; + } catch (error) { + // Free resources of this invalid model + if (model) { + model.dispose(); + } + + // Remove from pending loads + this.mapResourceToPendingModelLoaders.delete(resource); + + throw error; + } + } + + add(resource: URI, model: NotebookEditorModel): void { + const knownModel = this.mapResourceToModel.get(resource); + if (knownModel === model) { + return; // already cached + } + + // dispose any previously stored dispose listener for this resource + const disposeListener = this.mapResourceToDisposeListener.get(resource); + if (disposeListener) { + disposeListener.dispose(); + } + + // store in cache but remove when model gets disposed + this.mapResourceToModel.set(resource, model); + this.mapResourceToDisposeListener.set(resource, model.onDispose(() => this.remove(resource))); + } + + remove(resource: URI): void { + this.mapResourceToModel.delete(resource); + + const disposeListener = this.mapResourceToDisposeListener.get(resource); + if (disposeListener) { + dispose(disposeListener); + this.mapResourceToDisposeListener.delete(resource); + } + + const modelListener = this.mapResourceToModelListeners.get(resource); + if (modelListener) { + dispose(modelListener); + this.mapResourceToModelListeners.delete(resource); + } + } + + + private registerModel(model: NotebookEditorModel): void { + + } + + get(resource: URI): NotebookEditorModel | undefined { + return this.mapResourceToModel.get(resource); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts new file mode 100644 index 00000000000..a736beba982 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +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 { 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'; +import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; + +export const INotebookService = createDecorator('notebookService'); + +export interface IMainNotebookController { + resolveNotebook(viewType: string, uri: URI): 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; + save(uri: URI): Promise; +} + +export interface INotebookService { + _serviceBrand: undefined; + modelManager: INotebookEditorModelManager; + canResolve(viewType: string): Promise; + onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }>; + registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; + unregisterNotebookProvider(viewType: string): void; + 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; + executeNotebook(viewType: string, uri: URI): Promise; + executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; + + getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; + getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; + getNotebookProviderResourceRoots(): URI[]; + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; + updateActiveNotebookDocument(viewType: string, resource: URI): void; + save(viewType: string, resource: URI): Promise; + onDidReceiveMessage(viewType: string, uri: URI, message: any): void; + setToCopy(items: NotebookCellTextModel[]): void; + getToCopy(): NotebookCellTextModel[] | undefined; +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index fde8bcebcad..06df06f42de 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -35,8 +35,8 @@ suite('NotebookTextModel', () => { assert.equal(textModel.cells.length, 6); - assert.equal(textModel.cells[1].source.join('\n'), 'var e = 5;'); - assert.equal(textModel.cells[4].source.join('\n'), 'var f = 6;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[4].getValue(), 'var f = 6;'); } ); }); @@ -60,8 +60,8 @@ suite('NotebookTextModel', () => { assert.equal(textModel.cells.length, 6); - assert.equal(textModel.cells[1].source.join('\n'), 'var e = 5;'); - assert.equal(textModel.cells[2].source.join('\n'), 'var f = 6;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[2].getValue(), 'var f = 6;'); } ); }); @@ -83,8 +83,8 @@ suite('NotebookTextModel', () => { { editType: CellEditType.Delete, index: 3, count: 1 }, ]); - assert.equal(textModel.cells[0].source.join('\n'), 'var a = 1;'); - assert.equal(textModel.cells[1].source.join('\n'), 'var c = 3;'); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[1].getValue(), 'var c = 3;'); } ); }); @@ -108,8 +108,8 @@ suite('NotebookTextModel', () => { assert.equal(textModel.cells.length, 4); - assert.equal(textModel.cells[0].source.join('\n'), 'var a = 1;'); - assert.equal(textModel.cells[2].source.join('\n'), 'var e = 5;'); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[2].getValue(), 'var e = 5;'); } ); }); @@ -132,9 +132,9 @@ suite('NotebookTextModel', () => { ]); assert.equal(textModel.cells.length, 4); - assert.equal(textModel.cells[0].source.join('\n'), 'var a = 1;'); - assert.equal(textModel.cells[1].source.join('\n'), 'var e = 5;'); - assert.equal(textModel.cells[2].source.join('\n'), 'var c = 3;'); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[2].getValue(), 'var c = 3;'); } ); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index a3eddf949a1..4acc1060be1 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -6,10 +6,9 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind, NotebookCellMetadata, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { withTestNotebook, TestCell, NotebookEditorTestModel } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -25,9 +24,9 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel(0, 'notebook', URI.parse('test')); - const model = new NotebookEditorModel(notebook); + const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); - const viewModel = new NotebookViewModel('notebook', model, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); + const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); assert.equal(viewModel.viewType, 'notebook'); }); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 100b8ced56b..35ded8ed17d 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { CellKind, IOutput, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IOutput, CellUri, NotebookCellMetadata, INotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookViewModel, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookEditor, NotebookLayoutInfo, ICellViewModel, ICellRange, INotebookEditorMouseEvent, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; @@ -22,25 +20,18 @@ import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/v import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; - +import { EditorModel } from 'vs/workbench/common/editor'; export class TestCell extends NotebookCellTextModel { constructor( public viewType: string, handle: number, - source: string[], + public source: string[], language: string, cellKind: CellKind, outputs: IOutput[] ) { super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined); } - contentChange(): void { - // throw new Error('Method not implemented.'); - } - - resolveTextBufferFactory(): PieceTreeTextBufferFactory { - throw new Error('Method not implemented.'); - } } export class TestNotebookEditor implements INotebookEditor { @@ -117,7 +108,7 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } - splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null { + splitNotebookCell(cell: ICellViewModel): Promise { throw new Error('Method not implemented.'); } @@ -219,6 +210,54 @@ export class TestNotebookEditor implements INotebookEditor { // return createCellViewModel(instantiationService, viewType, notebookHandle, mockCell); // } +export class NotebookEditorTestModel extends EditorModel implements INotebookEditorModel { + private _dirty = false; + + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + + get notebook() { + return this._notebook; + } + + constructor( + private _notebook: NotebookTextModel + ) { + super(); + + if (_notebook && _notebook.onDidChangeCells) { + this._register(_notebook.onDidChangeContent(() => { + this._dirty = true; + this._onDidChangeDirty.fire(); + this._onDidChangeContent.fire(); + })); + } + } + + isDirty() { + return this._dirty; + } + + getNotebook(): NotebookTextModel { + return this._notebook; + } + + async save(): Promise { + if (this._notebook) { + this._dirty = false; + this._onDidChangeDirty.fire(); + // todo, flush all states + return true; + } + + return false; + } +} + export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const viewType = 'notebook'; const editor = new TestNotebookEditor(); @@ -226,9 +265,9 @@ export function withTestNotebook(instantiationService: IInstantiationService, bl notebook.cells = cells.map((cell, index) => { return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4]); }); - const model = new NotebookEditorModel(notebook); + const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); - const viewModel = new NotebookViewModel(viewType, model, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); + const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); callback(editor, viewModel, notebook);