mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 18:09:01 +01:00
Merge pull request #100545 from microsoft/rebornix/separate-textmodel-selection
Notebook: separate selections and text model
This commit is contained in:
@@ -56,7 +56,32 @@ async function splitEditor() {
|
||||
await once;
|
||||
}
|
||||
|
||||
suite('API tests', () => {
|
||||
suite('Notebook API tests', () => {
|
||||
// test.only('crash', async function () {
|
||||
// for (let i = 0; i < 200; i++) {
|
||||
// let resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
|
||||
// await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
|
||||
// await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
|
||||
|
||||
// resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb'));
|
||||
// await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
|
||||
// await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
|
||||
// }
|
||||
// });
|
||||
|
||||
// test.only('crash', async function () {
|
||||
// for (let i = 0; i < 200; i++) {
|
||||
// let resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
|
||||
// await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
|
||||
// await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
// await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
// resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb'));
|
||||
// await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
|
||||
// await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
// await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
// }
|
||||
// });
|
||||
|
||||
test('document open/close event', async function () {
|
||||
const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
|
||||
const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument);
|
||||
@@ -222,6 +247,13 @@ suite('API tests', () => {
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
|
||||
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
|
||||
const firstEditor = vscode.notebook.activeNotebookEditor;
|
||||
assert.equal(firstEditor?.document.cells.length, 1);
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
test('notebook editor active/visible', async function () {
|
||||
@@ -290,7 +322,7 @@ suite('API tests', () => {
|
||||
assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]);
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
test('initialzation should not emit cell change events.', async function () {
|
||||
@@ -766,6 +798,9 @@ suite('metadata', () => {
|
||||
assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false);
|
||||
assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123);
|
||||
assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript');
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.saveAll');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
|
||||
@@ -781,6 +816,9 @@ suite('metadata', () => {
|
||||
const activeCell = vscode.notebook.activeNotebookEditor!.selection;
|
||||
assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1);
|
||||
assert.equal(activeCell?.metadata.custom!['testCellMetadata'] as number, 123);
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.saveAll');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -808,7 +846,7 @@ suite('regression', () => {
|
||||
await vscode.commands.executeCommand('vscode.openWith', resource, 'default');
|
||||
assert.equal(vscode.window.activeTextEditor?.document.uri.path, resource.path);
|
||||
|
||||
await vscode.commands.executeCommand('workbench.action.files.saveAll');
|
||||
await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
@@ -824,7 +862,7 @@ suite('regression', () => {
|
||||
assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first');
|
||||
assert.notEqual(vscode.window.activeTextEditor, undefined);
|
||||
|
||||
// await vscode.commands.executeCommand('workbench.action.files.saveAll');
|
||||
await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1762,7 +1762,7 @@ declare module 'vscode' {
|
||||
resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise<void>;
|
||||
saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
|
||||
saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
|
||||
readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent>;
|
||||
readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent | NotebookDocumentEditEvent>;
|
||||
backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise<NotebookDocumentBackup>;
|
||||
|
||||
kernel?: NotebookKernel;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol';
|
||||
import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -40,7 +41,7 @@ export class MainThreadNotebookDocument extends Disposable {
|
||||
) {
|
||||
super();
|
||||
|
||||
this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri);
|
||||
this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri, undoRedoService);
|
||||
this._register(this._textModel.onDidModelChangeProxy(e => {
|
||||
this._proxy.$acceptModelChanged(this.uri, e);
|
||||
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null });
|
||||
@@ -51,9 +52,18 @@ export class MainThreadNotebookDocument extends Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean): Promise<boolean> {
|
||||
async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): Promise<boolean> {
|
||||
await this.notebookService.transformEditsOutputs(this.textModel, edits);
|
||||
return this._textModel.$applyEdit(modelVersionId, edits);
|
||||
if (synchronous) {
|
||||
return this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, synchronous);
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
this._register(DOM.scheduleAtNextAnimationFrame(() => {
|
||||
const ret = this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, true);
|
||||
resolve(ret);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) {
|
||||
@@ -528,7 +538,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
await mainthreadNotebook.applyEdit(mainthreadNotebook.textModel.versionId, [
|
||||
{ editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 },
|
||||
{ editType: CellEditType.Insert, index: 0, cells: data.cells }
|
||||
], true);
|
||||
], true, false);
|
||||
}
|
||||
return mainthreadNotebook.textModel;
|
||||
}
|
||||
@@ -548,7 +558,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
index: 0,
|
||||
cells: backup.cells || []
|
||||
}
|
||||
], false);
|
||||
], false, true);
|
||||
|
||||
// create document in ext host with cells data
|
||||
await this._mainThreadNotebook.addNotebookDocument({
|
||||
@@ -625,7 +635,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
|
||||
|
||||
if (mainthreadNotebook) {
|
||||
return await mainthreadNotebook.applyEdit(modelVersionId, edits, true);
|
||||
return await mainthreadNotebook.applyEdit(modelVersionId, edits, true, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -674,7 +674,7 @@ async function moveCell(context: INotebookCellActionContext, direction: 'up' | '
|
||||
|
||||
if (result) {
|
||||
// move cell command only works when the cell container has focus
|
||||
await context.notebookEditor.focusNotebookCell(context.cell, 'container');
|
||||
await context.notebookEditor.focusNotebookCell(result, 'container');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,17 +210,17 @@ export interface INotebookEditor extends IEditor {
|
||||
/**
|
||||
* Move a cell up one spot
|
||||
*/
|
||||
moveCellUp(cell: ICellViewModel): Promise<boolean>;
|
||||
moveCellUp(cell: ICellViewModel): Promise<ICellViewModel | null>;
|
||||
|
||||
/**
|
||||
* Move a cell down one spot
|
||||
*/
|
||||
moveCellDown(cell: ICellViewModel): Promise<boolean>;
|
||||
moveCellDown(cell: ICellViewModel): Promise<ICellViewModel | null>;
|
||||
|
||||
/**
|
||||
* Move a cell above or below another cell
|
||||
*/
|
||||
moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<boolean>;
|
||||
moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<ICellViewModel | null>;
|
||||
|
||||
/**
|
||||
* Focus the container of a cell (the monaco editor inside is not focused).
|
||||
@@ -371,6 +371,7 @@ export interface INotebookEditor extends IEditor {
|
||||
}
|
||||
|
||||
export interface INotebookCellList {
|
||||
isDisposed: boolean
|
||||
readonly contextKeyService: IContextKeyService;
|
||||
elementAt(position: number): ICellViewModel | undefined;
|
||||
elementHeight(element: ICellViewModel): number;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
@@ -30,7 +30,7 @@ export class NotebookEditor extends BaseEditor {
|
||||
static readonly ID: string = 'workbench.editor.notebook';
|
||||
|
||||
private readonly _editorMemento: IEditorMemento<INotebookEditorViewState>;
|
||||
private readonly _groupListener = this._register(new MutableDisposable());
|
||||
private readonly _groupListener = this._register(new DisposableStore());
|
||||
private readonly _widgetDisposableStore: DisposableStore = new DisposableStore();
|
||||
private _widget: IBorrowValue<NotebookEditorWidget> = { value: undefined };
|
||||
private _rootElement!: HTMLElement;
|
||||
@@ -49,13 +49,13 @@ export class NotebookEditor extends BaseEditor {
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
@IEditorDropService private readonly _editorDropService: IEditorDropService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@INotebookEditorWidgetService private readonly _notebookWidgetService: INotebookEditorWidgetService,
|
||||
) {
|
||||
super(NotebookEditor.ID, telemetryService, themeService, storageService);
|
||||
this._editorMemento = this.getEditorMemento<INotebookEditorViewState>(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY);
|
||||
this._editorMemento = this.getEditorMemento<INotebookEditorViewState>(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY);
|
||||
}
|
||||
|
||||
set viewModel(newModel: NotebookViewModel | undefined) {
|
||||
@@ -100,7 +100,14 @@ export class NotebookEditor extends BaseEditor {
|
||||
|
||||
setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
|
||||
super.setEditorVisible(visible, group);
|
||||
this._groupListener.value = group?.onWillCloseEditor(e => this._saveEditorViewState(e.editor));
|
||||
if (group) {
|
||||
this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)));
|
||||
this._groupListener.add(group.onDidGroupChange(() => {
|
||||
if (this._editorGroupService.activeGroup !== group) {
|
||||
this._widget?.value?.updateEditorFocus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
this._saveEditorViewState(this.input);
|
||||
@@ -197,6 +204,10 @@ export class NotebookEditor extends BaseEditor {
|
||||
|
||||
private _saveEditorViewState(input: IEditorInput | undefined): void {
|
||||
if (this.group && this._widget.value && input instanceof NotebookEditorInput) {
|
||||
if (this._widget.value.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this._widget.value.getEditorViewState();
|
||||
this._editorMemento.saveEditorState(this.group, input.resource, state);
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeModel = new Emitter<NotebookTextModel | undefined>();
|
||||
private readonly _onDidChangeModel = this._register(new Emitter<NotebookTextModel | undefined>());
|
||||
readonly onDidChangeModel: Event<NotebookTextModel | undefined> = this._onDidChangeModel.event;
|
||||
|
||||
private readonly _onDidFocusEditorWidget = new Emitter<void>();
|
||||
private readonly _onDidFocusEditorWidget = this._register(new Emitter<void>());
|
||||
readonly onDidFocusEditorWidget = this._onDidFocusEditorWidget.event;
|
||||
|
||||
set viewModel(newModel: NotebookViewModel | undefined) {
|
||||
@@ -129,7 +129,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
}
|
||||
|
||||
private _activeKernel: INotebookKernelInfo | undefined = undefined;
|
||||
private readonly _onDidChangeKernel = new Emitter<void>();
|
||||
private readonly _onDidChangeKernel = this._register(new Emitter<void>());
|
||||
readonly onDidChangeKernel: Event<void> = this._onDidChangeKernel.event;
|
||||
|
||||
get activeKernel() {
|
||||
@@ -217,7 +217,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
updateEditorFocus() {
|
||||
// Note - focus going to the webview will fire 'blur', but the webview element will be
|
||||
// a descendent of the notebook editor root.
|
||||
this._editorFocus?.set(DOM.isAncestor(document.activeElement, this._overlayContainer));
|
||||
const focused = DOM.isAncestor(document.activeElement, this._overlayContainer);
|
||||
this._editorFocus?.set(focused);
|
||||
this._notebookViewModel?.setFocus(focused);
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
@@ -700,14 +702,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
const focus = this._list.getFocus()[0];
|
||||
if (typeof focus === 'number') {
|
||||
const element = this._notebookViewModel!.viewCells[focus];
|
||||
const itemDOM = this._list?.domElementOfElement(element!);
|
||||
let editorFocused = false;
|
||||
if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) {
|
||||
editorFocused = true;
|
||||
}
|
||||
if (element) {
|
||||
const itemDOM = this._list?.domElementOfElement(element);
|
||||
let editorFocused = !!(document.activeElement && itemDOM && itemDOM.contains(document.activeElement));
|
||||
|
||||
state.editorFocused = editorFocused;
|
||||
state.focus = focus;
|
||||
state.editorFocused = editorFocused;
|
||||
state.focus = focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,7 +907,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
(direction === 'above' ? index : nextIndex) :
|
||||
index;
|
||||
const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, cell?.metadata, true);
|
||||
return newCell;
|
||||
return newCell as CellViewModel;
|
||||
}
|
||||
|
||||
async splitNotebookCell(cell: ICellViewModel): Promise<CellViewModel[] | null> {
|
||||
@@ -946,41 +947,41 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
return true;
|
||||
}
|
||||
|
||||
async moveCellDown(cell: ICellViewModel): Promise<boolean> {
|
||||
async moveCellDown(cell: ICellViewModel): Promise<ICellViewModel | null> {
|
||||
if (!this._notebookViewModel!.metadata.editable) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this._notebookViewModel!.getCellIndex(cell);
|
||||
if (index === this._notebookViewModel!.length - 1) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const newIdx = index + 1;
|
||||
return this._moveCellToIndex(index, newIdx);
|
||||
}
|
||||
|
||||
async moveCellUp(cell: ICellViewModel): Promise<boolean> {
|
||||
async moveCellUp(cell: ICellViewModel): Promise<ICellViewModel | null> {
|
||||
if (!this._notebookViewModel!.metadata.editable) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this._notebookViewModel!.getCellIndex(cell);
|
||||
if (index === 0) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const newIdx = index - 1;
|
||||
return this._moveCellToIndex(index, newIdx);
|
||||
}
|
||||
|
||||
async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<boolean> {
|
||||
async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<ICellViewModel | null> {
|
||||
if (!this._notebookViewModel!.metadata.editable) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cell === relativeToCell) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalIdx = this._notebookViewModel!.getCellIndex(cell);
|
||||
@@ -994,23 +995,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
return this._moveCellToIndex(originalIdx, newIdx);
|
||||
}
|
||||
|
||||
private async _moveCellToIndex(index: number, newIdx: number): Promise<boolean> {
|
||||
private async _moveCellToIndex(index: number, newIdx: number): Promise<ICellViewModel | null> {
|
||||
if (index === newIdx) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._notebookViewModel!.moveCellToIdx(index, newIdx, true)) {
|
||||
throw new Error('Notebook Editor move cell, index out of range');
|
||||
}
|
||||
|
||||
let r: (val: boolean) => void;
|
||||
let r: (val: ICellViewModel | null) => void;
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
if (this._isDisposed) {
|
||||
r(false);
|
||||
r(null);
|
||||
}
|
||||
|
||||
this._list?.revealElementInView(this._notebookViewModel!.viewCells[newIdx]);
|
||||
r(true);
|
||||
const viewCell = this._notebookViewModel!.viewCells[newIdx];
|
||||
this._list?.revealElementInView(viewCell);
|
||||
r(viewCell);
|
||||
});
|
||||
|
||||
return new Promise(resolve => { r = resolve; });
|
||||
@@ -1262,6 +1264,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
|
||||
dispose() {
|
||||
this._isDisposed = true;
|
||||
// dispose webview first
|
||||
this._webview?.dispose();
|
||||
|
||||
this.notebookService.removeNotebookEditor(this);
|
||||
const keys = Object.keys(this._contributions);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
@@ -1271,7 +1276,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
||||
|
||||
this._localStore.clear();
|
||||
this._list?.dispose();
|
||||
this._webview?.dispose();
|
||||
|
||||
this._overlayContainer.remove();
|
||||
this.viewModel?.dispose();
|
||||
|
||||
@@ -92,8 +92,9 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService {
|
||||
|
||||
private _disposeWidget(widget: NotebookEditorWidget): void {
|
||||
widget.onWillHide();
|
||||
widget.getDomNode().remove();
|
||||
const domNode = widget.getDomNode();
|
||||
widget.dispose();
|
||||
domNode.remove();
|
||||
}
|
||||
|
||||
private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void {
|
||||
|
||||
@@ -45,6 +45,12 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
private _hiddenRangeIds: string[] = [];
|
||||
private hiddenRangesPrefixSum: PrefixSumComputer | null = null;
|
||||
|
||||
private _isDisposed = false;
|
||||
|
||||
get isDisposed() {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private listUser: string,
|
||||
container: HTMLElement,
|
||||
@@ -162,6 +168,10 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
attachViewModel(model: NotebookViewModel) {
|
||||
this._viewModel = model;
|
||||
this._viewModelStore.add(model.onDidChangeViewCells((e) => {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRanges = this._hiddenRangeIds.map(id => this._viewModel!.getTrackedRange(id)).filter(range => range !== null) as ICellRange[];
|
||||
const newVisibleViewCells: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], currentRanges);
|
||||
|
||||
@@ -197,7 +207,11 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
deletedOutputs.forEach(output => this._onDidRemoveOutput.fire(output));
|
||||
});
|
||||
} else {
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this._viewModelStore.add(DOM.scheduleAtNextAnimationFrame(() => {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewDiffs.reverse().forEach((diff) => {
|
||||
const hideOutputs: IProcessedOutput[] = [];
|
||||
const deletedOutputs: IProcessedOutput[] = [];
|
||||
@@ -216,7 +230,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
hideOutputs.forEach(output => this._onDidHideOutput.fire(output));
|
||||
deletedOutputs.forEach(output => this._onDidRemoveOutput.fire(output));
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -490,7 +504,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
|
||||
domElementOfElement(element: ICellViewModel): HTMLElement | null {
|
||||
const index = this._getViewIndexUpperBound(element);
|
||||
if (index !== undefined) {
|
||||
if (index !== undefined && index >= 0) {
|
||||
return this.view.domElement(index);
|
||||
}
|
||||
|
||||
@@ -518,7 +532,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
|
||||
updateElementHeight2(element: ICellViewModel, size: number): void {
|
||||
const index = this._getViewIndexUpperBound(element);
|
||||
if (index === undefined) {
|
||||
if (index === undefined || index < 0 || index >= this.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -874,6 +888,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._isDisposed = true;
|
||||
this._viewModelStore.dispose();
|
||||
this._localDisposableStore.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -61,7 +61,12 @@ class RichRenderer implements IOutputTransformContribution {
|
||||
|
||||
let mimeTypesMessage = mimeTypes.join(', ');
|
||||
|
||||
contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`;
|
||||
if (preferredMimeType) {
|
||||
contentNode.innerText = `No renderer could be found for MIME type: ${preferredMimeType}`;
|
||||
} else {
|
||||
contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`;
|
||||
}
|
||||
|
||||
container.appendChild(contentNode);
|
||||
|
||||
return {
|
||||
|
||||
@@ -340,11 +340,19 @@ ${loaderJs}
|
||||
}
|
||||
|
||||
async initialize(content: string) {
|
||||
if (!document.body.contains(this.element)) {
|
||||
throw new Error('Element is already detached from the DOM tree');
|
||||
}
|
||||
|
||||
this.webview = this._createInset(this.webviewService, content);
|
||||
this.webview.mountTo(this.element);
|
||||
this._register(this.webview);
|
||||
|
||||
this._register(this.webview.onDidClickLink(link => {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
@@ -356,6 +364,10 @@ ${loaderJs}
|
||||
}));
|
||||
|
||||
this._register(this.webview.onDidReload(() => {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preloadsCache.clear();
|
||||
for (const [output, inset] of this.insetMapping.entries()) {
|
||||
this.updateRendererPreloads(inset.preloads);
|
||||
@@ -364,6 +376,10 @@ ${loaderJs}
|
||||
}));
|
||||
|
||||
this._register(this.webview.onMessage((data: FromWebviewMessage) => {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.__vscode_notebook_message) {
|
||||
if (data.type === 'dimension') {
|
||||
let height = data.data.height;
|
||||
@@ -739,6 +755,10 @@ ${loaderJs}
|
||||
}
|
||||
|
||||
private _sendMessageToWebview(message: ToWebviewMessage) {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webview.postMessage(message);
|
||||
}
|
||||
|
||||
@@ -748,6 +768,7 @@ ${loaderJs}
|
||||
|
||||
dispose() {
|
||||
this._disposed = true;
|
||||
this.webview.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,14 +90,17 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
options: model.IModelDeltaDecoration;
|
||||
}>();
|
||||
private _lastDecorationId: number = 0;
|
||||
protected _textModel?: model.ITextModel;
|
||||
|
||||
get textModel(): model.ITextModel | undefined {
|
||||
return this._textModel;
|
||||
return this.model.textModel;
|
||||
}
|
||||
|
||||
set textModel(m: model.ITextModel | undefined) {
|
||||
this.model.textModel = m;
|
||||
}
|
||||
|
||||
hasModel(): this is IEditableCellViewModel {
|
||||
return !!this._textModel;
|
||||
return !!this.model.textModel;
|
||||
}
|
||||
|
||||
private _dragging: boolean = false;
|
||||
@@ -127,7 +130,7 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
abstract onDeselect(): void;
|
||||
|
||||
assertTextModelAttached(): boolean {
|
||||
if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) {
|
||||
if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
}
|
||||
|
||||
this._textEditor = editor;
|
||||
this._textModel = this._textEditor.getModel() || undefined;
|
||||
this.textModel = this._textEditor.getModel() || undefined;
|
||||
|
||||
if (this._editorViewStates) {
|
||||
this._restoreViewState(this._editorViewStates);
|
||||
@@ -183,7 +186,7 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
});
|
||||
|
||||
this._textEditor = undefined;
|
||||
this._textModel = undefined;
|
||||
this.textModel = undefined;
|
||||
this._cursorChangeListener?.dispose();
|
||||
this._cursorChangeListener = null;
|
||||
this._onDidChangeEditorAttachState.fire();
|
||||
@@ -315,7 +318,7 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
}
|
||||
|
||||
const firstViewLineTop = this._textEditor.getTopForPosition(1, 1);
|
||||
const lastViewLineTop = this._textEditor.getTopForPosition(this._textModel!.getLineCount(), this._textModel!.getLineLength(this._textModel!.getLineCount()));
|
||||
const lastViewLineTop = this._textEditor.getTopForPosition(this.textModel!.getLineCount(), this.textModel!.getLineLength(this.textModel!.getLineCount()));
|
||||
const selectionTop = this._textEditor.getTopForPosition(selection.startLineNumber, selection.startColumn);
|
||||
|
||||
if (selectionTop === lastViewLineTop) {
|
||||
@@ -343,7 +346,7 @@ export abstract class BaseCellViewModel extends Disposable {
|
||||
let cellMatches: model.FindMatch[] = [];
|
||||
|
||||
if (this.assertTextModelAttached()) {
|
||||
cellMatches = this._textModel!.findMatches(value, false, false, false, null, false);
|
||||
cellMatches = this.textModel!.findMatches(value, false, false, false, null, false);
|
||||
} else {
|
||||
const lineCount = this.textBuffer.getLineCount();
|
||||
const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1);
|
||||
|
||||
@@ -5,181 +5,24 @@
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ICell, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { ITextCellEditingDelegate } from 'vs/workbench/contrib/notebook/common/model/cellEdit';
|
||||
|
||||
/**
|
||||
* It should not modify Undo/Redo stack
|
||||
*/
|
||||
export interface ICellEditingDelegate {
|
||||
insertCell?(index: number, viewCell: BaseCellViewModel): void;
|
||||
deleteCell?(index: number): void;
|
||||
moveCell?(fromIndex: number, toIndex: number): void;
|
||||
createCellViewModel?(cell: ICell): BaseCellViewModel;
|
||||
|
||||
export interface IViewCellEditingDelegate extends ITextCellEditingDelegate {
|
||||
createCellViewModel?(cell: NotebookCellTextModel): BaseCellViewModel;
|
||||
createCell?(index: number, source: string | string[], language: string, type: CellKind): BaseCellViewModel;
|
||||
setSelections(selections: number[]): void;
|
||||
}
|
||||
|
||||
export class InsertCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Insert Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
private cell: BaseCellViewModel,
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private beforedSelections: number[],
|
||||
private endSelections: number[]
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex);
|
||||
this.editingDelegate.setSelections(this.beforedSelections);
|
||||
}
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.insertCell(this.insertIndex, this.cell);
|
||||
this.editingDelegate.setSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
|
||||
private _rawCell: ICell;
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
cell: BaseCellViewModel,
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private beforedSelections: number[],
|
||||
private endSelections: number[]
|
||||
) {
|
||||
this._rawCell = cell.model;
|
||||
|
||||
// save inmem text to `ICell`
|
||||
// no needed any more as the text buffer is transfered to `raw_cell`
|
||||
// this._rawCell.source = [cell.getText()];
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell || !this.editingDelegate.createCellViewModel) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
const cell = this.editingDelegate.createCellViewModel(this._rawCell);
|
||||
this.editingDelegate.insertCell(this.insertIndex, cell);
|
||||
this.editingDelegate.setSelections(this.beforedSelections);
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex);
|
||||
this.editingDelegate.setSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
|
||||
export class MoveCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private fromIndex: number,
|
||||
private toIndex: number,
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private beforedSelections: number[],
|
||||
private endSelections: number[]
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.toIndex, this.fromIndex);
|
||||
this.editingDelegate.setSelections(this.beforedSelections);
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.fromIndex, this.toIndex);
|
||||
this.editingDelegate.setSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
|
||||
export class SpliceCellsEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Insert Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private diffs: [number, CellViewModel[], CellViewModel[]][],
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private beforeHandles: number[],
|
||||
private endHandles: number[]
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.diffs.forEach(diff => {
|
||||
for (let i = 0; i < diff[2].length; i++) {
|
||||
this.editingDelegate.deleteCell!(diff[0]);
|
||||
}
|
||||
|
||||
diff[1].reverse().forEach(cell => {
|
||||
this.editingDelegate.insertCell!(diff[0], cell);
|
||||
});
|
||||
});
|
||||
this.editingDelegate.setSelections(this.beforeHandles);
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.diffs.reverse().forEach(diff => {
|
||||
for (let i = 0; i < diff[1].length; i++) {
|
||||
this.editingDelegate.deleteCell!(diff[0]);
|
||||
}
|
||||
|
||||
diff[2].reverse().forEach(cell => {
|
||||
this.editingDelegate.insertCell!(diff[0], cell);
|
||||
});
|
||||
});
|
||||
|
||||
this.editingDelegate.setSelections(this.endHandles);
|
||||
}
|
||||
}
|
||||
|
||||
export class JoinCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Join Cell';
|
||||
private _deletedRawCell: ICell;
|
||||
private _deletedRawCell: NotebookCellTextModel;
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private index: number,
|
||||
@@ -189,7 +32,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement {
|
||||
private inverseRange: Range,
|
||||
private insertContent: string,
|
||||
private removedCell: BaseCellViewModel,
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private editingDelegate: IViewCellEditingDelegate,
|
||||
) {
|
||||
this._deletedRawCell = this.removedCell.model;
|
||||
}
|
||||
@@ -209,12 +52,12 @@ export class JoinCellEdit implements IResourceUndoRedoElement {
|
||||
|
||||
const cell = this.editingDelegate.createCellViewModel(this._deletedRawCell);
|
||||
if (this.direction === 'above') {
|
||||
this.editingDelegate.insertCell(this.index, cell);
|
||||
this.editingDelegate.setSelections([cell.handle]);
|
||||
this.editingDelegate.insertCell(this.index, this._deletedRawCell);
|
||||
this.editingDelegate.emitSelections([cell.handle]);
|
||||
cell.focusMode = CellFocusMode.Editor;
|
||||
} else {
|
||||
this.editingDelegate.insertCell(this.index, cell);
|
||||
this.editingDelegate.setSelections([this.cell.handle]);
|
||||
this.editingDelegate.insertCell(this.index, cell.model);
|
||||
this.editingDelegate.emitSelections([this.cell.handle]);
|
||||
this.cell.focusMode = CellFocusMode.Editor;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +73,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement {
|
||||
]);
|
||||
|
||||
this.editingDelegate.deleteCell(this.index);
|
||||
this.editingDelegate.setSelections([this.cell.handle]);
|
||||
this.editingDelegate.emitSelections([this.cell.handle]);
|
||||
this.cell.focusMode = CellFocusMode.Editor;
|
||||
}
|
||||
}
|
||||
@@ -247,13 +90,13 @@ export class SplitCellEdit implements IResourceUndoRedoElement {
|
||||
private cellContents: string[],
|
||||
private language: string,
|
||||
private cellKind: CellKind,
|
||||
private editingDelegate: ICellEditingDelegate
|
||||
private editingDelegate: IViewCellEditingDelegate
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
async undo(): Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell || !this.editingDelegate.createCellViewModel) {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
@@ -270,12 +113,12 @@ export class SplitCellEdit implements IResourceUndoRedoElement {
|
||||
this.editingDelegate.deleteCell(this.index + 1);
|
||||
}
|
||||
|
||||
this.editingDelegate.setSelections([this.cell.handle]);
|
||||
this.editingDelegate.emitSelections([this.cell.handle]);
|
||||
this.cell.focusMode = CellFocusMode.Editor;
|
||||
}
|
||||
|
||||
async redo(): Promise<void> {
|
||||
if (!this.editingDelegate.insertCell || !this.editingDelegate.createCell) {
|
||||
if (!this.editingDelegate.createCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
@@ -291,7 +134,7 @@ export class SplitCellEdit implements IResourceUndoRedoElement {
|
||||
}
|
||||
|
||||
if (lastCell) {
|
||||
this.editingDelegate.setSelections([lastCell.handle]);
|
||||
this.editingDelegate.emitSelections([lastCell.handle]);
|
||||
lastCell.focusMode = CellFocusMode.Editor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,17 +173,17 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
|
||||
* Text model is used for editing.
|
||||
*/
|
||||
async resolveTextModel(): Promise<model.ITextModel> {
|
||||
if (!this._textModel) {
|
||||
if (!this.textModel) {
|
||||
const ref = await this._modelService.createModelReference(this.model.uri);
|
||||
this._textModel = ref.object.textEditorModel;
|
||||
this.textModel = ref.object.textEditorModel;
|
||||
this._register(ref);
|
||||
this._register(this._textModel.onDidChangeContent(() => {
|
||||
this._register(this.textModel.onDidChangeContent(() => {
|
||||
this.editState = CellEditState.Editing;
|
||||
this._onDidChangeState.fire({ contentChanged: true });
|
||||
}));
|
||||
}
|
||||
|
||||
return this._textModel;
|
||||
return this.textModel;
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
|
||||
@@ -147,16 +147,16 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
|
||||
}
|
||||
|
||||
async resolveTextModel(): Promise<model.ITextModel> {
|
||||
if (!this._textModel) {
|
||||
if (!this.textModel) {
|
||||
const ref = await this._modelService.createModelReference(this.model.uri);
|
||||
this._textModel = ref.object.textEditorModel;
|
||||
this.textModel = ref.object.textEditorModel;
|
||||
this._register(ref);
|
||||
this._register(this._textModel.onDidChangeContent(() => {
|
||||
this._register(this.textModel.onDidChangeContent(() => {
|
||||
this._html = null;
|
||||
this._onDidChangeState.fire({ contentChanged: true });
|
||||
}));
|
||||
}
|
||||
return this._textModel;
|
||||
return this.textModel;
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
|
||||
@@ -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, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { DeleteCellEdit, InsertCellEdit, MoveCellEdit, SpliceCellsEdit, JoinCellEdit, SplitCellEdit } 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';
|
||||
import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
|
||||
@@ -31,6 +30,8 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no
|
||||
import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer';
|
||||
import { dirname } from 'vs/base/common/resources';
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { SplitCellEdit, JoinCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit';
|
||||
import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel';
|
||||
import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
|
||||
|
||||
export interface INotebookEditorViewState {
|
||||
@@ -199,7 +200,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
return this._notebook.metadata;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeViewCells = new Emitter<INotebookViewCellsUpdateEvent>();
|
||||
private readonly _onDidChangeViewCells = this._register(new Emitter<INotebookViewCellsUpdateEvent>());
|
||||
get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; }
|
||||
|
||||
private _lastNotebookEditResource: URI[] = [];
|
||||
@@ -215,7 +216,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
return this._layoutInfo;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeSelection = new Emitter<void>();
|
||||
private readonly _onDidChangeSelection = this._register(new Emitter<void>());
|
||||
get onDidChangeSelection(): Event<void> { return this._onDidChangeSelection.event; }
|
||||
|
||||
private _selections: number[] = [];
|
||||
@@ -242,6 +243,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
public readonly id: string;
|
||||
private _foldingRanges: FoldingRegions | null = null;
|
||||
private _hiddenRanges: ICellRange[] = [];
|
||||
private _focused: boolean = true;
|
||||
|
||||
get focused() {
|
||||
return this._focused;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public viewType: string,
|
||||
@@ -259,23 +265,20 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
this._instanceId = strings.singleLetterHash(MODEL_ID);
|
||||
|
||||
this._register(this._notebook.onDidChangeCells(e => {
|
||||
const diffs = e.map(splice => {
|
||||
const diffs = e.splices.map(splice => {
|
||||
return [splice[0], splice[1], splice[2].map(cell => {
|
||||
return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel);
|
||||
})] as [number, number, CellViewModel[]];
|
||||
});
|
||||
|
||||
const undoDiff = diffs.map(diff => {
|
||||
const deletedCells = this.viewCells.slice(diff[0], diff[0] + diff[1]);
|
||||
|
||||
return [diff[0], deletedCells, diff[2]] as [number, CellViewModel[], CellViewModel[]];
|
||||
});
|
||||
|
||||
diffs.reverse().forEach(diff => {
|
||||
const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]);
|
||||
|
||||
this._decorationsTree.acceptReplace(diff[0], diff[1], diff[2].length, true);
|
||||
deletedCells.forEach(cell => {
|
||||
this._handleToViewCellMapping.delete(cell.handle);
|
||||
// dispsoe the cell to release ref to the cell text document
|
||||
cell.dispose();
|
||||
});
|
||||
|
||||
diff[2].forEach(cell => {
|
||||
@@ -285,7 +288,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
});
|
||||
|
||||
this._onDidChangeViewCells.fire({
|
||||
synchronous: true,
|
||||
synchronous: e.synchronous,
|
||||
splices: diffs
|
||||
});
|
||||
|
||||
@@ -315,12 +318,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
}
|
||||
}
|
||||
|
||||
this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
}, this.selectionHandles, endSelectionHandles));
|
||||
|
||||
this.selectionHandles = endSelectionHandles;
|
||||
}));
|
||||
|
||||
@@ -328,6 +325,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
this.eventDispatcher.emit([new NotebookMetadataChangedEvent(e)]);
|
||||
}));
|
||||
|
||||
this._register(this._notebook.emitSelections(selections => {
|
||||
// text model emit selection change (for example, undo/redo)
|
||||
// we should update the selection handle wisely
|
||||
// TODO, if the editor is note selected, undo/redo should not change the focused element selection
|
||||
this.updateSelectionsFromEdits(selections);
|
||||
}));
|
||||
|
||||
this._register(this.eventDispatcher.onDidChangeLayout((e) => {
|
||||
this._layoutInfo = e.value;
|
||||
|
||||
@@ -353,6 +357,16 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
});
|
||||
}
|
||||
|
||||
setFocus(focused: boolean) {
|
||||
this._focused = focused;
|
||||
}
|
||||
|
||||
updateSelectionsFromEdits(selections: number[]) {
|
||||
if (this._focused) {
|
||||
this.selectionHandles = selections;
|
||||
}
|
||||
}
|
||||
|
||||
getFoldingStartIndex(index: number): number {
|
||||
if (!this._foldingRanges) {
|
||||
return -1;
|
||||
@@ -582,87 +596,20 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
return result;
|
||||
}
|
||||
|
||||
private _createCellDelegate(index: number, source: string | string[], language: string, type: CellKind) {
|
||||
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._notebook.insertNewCell(index, [cell]);
|
||||
this._localStore.add(newCell);
|
||||
this._decorationsTree.acceptReplace(index, 0, 1, true);
|
||||
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[index, 0, [newCell]]] });
|
||||
return newCell;
|
||||
}
|
||||
|
||||
private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) {
|
||||
this._viewCells!.splice(insertIndex, 0, insertCell);
|
||||
this._handleToViewCellMapping.set(insertCell.handle, insertCell);
|
||||
this._notebook.insertNewCell(insertIndex, [insertCell.model as NotebookCellTextModel]);
|
||||
this._localStore.add(insertCell);
|
||||
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] });
|
||||
}
|
||||
|
||||
private _deleteCellDelegate(deleteIndex: number) {
|
||||
const deleteCell = this._viewCells[deleteIndex];
|
||||
this._viewCells.splice(deleteIndex, 1);
|
||||
this._handleToViewCellMapping.delete(deleteCell.handle);
|
||||
|
||||
this._notebook.removeCell(deleteIndex, 1);
|
||||
this._decorationsTree.acceptReplace(deleteIndex, 1, 0, true);
|
||||
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] });
|
||||
}
|
||||
|
||||
private _setSelectionsDelegate(selections: number[]) {
|
||||
this.selectionHandles = selections;
|
||||
}
|
||||
|
||||
createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) {
|
||||
const cell = this._notebook.createCellTextModel(source, language, type, [], metadata);
|
||||
let newCell: CellViewModel = createCellViewModel(this._instantiationService, this, cell);
|
||||
this._viewCells!.splice(index, 0, newCell);
|
||||
this._handleToViewCellMapping.set(newCell.handle, newCell);
|
||||
this._notebook.insertNewCell(index, [cell]);
|
||||
this._localStore.add(newCell);
|
||||
|
||||
if (pushUndoStop) {
|
||||
this._undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
}, this.selectionHandles, this.selectionHandles));
|
||||
}
|
||||
|
||||
this._decorationsTree.acceptReplace(index, 0, 1, true);
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] });
|
||||
return newCell;
|
||||
this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined);
|
||||
// TODO, rely on createCell to be sync
|
||||
return this.viewCells[index];
|
||||
}
|
||||
|
||||
insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean): CellViewModel {
|
||||
let newCell: CellViewModel = createCellViewModel(this._instantiationService, this, cell);
|
||||
this._viewCells!.splice(index, 0, newCell);
|
||||
this._handleToViewCellMapping.set(newCell.handle, newCell);
|
||||
|
||||
this._notebook.insertNewCell(index, [newCell.model]);
|
||||
this._localStore.add(newCell);
|
||||
this._undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
}, this.selectionHandles, this.selectionHandles));
|
||||
|
||||
this._decorationsTree.acceptReplace(index, 0, 1, true);
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] });
|
||||
return newCell;
|
||||
insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean = true): CellViewModel {
|
||||
this._notebook.insertCell2(index, cell, synchronous, pushUndoStop);
|
||||
// TODO, rely on createCell to be sync // this will trigger it to synchronous update
|
||||
return this._viewCells[index];
|
||||
}
|
||||
|
||||
deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean = true) {
|
||||
const primarySelectionIndex = this.selectionHandles.length ? this._viewCells.indexOf(this.getCellByHandle(this.selectionHandles[0])!) : null;
|
||||
|
||||
let viewCell = this._viewCells[index];
|
||||
this._viewCells.splice(index, 1);
|
||||
this._handleToViewCellMapping.delete(viewCell.handle);
|
||||
this._notebook.removeCell(index, 1);
|
||||
|
||||
let endSelections: number[] = [];
|
||||
if (this.selectionHandles.length) {
|
||||
const primarySelectionHandle = this.selectionHandles[0];
|
||||
@@ -680,23 +627,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
}
|
||||
}
|
||||
|
||||
if (pushUndoStop) {
|
||||
this._undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
createCellViewModel: (cell: NotebookCellTextModel) => {
|
||||
return createCellViewModel(this._instantiationService, this, cell);
|
||||
},
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
}, this.selectionHandles, endSelections));
|
||||
}
|
||||
|
||||
this.selectionHandles = endSelections;
|
||||
|
||||
this._decorationsTree.acceptReplace(index, 1, 0, true);
|
||||
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
viewCell.dispose();
|
||||
this._notebook.deleteCell2(index, synchronous, pushUndoStop, this.selectionHandles, endSelections);
|
||||
}
|
||||
|
||||
moveCellToIdx(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean {
|
||||
@@ -705,24 +636,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
return false;
|
||||
}
|
||||
|
||||
this.viewCells.splice(index, 1);
|
||||
this.viewCells!.splice(newIdx, 0, viewCell);
|
||||
this._notebook.moveCellToIdx(index, newIdx);
|
||||
|
||||
if (pushedToUndoStack) {
|
||||
this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, {
|
||||
moveCell: (fromIndex: number, toIndex: number) => {
|
||||
this.moveCellToIdx(fromIndex, toIndex, true, false);
|
||||
},
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
}, this.selectionHandles, this.selectionHandles));
|
||||
}
|
||||
|
||||
this.selectionHandles = this.selectionHandles;
|
||||
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [viewCell]]] });
|
||||
|
||||
this._notebook.moveCellToIdx2(index, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -806,23 +720,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
|
||||
let newLinesContents = this._computeCellLinesContents(cell, splitPoints);
|
||||
if (newLinesContents) {
|
||||
|
||||
const editorSelections = cell.getSelections();
|
||||
// update the contents of the first cell
|
||||
cell.textModel.applyEdits([
|
||||
{ range: cell.textModel.getFullModelRange(), text: newLinesContents[0] }
|
||||
], false);
|
||||
|
||||
// create new cells based on the new text models
|
||||
const language = cell.model.language;
|
||||
this._notebook.splitNotebookCell(index, newLinesContents, this.selectionHandles);
|
||||
const language = cell.language;
|
||||
const kind = cell.cellKind;
|
||||
let insertIndex = this.getCellIndex(cell) + 1;
|
||||
const newCells = [];
|
||||
for (let j = 1; j < newLinesContents.length; j++, insertIndex++) {
|
||||
newCells.push(this.createCell(insertIndex, newLinesContents[j], language, kind, undefined, true, false));
|
||||
}
|
||||
|
||||
this.selectionHandles = [cell.handle];
|
||||
|
||||
this._undoService.pushElement(new SplitCellEdit(
|
||||
this.uri,
|
||||
@@ -833,16 +734,17 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
language,
|
||||
kind,
|
||||
{
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
createCellViewModel: (cell: NotebookCellTextModel) => {
|
||||
return createCellViewModel(this._instantiationService, this, cell);
|
||||
createCell: (index: number, source: string | string[], language: string, type: CellKind) => {
|
||||
return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel;
|
||||
},
|
||||
createCell: this._createCellDelegate.bind(this),
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
deleteCell: (index: number) => {
|
||||
this.deleteCell(index, true, false);
|
||||
},
|
||||
emitSelections: (selections: number[]) => {
|
||||
this.updateSelectionsFromEdits(selections);
|
||||
}
|
||||
}
|
||||
));
|
||||
return newCells;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,12 +810,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
insertContent,
|
||||
cell,
|
||||
{
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
insertCell: (index: number, cell: NotebookCellTextModel) => {
|
||||
this.insertCell(index, cell, true, false);
|
||||
},
|
||||
deleteCell: (index: number) => {
|
||||
this.deleteCell(index, true, false);
|
||||
},
|
||||
createCellViewModel: (cell: NotebookCellTextModel) => {
|
||||
return createCellViewModel(this._instantiationService, this, cell);
|
||||
},
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
emitSelections: (selections: number[]) => {
|
||||
this.updateSelectionsFromEdits(selections);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -956,12 +864,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
||||
insertContent,
|
||||
below,
|
||||
{
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
insertCell: (index: number, cell: NotebookCellTextModel) => {
|
||||
this.insertCell(index, cell, true, false);
|
||||
},
|
||||
deleteCell: (index: number) => {
|
||||
this.deleteCell(index, true, false);
|
||||
},
|
||||
createCellViewModel: (cell: NotebookCellTextModel) => {
|
||||
return createCellViewModel(this._instantiationService, this, cell);
|
||||
},
|
||||
setSelections: this._setSelectionsDelegate.bind(this)
|
||||
emitSelections: (selections: number[]) => {
|
||||
this.updateSelectionsFromEdits(selections);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
|
||||
/**
|
||||
* It should not modify Undo/Redo stack
|
||||
*/
|
||||
export interface ITextCellEditingDelegate {
|
||||
insertCell?(index: number, cell: NotebookCellTextModel): void;
|
||||
deleteCell?(index: number): void;
|
||||
moveCell?(fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void;
|
||||
emitSelections(selections: number[]): void;
|
||||
}
|
||||
|
||||
|
||||
export class InsertCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Insert Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
private cell: NotebookCellTextModel,
|
||||
private editingDelegate: ITextCellEditingDelegate,
|
||||
private beforedSelections: number[] | undefined,
|
||||
private endSelections: number[] | undefined
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex);
|
||||
if (this.beforedSelections) {
|
||||
this.editingDelegate.emitSelections(this.beforedSelections);
|
||||
}
|
||||
}
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.insertCell(this.insertIndex, this.cell);
|
||||
if (this.endSelections) {
|
||||
this.editingDelegate.emitSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
private _cell: NotebookCellTextModel,
|
||||
private editingDelegate: ITextCellEditingDelegate,
|
||||
private beforedSelections: number[] | undefined,
|
||||
private endSelections: number[] | undefined
|
||||
) {
|
||||
|
||||
// save inmem text to `ICell`
|
||||
// no needed any more as the text buffer is transfered to `raw_cell`
|
||||
// this._rawCell.source = [cell.getText()];
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.insertCell(this.insertIndex, this._cell);
|
||||
if (this.beforedSelections) {
|
||||
this.editingDelegate.emitSelections(this.beforedSelections);
|
||||
}
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex);
|
||||
if (this.endSelections) {
|
||||
this.editingDelegate.emitSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MoveCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private fromIndex: number,
|
||||
private toIndex: number,
|
||||
private editingDelegate: ITextCellEditingDelegate,
|
||||
private beforedSelections: number[] | undefined,
|
||||
private endSelections: number[] | undefined
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.toIndex, this.fromIndex, this.endSelections, this.beforedSelections);
|
||||
if (this.beforedSelections) {
|
||||
this.editingDelegate.emitSelections(this.beforedSelections);
|
||||
}
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.fromIndex, this.toIndex, this.beforedSelections, this.endSelections);
|
||||
if (this.endSelections) {
|
||||
this.editingDelegate.emitSelections(this.endSelections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SpliceCellsEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Insert Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private diffs: [number, NotebookCellTextModel[], NotebookCellTextModel[]][],
|
||||
private editingDelegate: ITextCellEditingDelegate,
|
||||
private beforeHandles: number[] | undefined,
|
||||
private endHandles: number[] | undefined
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.diffs.forEach(diff => {
|
||||
for (let i = 0; i < diff[2].length; i++) {
|
||||
this.editingDelegate.deleteCell!(diff[0]);
|
||||
}
|
||||
|
||||
diff[1].reverse().forEach(cell => {
|
||||
this.editingDelegate.insertCell!(diff[0], cell);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.beforeHandles) {
|
||||
this.editingDelegate.emitSelections(this.beforeHandles);
|
||||
}
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.diffs.reverse().forEach(diff => {
|
||||
for (let i = 0; i < diff[1].length; i++) {
|
||||
this.editingDelegate.deleteCell!(diff[0]);
|
||||
}
|
||||
|
||||
diff[2].reverse().forEach(cell => {
|
||||
this.editingDelegate.insertCell!(diff[0], cell);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.endHandles) {
|
||||
this.editingDelegate.emitSelections(this.endHandles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as model from 'vs/editor/common/model';
|
||||
@@ -69,6 +69,16 @@ export class NotebookCellTextModel extends Disposable implements ICell {
|
||||
return this._textBuffer;
|
||||
}
|
||||
|
||||
private _textModel?: model.ITextModel;
|
||||
|
||||
get textModel(): model.ITextModel | undefined {
|
||||
return this._textModel;
|
||||
}
|
||||
|
||||
set textModel(m: model.ITextModel | undefined) {
|
||||
this._textModel = m;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly uri: URI,
|
||||
public handle: number,
|
||||
@@ -109,4 +119,24 @@ export class NotebookCellTextModel extends Disposable implements ICell {
|
||||
|
||||
this._onDidChangeOutputs.fire(splices);
|
||||
}
|
||||
|
||||
getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata): NotebookCellMetadata {
|
||||
const editable = this.metadata?.editable ??
|
||||
documentMetadata.cellEditable;
|
||||
|
||||
const runnable = this.metadata?.runnable ??
|
||||
documentMetadata.cellRunnable;
|
||||
|
||||
const hasExecutionOrder = this.metadata?.hasExecutionOrder ??
|
||||
documentMetadata.cellHasExecutionOrder;
|
||||
|
||||
return {
|
||||
...(this.metadata || {}),
|
||||
...{
|
||||
editable,
|
||||
runnable,
|
||||
hasExecutionOrder
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit';
|
||||
|
||||
function compareRangesUsingEnds(a: [number, number], b: [number, number]): number {
|
||||
if (a[1] === b[1]) {
|
||||
@@ -68,15 +70,17 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
|
||||
|
||||
private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onWillDispose: Event<void> = this._onWillDispose.event;
|
||||
private readonly _onDidChangeCells = new Emitter<NotebookCellTextModelSplice[]>();
|
||||
get onDidChangeCells(): Event<NotebookCellTextModelSplice[]> { return this._onDidChangeCells.event; }
|
||||
private _onDidModelChangeProxy = new Emitter<NotebookCellsChangedEvent>();
|
||||
private readonly _onDidChangeCells = this._register(new Emitter<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>());
|
||||
get onDidChangeCells() { return this._onDidChangeCells.event; }
|
||||
private readonly _emitSelections = this._register(new Emitter<number[]>());
|
||||
get emitSelections() { return this._emitSelections.event; }
|
||||
private _onDidModelChangeProxy = this._register(new Emitter<NotebookCellsChangedEvent>());
|
||||
get onDidModelChangeProxy(): Event<NotebookCellsChangedEvent> { return this._onDidModelChangeProxy.event; }
|
||||
private _onDidSelectionChangeProxy = new Emitter<number[] | null>();
|
||||
private _onDidSelectionChangeProxy = this._register(new Emitter<number[] | null>());
|
||||
get onDidSelectionChange(): Event<number[] | null> { return this._onDidSelectionChangeProxy.event; }
|
||||
private _onDidChangeContent = new Emitter<void>();
|
||||
private _onDidChangeContent = this._register(new Emitter<void>());
|
||||
onDidChangeContent: Event<void> = this._onDidChangeContent.event;
|
||||
private _onDidChangeMetadata = new Emitter<NotebookDocumentMetadata>();
|
||||
private _onDidChangeMetadata = this._register(new Emitter<NotebookDocumentMetadata>());
|
||||
onDidChangeMetadata: Event<NotebookDocumentMetadata> = this._onDidChangeMetadata.event;
|
||||
private _mapping: Map<number, NotebookCellTextModel> = new Map();
|
||||
private _cellListeners: Map<number, IDisposable> = new Map();
|
||||
@@ -110,7 +114,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
|
||||
public handle: number,
|
||||
public viewType: string,
|
||||
public supportBackup: boolean,
|
||||
public uri: URI
|
||||
public uri: URI,
|
||||
private _undoService: IUndoRedoService
|
||||
) {
|
||||
super();
|
||||
this.cells = [];
|
||||
@@ -165,7 +170,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
|
||||
this._increaseVersionId();
|
||||
}
|
||||
|
||||
$applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean = true): boolean {
|
||||
$applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): boolean {
|
||||
if (modelVersionId !== this._versionId) {
|
||||
return false;
|
||||
}
|
||||
@@ -242,7 +247,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
|
||||
});
|
||||
}
|
||||
|
||||
this._onDidChangeCells.fire(diffs);
|
||||
const undoDiff = diffs.map(diff => {
|
||||
const deletedCells = this.cells.slice(diff[0], diff[0] + diff[1]);
|
||||
|
||||
return [diff[0], deletedCells, diff[2]] as [number, NotebookCellTextModel[], NotebookCellTextModel[]];
|
||||
});
|
||||
|
||||
this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
emitSelections: this._emitSelectionsDelegate.bind(this)
|
||||
}, undefined, undefined));
|
||||
|
||||
this._onDidChangeCells.fire({ synchronous: synchronous, splices: diffs });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -446,6 +463,123 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
|
||||
this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.CellsClearOutput, versionId: this._versionId });
|
||||
}
|
||||
|
||||
//#region Notebook Text Model Edit API
|
||||
|
||||
private _insertCellDelegate(insertIndex: number, insertCell: NotebookCellTextModel) {
|
||||
this.insertNewCell(insertIndex, [insertCell]);
|
||||
this._onDidChangeCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] });
|
||||
}
|
||||
|
||||
private _deleteCellDelegate(deleteIndex: number) {
|
||||
this.removeCell(deleteIndex, 1);
|
||||
this._onDidChangeCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] });
|
||||
}
|
||||
|
||||
private _emitSelectionsDelegate(selections: number[]) {
|
||||
this._emitSelections.fire(selections);
|
||||
}
|
||||
|
||||
createCell2(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) {
|
||||
const cell = this.createCellTextModel(source, language, type, [], metadata);
|
||||
|
||||
if (pushUndoStop) {
|
||||
this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
emitSelections: this._emitSelectionsDelegate.bind(this)
|
||||
}, beforeSelections, endSelections));
|
||||
}
|
||||
|
||||
|
||||
this.insertNewCell(index, [cell]);
|
||||
|
||||
this._onDidChangeCells.fire({ synchronous, splices: [[index, 0, [cell]]] });
|
||||
|
||||
if (endSelections) {
|
||||
this._emitSelections.fire(endSelections);
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
insertCell2(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean): void {
|
||||
if (pushUndoStop) {
|
||||
this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
emitSelections: this._emitSelectionsDelegate.bind(this)
|
||||
}, undefined, undefined));
|
||||
}
|
||||
|
||||
this.insertNewCell(index, [cell]);
|
||||
this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 0, [cell]]] });
|
||||
}
|
||||
|
||||
deleteCell2(index: number, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) {
|
||||
const cell = this.cells[index];
|
||||
if (pushUndoStop) {
|
||||
this._undoService.pushElement(new DeleteCellEdit(this.uri, index, cell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this),
|
||||
emitSelections: this._emitSelectionsDelegate.bind(this)
|
||||
}, beforeSelections, endSelections));
|
||||
}
|
||||
|
||||
this.removeCell(index, 1);
|
||||
this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
if (endSelections) {
|
||||
this._emitSelections.fire(endSelections);
|
||||
}
|
||||
}
|
||||
|
||||
moveCellToIdx2(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean {
|
||||
const cell = this.cells[index];
|
||||
if (pushedToUndoStack) {
|
||||
this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, {
|
||||
moveCell: (fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => {
|
||||
this.moveCellToIdx2(fromIndex, toIndex, true, false, beforeSelections, endSelections);
|
||||
},
|
||||
emitSelections: this._emitSelectionsDelegate.bind(this)
|
||||
}, beforeSelections, endSelections));
|
||||
}
|
||||
|
||||
this.moveCellToIdx(index, newIdx);
|
||||
// todo, we can't emit this change as it will create a new view model and that will hold
|
||||
// a new reference to the document, thus
|
||||
this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [cell]]] });
|
||||
if (endSelections) {
|
||||
this._emitSelections.fire(endSelections);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async splitNotebookCell(index: number, newLinesContents: string[], endSelections: number[]) {
|
||||
const cell = this.cells[index];
|
||||
|
||||
if (!cell.textModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
cell.textModel.applyEdits([
|
||||
{ range: cell.textModel.getFullModelRange(), text: newLinesContents[0] }
|
||||
], false);
|
||||
|
||||
// create new cells based on the new text models
|
||||
const language = cell.language;
|
||||
const kind = cell.cellKind;
|
||||
let insertIndex = index + 1;
|
||||
const newCells = [];
|
||||
for (let j = 1; j < newLinesContents.length; j++, insertIndex++) {
|
||||
newCells.push(this.createCell2(insertIndex, newLinesContents[j], language, kind, undefined, true, false, undefined, undefined));
|
||||
}
|
||||
|
||||
if (endSelections) {
|
||||
this._emitSelections.fire(endSelections);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
dispose() {
|
||||
this._onWillDispose.fire();
|
||||
this._cellListeners.forEach(val => val.dispose());
|
||||
|
||||
@@ -269,7 +269,7 @@ export interface INotebookTextModel {
|
||||
languages: string[];
|
||||
cells: ICell[];
|
||||
renderers: Set<string>;
|
||||
onDidChangeCells?: Event<NotebookCellTextModelSplice[]>;
|
||||
onDidChangeCells?: Event<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>;
|
||||
onDidChangeContent: Event<void>;
|
||||
onWillDispose(listener: () => void): IDisposable;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ suite('NotebookTextModel', () => {
|
||||
textModel.$applyEdit(textModel.versionId, [
|
||||
{ editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] },
|
||||
{ editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [])] },
|
||||
]);
|
||||
], true, true);
|
||||
|
||||
assert.equal(textModel.cells.length, 6);
|
||||
|
||||
@@ -56,7 +56,7 @@ suite('NotebookTextModel', () => {
|
||||
textModel.$applyEdit(textModel.versionId, [
|
||||
{ editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] },
|
||||
{ editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [])] },
|
||||
]);
|
||||
], true, true);
|
||||
|
||||
assert.equal(textModel.cells.length, 6);
|
||||
|
||||
@@ -81,7 +81,7 @@ suite('NotebookTextModel', () => {
|
||||
textModel.$applyEdit(textModel.versionId, [
|
||||
{ editType: CellEditType.Delete, index: 1, count: 1 },
|
||||
{ editType: CellEditType.Delete, index: 3, count: 1 },
|
||||
]);
|
||||
], true, true);
|
||||
|
||||
assert.equal(textModel.cells[0].getValue(), 'var a = 1;');
|
||||
assert.equal(textModel.cells[1].getValue(), 'var c = 3;');
|
||||
@@ -104,7 +104,7 @@ suite('NotebookTextModel', () => {
|
||||
textModel.$applyEdit(textModel.versionId, [
|
||||
{ editType: CellEditType.Delete, index: 1, count: 1 },
|
||||
{ editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] },
|
||||
]);
|
||||
], true, true);
|
||||
|
||||
assert.equal(textModel.cells.length, 4);
|
||||
|
||||
@@ -129,7 +129,7 @@ suite('NotebookTextModel', () => {
|
||||
textModel.$applyEdit(textModel.versionId, [
|
||||
{ editType: CellEditType.Delete, index: 1, count: 1 },
|
||||
{ editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] },
|
||||
]);
|
||||
], true, true);
|
||||
|
||||
assert.equal(textModel.cells.length, 4);
|
||||
assert.equal(textModel.cells[0].getValue(), 'var a = 1;');
|
||||
|
||||
@@ -23,7 +23,7 @@ suite('NotebookViewModel', () => {
|
||||
instantiationService.spy(IUndoRedoService, 'pushElement');
|
||||
|
||||
test('ctor', function () {
|
||||
const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test'));
|
||||
const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test'), undoRedoService);
|
||||
const model = new NotebookEditorTestModel(notebook);
|
||||
const eventDispatcher = new NotebookEventDispatcher();
|
||||
const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService);
|
||||
|
||||
@@ -137,15 +137,15 @@ export class TestNotebookEditor implements INotebookEditor {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
moveCellDown(cell: CellViewModel): Promise<boolean> {
|
||||
moveCellDown(cell: CellViewModel): Promise<ICellViewModel | null> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
moveCellUp(cell: CellViewModel): Promise<boolean> {
|
||||
moveCellUp(cell: CellViewModel): Promise<ICellViewModel | null> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<boolean> {
|
||||
moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise<ICellViewModel | null> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi
|
||||
export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) {
|
||||
const viewType = 'notebook';
|
||||
const editor = new TestNotebookEditor();
|
||||
const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'));
|
||||
const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService);
|
||||
notebook.cells = cells.map((cell, index) => {
|
||||
return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user