Merge branch 'notebook/dev' into main

This commit is contained in:
rebornix 2021-03-29 14:39:40 -07:00
commit 06a451d815
No known key found for this signature in database
GPG key ID: 181FC90D15393C20
28 changed files with 982 additions and 586 deletions

View file

@ -16,9 +16,6 @@ suite('Notebook Document', function () {
new vscode.NotebookDocumentMetadata()
);
}
async resolveNotebook(_document: vscode.NotebookDocument, _webview: vscode.NotebookCommunication) {
//
}
async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) {
//
}

View file

@ -17,9 +17,6 @@ suite('Notebook Editor', function () {
);
}
async resolveNotebook(_document: vscode.NotebookDocument, _webview: vscode.NotebookCommunication) {
//
}
async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) {
//
}

View file

@ -233,9 +233,6 @@ suite('Notebook API tests', function () {
};
return dto;
},
resolveNotebook: async (_document: vscode.NotebookDocument) => {
return;
},
saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => {
return;
},

View file

@ -44,9 +44,6 @@ export function activate(context: vscode.ExtensionContext): any {
return dto;
},
resolveNotebook: async (_document: vscode.NotebookDocument) => {
return;
},
saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => {
return;
},

View file

@ -1048,18 +1048,36 @@ declare module 'vscode' {
readonly uri: Uri;
readonly version: number;
/** @deprecated Use `uri` instead */
// todo@API don't have this...
readonly fileName: string;
readonly isDirty: boolean;
readonly isUntitled: boolean;
readonly cells: ReadonlyArray<NotebookCell>;
/**
* `true` if the notebook has been closed. A closed notebook isn't synchronized anymore
* and won't be re-used when the same resource is opened again.
*/
readonly isClosed: boolean;
readonly metadata: NotebookDocumentMetadata;
// todo@API should we really expose this?
readonly viewType: string;
/** @deprecated Use `getCells(<...>) instead */
readonly cells: ReadonlyArray<NotebookCell>;
/**
* Get the cells of this notebook. A subset can be retrieved by providing
* a range. The range will be adjuset to the notebook.
*
* @param range A notebook range.
* @returns The cells contained by the range or all cells.
*/
getCells(range?: NotebookCellRange): ReadonlyArray<NotebookCell>;
/**
* Save the document. The saving will be handled by the corresponding content provider
*
@ -1070,6 +1088,7 @@ declare module 'vscode' {
save(): Thenable<boolean>;
}
// todo@API RENAME to NotebookRange
// todo@API maybe have a NotebookCellPosition sibling
export class NotebookCellRange {
readonly start: number;
@ -1081,6 +1100,8 @@ declare module 'vscode' {
readonly isEmpty: boolean;
constructor(start: number, end: number);
with(change: { start?: number, end?: number }): NotebookCellRange;
}
export enum NotebookEditorRevealType {
@ -1435,13 +1456,6 @@ declare module 'vscode' {
readonly options?: NotebookDocumentContentOptions;
readonly onDidChangeNotebookContentOptions?: Event<NotebookDocumentContentOptions>;
// todo@API remove! against separation of data provider and renderer
/**
* @deprecated
*/
// eslint-disable-next-line vscode-dts-cancellation
resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Thenable<void>;
/**
* Content providers should always use [file system providers](#FileSystemProvider) to
* resolve the raw content for `uri` as the resouce is not necessarily a file on disk.

View file

@ -19,7 +19,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';

View file

@ -463,12 +463,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
webComm = new ExtHostWebviewCommWrapper(editorId, revivedUri, this._proxy, this._webviewInitData, document);
this._webviewComm.set(editorId, webComm);
}
if (!provider.provider.resolveNotebook) {
return;
}
await provider.provider.resolveNotebook(document.notebookDocument, webComm.contentProviderComm);
}
async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellRange: ICellRange[]): Promise<void> {

View file

@ -177,10 +177,17 @@ export class ExtHostNotebookDocument extends Disposable {
get viewType() { return that._viewType; },
get isDirty() { return that._isDirty; },
get isUntitled() { return that.uri.scheme === Schemas.untitled; },
get cells(): ReadonlyArray<vscode.NotebookCell> { return that._cells.map(cell => cell.cell); },
get isClosed() { return that._disposed; },
get metadata() { return that._metadata; },
set metadata(_value: Required<vscode.NotebookDocumentMetadata>) { throw new Error('Use WorkspaceEdit to update metadata.'); },
save() { return that._save(); }
get cells(): ReadonlyArray<vscode.NotebookCell> { return that._cells.map(cell => cell.cell); },
getCells(range) {
const cells = range ? that._getCells(range) : that._cells;
return cells.map(cell => cell.cell);
},
save() {
return that._save();
}
});
}
return this._notebook;
@ -225,6 +232,25 @@ export class ExtHostNotebookDocument extends Disposable {
}
}
private _validateRange(range: vscode.NotebookCellRange): vscode.NotebookCellRange {
if (range.start < 0) {
range = range.with({ start: 0 });
}
if (range.end > this._cells.length) {
range = range.with({ end: this._cells.length });
}
return range;
}
private _getCells(range: vscode.NotebookCellRange): ExtHostCell[] {
range = this._validateRange(range);
const result: ExtHostCell[] = [];
for (let i = range.start; i < range.end; i++) {
result.push(this._cells[i]);
}
return result;
}
private async _save(): Promise<boolean> {
if (this._disposed) {
return Promise.reject(new Error('Notebook has been closed'));

View file

@ -2922,6 +2922,22 @@ export class NotebookCellRange {
this._start = start;
this._end = end;
}
with(change: { start?: number, end?: number }): NotebookCellRange {
let start = this._start;
let end = this._end;
if (change.start !== undefined) {
start = change.start;
}
if (change.end !== undefined) {
end = change.end;
}
if (start === this._start && end === this._end) {
return this;
}
return new NotebookCellRange(start, end);
}
}
export class NotebookCellMetadata {

View file

@ -9,11 +9,11 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { expandCellRangesWithHiddenCells, getNotebookEditorFromEditorPane, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { expandCellRangesWithHiddenCells, getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { cloneNotebookCellTextModel, NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellEditType, ICellEditOperation, ICellRange, ISelectionState, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import * as platform from 'vs/base/common/platform';
@ -25,190 +25,252 @@ import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkey
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
class NotebookClipboardContribution extends Disposable {
export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewModel | undefined, pasteCells: {
items: NotebookCellTextModel[];
isCopy: boolean;
}): boolean {
const viewModel = editor.viewModel;
if (!viewModel || !viewModel.metadata.editable) {
return false;
}
const originalState: ISelectionState = {
kind: SelectionStateType.Index,
focus: viewModel.getFocus(),
selections: viewModel.getSelections()
};
if (activeCell) {
const currCellIndex = viewModel.getCellIndex(activeCell);
const newFocusIndex = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0;
viewModel.notebookDocument.applyEdits([
{
editType: CellEditType.Replace,
index: newFocusIndex,
count: 0,
cells: pasteCells.items.map(cell => cloneNotebookCellTextModel(cell))
}
], true, originalState, () => ({
kind: SelectionStateType.Index,
focus: { start: newFocusIndex, end: newFocusIndex + 1 },
selections: [{ start: newFocusIndex, end: newFocusIndex + pasteCells.items.length }]
}), undefined);
} else {
if (viewModel.length !== 0) {
return false;
}
viewModel.notebookDocument.applyEdits([
{
editType: CellEditType.Replace,
index: 0,
count: 0,
cells: pasteCells.items.map(cell => cloneNotebookCellTextModel(cell))
}
], true, originalState, () => ({
kind: SelectionStateType.Index,
focus: { start: 0, end: 1 },
selections: [{ start: 1, end: pasteCells.items.length + 1 }]
}), undefined);
}
return true;
}
function cellRangeToViewCells(viewModel: NotebookViewModel, ranges: ICellRange[]) {
const cells: ICellViewModel[] = [];
ranges.forEach(range => {
cells.push(...viewModel.viewCells.slice(range.start, range.end));
});
return cells;
}
export function runCopyCells(accessor: ServicesAccessor, editor: INotebookEditor, targetCell: ICellViewModel | undefined): boolean {
if (!editor.hasModel()) {
return false;
}
if (editor.hasOutputTextSelection()) {
document.execCommand('copy');
return true;
}
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
const viewModel = editor.viewModel;
const selections = viewModel.getSelections();
if (targetCell) {
const targetCellIndex = viewModel.getCellIndex(targetCell);
const containingSelection = selections.find(selection => selection.start <= targetCellIndex && targetCellIndex < selection.end);
if (!containingSelection) {
clipboardService.writeText(targetCell.getText());
notebookService.setToCopy([targetCell.model], true);
return true;
}
}
const selectionRanges = expandCellRangesWithHiddenCells(editor, editor.viewModel, editor.viewModel.getSelections());
const selectedCells = cellRangeToViewCells(editor.viewModel, selectionRanges);
if (!selectedCells.length) {
return false;
}
clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n'));
notebookService.setToCopy(selectedCells.map(cell => cell.model), true);
return true;
}
export function runCutCells(accessor: ServicesAccessor, editor: INotebookEditor, targetCell: ICellViewModel | undefined): boolean {
const viewModel = editor.viewModel;
if (!viewModel || !viewModel.metadata.editable) {
return false;
}
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
const selections = viewModel.getSelections();
if (targetCell) {
// from ui
const targetCellIndex = viewModel.getCellIndex(targetCell);
const containingSelection = selections.find(selection => selection.start <= targetCellIndex && targetCellIndex < selection.end);
if (!containingSelection) {
clipboardService.writeText(targetCell.getText());
// delete cell
const focus = viewModel.getFocus();
const newFocus = focus.end <= targetCellIndex ? focus : { start: focus.start - 1, end: focus.end - 1 };
const newSelections = selections.map(selection => (selection.end <= targetCellIndex ? selection : { start: selection.start - 1, end: selection.end - 1 }));
viewModel.notebookDocument.applyEdits([
{ editType: CellEditType.Replace, index: targetCellIndex, count: 1, cells: [] }
], true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: selections }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: newSelections }), undefined, true);
notebookService.setToCopy([targetCell.model], false);
return true;
}
}
const selectionRanges = expandCellRangesWithHiddenCells(editor, viewModel, viewModel.getSelections());
const selectedCells = cellRangeToViewCells(viewModel, selectionRanges);
if (!selectedCells.length) {
return false;
}
clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n'));
const edits: ICellEditOperation[] = selectionRanges.map(range => ({ editType: CellEditType.Replace, index: range.start, count: range.end - range.start, cells: [] }));
const firstSelectIndex = selectionRanges[0].start;
/**
* If we have cells, 0, 1, 2, 3, 4, 5, 6
* and cells 1, 2 are selected, and then we delete cells 1 and 2
* the new focused cell should still be at index 1
*/
const newFocusedCellIndex = firstSelectIndex < viewModel.notebookDocument.cells.length - 1
? firstSelectIndex
: Math.max(viewModel.notebookDocument.cells.length - 2, 0);
viewModel.notebookDocument.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: selectionRanges }, () => {
return {
kind: SelectionStateType.Index,
focus: { start: newFocusedCellIndex, end: newFocusedCellIndex + 1 },
selections: [{ start: newFocusedCellIndex, end: newFocusedCellIndex + 1 }]
};
}, undefined, true);
notebookService.setToCopy(selectedCells.map(cell => cell.model), false);
return true;
}
export class NotebookClipboardContribution extends Disposable {
constructor(@IEditorService private readonly _editorService: IEditorService) {
super();
const getContext = () => {
const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane);
const activeCell = editor?.getActiveCell();
return {
editor,
activeCell
};
};
const PRIORITY = 105;
if (CopyAction) {
this._register(CopyAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const { editor } = getContext();
if (!editor) {
return false;
}
if (!editor.hasModel()) {
return false;
}
if (editor.hasOutputTextSelection()) {
document.execCommand('copy');
return true;
}
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
const selectionRanges = expandCellRangesWithHiddenCells(editor, editor.viewModel, editor.viewModel.getSelections());
const selectedCells = this._cellRangeToViewCells(editor.viewModel, selectionRanges);
if (!selectedCells.length) {
return false;
}
clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n'));
notebookService.setToCopy(selectedCells.map(cell => cell.model), true);
return true;
return this.runCopyAction(accessor);
}));
}
if (PasteAction) {
PasteAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const notebookService = accessor.get<INotebookService>(INotebookService);
const pasteCells = notebookService.getToCopy();
if (!pasteCells) {
return false;
}
const { editor, activeCell } = getContext();
if (!editor) {
return false;
}
const viewModel = editor.viewModel;
if (!viewModel || !viewModel.metadata.editable) {
return false;
}
const originalState: ISelectionState = {
kind: SelectionStateType.Index,
focus: viewModel.getFocus(),
selections: viewModel.getSelections()
};
if (activeCell) {
const currCellIndex = viewModel.getCellIndex(activeCell);
const newFocusIndex = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0;
viewModel.notebookDocument.applyEdits([
{
editType: CellEditType.Replace,
index: newFocusIndex,
count: 0,
cells: pasteCells.items.map(cell => cloneNotebookCellTextModel(cell))
}
], true, originalState, () => ({
kind: SelectionStateType.Index,
focus: { start: newFocusIndex, end: newFocusIndex + 1 },
selections: [{ start: newFocusIndex, end: newFocusIndex + pasteCells.items.length }]
}), undefined);
} else {
if (viewModel.length !== 0) {
return false;
}
viewModel.notebookDocument.applyEdits([
{
editType: CellEditType.Replace,
index: 0,
count: 0,
cells: pasteCells.items.map(cell => cloneNotebookCellTextModel(cell))
}
], true, originalState, () => ({
kind: SelectionStateType.Index,
focus: { start: 0, end: 1 },
selections: [{ start: 1, end: pasteCells.items.length + 1 }]
}), undefined);
}
return true;
return this.runPasteAction(accessor);
});
}
if (CutAction) {
CutAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const { editor } = getContext();
if (!editor) {
return false;
}
const viewModel = editor.viewModel;
if (!viewModel || !viewModel.metadata.editable) {
return false;
}
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
const selectionRanges = expandCellRangesWithHiddenCells(editor, viewModel, viewModel.getSelections());
const selectedCells = this._cellRangeToViewCells(viewModel, selectionRanges);
if (!selectedCells.length) {
return false;
}
clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n'));
const edits: ICellEditOperation[] = selectionRanges.map(range => ({ editType: CellEditType.Replace, index: range.start, count: range.end - range.start, cells: [] }));
const firstSelectIndex = selectionRanges[0].start;
/**
* If we have cells, 0, 1, 2, 3, 4, 5, 6
* and cells 1, 2 are selected, and then we delete cells 1 and 2
* the new focused cell should still be at index 1
*/
const newFocusedCellIndex = firstSelectIndex < viewModel.notebookDocument.cells.length - 1
? firstSelectIndex
: Math.max(viewModel.notebookDocument.cells.length - 2, 0);
viewModel.notebookDocument.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: selectionRanges }, () => {
return {
kind: SelectionStateType.Index,
focus: { start: newFocusedCellIndex, end: newFocusedCellIndex + 1 },
selections: [{ start: newFocusedCellIndex, end: newFocusedCellIndex + 1 }]
};
}, undefined, true);
notebookService.setToCopy(selectedCells.map(cell => cell.model), false);
return true;
return this.runCutAction(accessor);
});
}
}
private _cellRangeToViewCells(viewModel: NotebookViewModel, ranges: ICellRange[]) {
const cells: ICellViewModel[] = [];
ranges.forEach(range => {
cells.push(...viewModel.viewCells.slice(range.start, range.end));
});
private _getContext() {
const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane);
const activeCell = editor?.getActiveCell();
return cells;
return {
editor,
activeCell
};
}
runCopyAction(accessor: ServicesAccessor) {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const { editor } = this._getContext();
if (!editor) {
return false;
}
return runCopyCells(accessor, editor, undefined);
}
runPasteAction(accessor: ServicesAccessor) {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const notebookService = accessor.get<INotebookService>(INotebookService);
const pasteCells = notebookService.getToCopy();
if (!pasteCells) {
return false;
}
const { editor, activeCell } = this._getContext();
if (!editor) {
return false;
}
return runPasteCells(editor, activeCell, pasteCells);
}
runCutAction(accessor: ServicesAccessor) {
const activeElement = <HTMLElement>document.activeElement;
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
return false;
}
const { editor } = this._getContext();
if (!editor) {
return false;
}
return runCutCells(accessor, editor, undefined);
}
}
@ -241,27 +303,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
if (context.notebookEditor.hasOutputTextSelection()) {
document.execCommand('copy');
return;
}
const viewModel = context.notebookEditor.viewModel;
const selections = viewModel.getSelections();
const targetCellIndex = viewModel.getCellIndex(context.cell);
const containingSelection = selections.find(selection => selection.start <= targetCellIndex && targetCellIndex < selection.end);
if (containingSelection) {
const cells = viewModel.viewCells.slice(containingSelection.start, containingSelection.end);
clipboardService.writeText(cells.map(cell => cell.getText()).join('\n'));
notebookService.setToCopy(cells.map(cell => cell.model), true);
} else {
clipboardService.writeText(context.cell.getText());
notebookService.setToCopy([context.cell.model], true);
}
runCopyCells(accessor, context.notebookEditor, context.cell);
}
});
@ -286,49 +328,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const clipboardService = accessor.get<IClipboardService>(IClipboardService);
const notebookService = accessor.get<INotebookService>(INotebookService);
clipboardService.writeText(context.cell.getText());
const viewModel = context.notebookEditor.viewModel;
if (!viewModel || !viewModel.metadata.editable) {
return;
}
const selections = viewModel.getSelections();
const targetCellIndex = viewModel.getCellIndex(context.cell);
const containingSelection = selections.find(selection => selection.start <= targetCellIndex && targetCellIndex < selection.end);
if (containingSelection) {
const cellTextModels = viewModel.viewCells.slice(containingSelection.start, containingSelection.end).map(cell => cell.model);
let finalSelections: ICellRange[] = [];
const delta = containingSelection.end - containingSelection.start;
for (let i = 0; i < selections.length; i++) {
const selection = selections[i];
if (selection.end <= targetCellIndex) {
finalSelections.push(selection);
} else if (selection.start > targetCellIndex) {
finalSelections.push({ start: selection.start - delta, end: selection.end - delta });
} else {
finalSelections.push({ start: containingSelection.start, end: containingSelection.start + 1 });
}
}
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: containingSelection.start, count: containingSelection.end - containingSelection.start, cells: []
}], true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => {
const newFocusCellIdx = containingSelection.start < context.notebookEditor.viewModel.notebookDocument.length ? containingSelection.start : context.notebookEditor.viewModel.notebookDocument.length - 1;
return {
kind: SelectionStateType.Index, focus: { start: newFocusCellIdx, end: newFocusCellIdx + 1 }, selections: finalSelections
};
}, undefined);
notebookService.setToCopy(cellTextModels, true);
} else {
viewModel.deleteCell(viewModel.getCellIndex(context.cell), true);
notebookService.setToCopy([context.cell.model], false);
}
runCutCells(accessor, context.notebookEditor, context.cell);
}
});
@ -367,32 +367,7 @@ registerAction2(class extends NotebookAction {
return;
}
const currCellIndex = context.cell && viewModel.getCellIndex(context.cell);
let topPastedCell: CellViewModel | undefined = undefined;
pasteCells.items.reverse().map(cell => {
return {
source: cell.getValue(),
language: cell.language,
cellKind: cell.cellKind,
outputs: cell.outputs,
metadata: {
editable: cell.metadata?.editable,
breakpointMargin: cell.metadata?.breakpointMargin,
hasExecutionOrder: cell.metadata?.hasExecutionOrder,
inputCollapsed: cell.metadata?.inputCollapsed,
outputCollapsed: cell.metadata?.outputCollapsed,
custom: cell.metadata?.custom
}
};
}).forEach(pasteCell => {
const newIdx = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0;
topPastedCell = viewModel.createCell(newIdx, pasteCell.source, pasteCell.language, pasteCell.cellKind, pasteCell.metadata, pasteCell.outputs, true);
});
if (topPastedCell) {
context.notebookEditor.focusNotebookCell(topPastedCell, 'container');
}
runPasteCells(context.notebookEditor, context.cell, pasteCells);
}
});
@ -427,22 +402,7 @@ registerAction2(class extends NotebookCellAction {
const currCellIndex = viewModel.getCellIndex(context.cell);
let topPastedCell: CellViewModel | undefined = undefined;
pasteCells.items.reverse().map(cell => {
return {
source: cell.getValue(),
language: cell.language,
cellKind: cell.cellKind,
outputs: cell.outputs,
metadata: {
editable: cell.metadata?.editable,
breakpointMargin: cell.metadata?.breakpointMargin,
hasExecutionOrder: cell.metadata?.hasExecutionOrder,
inputCollapsed: cell.metadata?.inputCollapsed,
outputCollapsed: cell.metadata?.outputCollapsed,
custom: cell.metadata?.custom
}
};
}).forEach(pasteCell => {
pasteCells.items.reverse().map(cell => cloneNotebookCellTextModel(cell)).forEach(pasteCell => {
topPastedCell = viewModel.createCell(currCellIndex, pasteCell.source, pasteCell.language, pasteCell.cellKind, pasteCell.metadata, pasteCell.outputs, true);
return;
});

View file

@ -0,0 +1,254 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { mock } from 'vs/base/test/common/mock';
import { NotebookClipboardContribution, runCopyCells, runCutCells } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard';
import { CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IActiveNotebookEditor, INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { IVisibleEditorPane } from 'vs/workbench/common/editor';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
suite('Notebook Clipboard', () => {
const createEditorService = (editor: IActiveNotebookEditor) => {
const visibleEditorPane = new class extends mock<IVisibleEditorPane>() {
getId(): string {
return NOTEBOOK_EDITOR_ID;
}
getControl(): INotebookEditor {
return editor;
}
};
const editorService: IEditorService = new class extends mock<IEditorService>() {
get activeEditorPane(): IVisibleEditorPane | undefined {
return visibleEditorPane;
}
};
return editorService;
};
test('Cut multiple selected cells', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 2', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
accessor.stub(INotebookService, new class extends mock<INotebookService>() { setToCopy() { } });
const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor));
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 2 }, selections: [{ start: 0, end: 2 }] }, 'model');
assert.ok(clipboardContrib.runCutAction(accessor));
assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });
assert.strictEqual(viewModel.length, 1);
assert.strictEqual(viewModel.viewCells[0].getText(), 'paragraph 2');
});
});
test('Cut should take folding info into account', async function () {
await withTestNotebook(
[
['# header a', 'markdown', CellKind.Markdown, [], {}],
['var b = 1;', 'javascript', CellKind.Code, [], {}],
['# header b', 'markdown', CellKind.Markdown, [], {}],
['var b = 2;', 'javascript', CellKind.Code, [], {}],
['var c = 3', 'javascript', CellKind.Markdown, [], {}],
['# header d', 'markdown', CellKind.Markdown, [], {}],
['var e = 4;', 'javascript', CellKind.Code, [], {}],
],
async (editor, accessor) => {
const viewModel = editor.viewModel;
const foldingModel = new FoldingModel();
foldingModel.attachViewModel(viewModel);
updateFoldingStateAtIndex(foldingModel, 0, true);
updateFoldingStateAtIndex(foldingModel, 2, true);
viewModel.updateFoldingRanges(foldingModel.regions);
editor.setHiddenAreas(viewModel.getHiddenRanges());
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }, 'model');
accessor.stub(INotebookService, new class extends mock<INotebookService>() { setToCopy() { } });
const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor));
clipboardContrib.runCutAction(accessor);
assert.strictEqual(viewModel.length, 5);
await viewModel.undo();
assert.strictEqual(viewModel.length, 7);
});
});
test('Copy should take folding info into account', async function () {
await withTestNotebook(
[
['# header a', 'markdown', CellKind.Markdown, [], {}],
['var b = 1;', 'javascript', CellKind.Code, [], {}],
['# header b', 'markdown', CellKind.Markdown, [], {}],
['var b = 2;', 'javascript', CellKind.Code, [], {}],
['var c = 3', 'javascript', CellKind.Markdown, [], {}],
['# header d', 'markdown', CellKind.Markdown, [], {}],
['var e = 4;', 'javascript', CellKind.Code, [], {}],
],
async (editor, accessor) => {
const viewModel = editor.viewModel;
const foldingModel = new FoldingModel();
foldingModel.attachViewModel(viewModel);
updateFoldingStateAtIndex(foldingModel, 0, true);
updateFoldingStateAtIndex(foldingModel, 2, true);
viewModel.updateFoldingRanges(foldingModel.regions);
editor.setHiddenAreas(viewModel.getHiddenRanges());
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }, 'model');
let _cells: NotebookCellTextModel[] = [];
accessor.stub(INotebookService, new class extends mock<INotebookService>() {
setToCopy(cells: NotebookCellTextModel[]) { _cells = cells; }
getToCopy() { return { items: _cells, isCopy: true }; }
});
const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor));
clipboardContrib.runCopyAction(accessor);
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 6, end: 7 }, selections: [{ start: 6, end: 7 }] }, 'model');
clipboardContrib.runPasteAction(accessor);
assert.strictEqual(viewModel.length, 9);
assert.strictEqual(viewModel.viewCells[8].getText(), 'var b = 1;');
});
});
test('#119773, cut last item should not focus on the top first cell', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 2', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
accessor.stub(INotebookService, new class extends mock<INotebookService>() { setToCopy() { } });
const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor));
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 2, end: 3 }, selections: [{ start: 2, end: 3 }] }, 'model');
assert.ok(clipboardContrib.runCutAction(accessor));
// it should be the last cell, other than the first one.
assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });
});
});
test('#119771, undo paste should restore selections', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 2', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
accessor.stub(INotebookService, new class extends mock<INotebookService>() {
setToCopy() { }
getToCopy() {
return {
items: [
editor.viewModel.viewCells[0].model
],
isCopy: true
};
}
});
const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor));
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 2, end: 3 }, selections: [{ start: 2, end: 3 }] }, 'model');
assert.ok(clipboardContrib.runPasteAction(accessor));
assert.strictEqual(viewModel.length, 4);
assert.deepStrictEqual(viewModel.getFocus(), { start: 3, end: 4 });
assert.strictEqual(viewModel.viewCells[3].getText(), '# header 1');
await viewModel.undo();
assert.strictEqual(viewModel.length, 3);
assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 });
});
});
test('copy cell from ui still works if the target cell is not part of a selection', async () => {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 2', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
let _toCopy: NotebookCellTextModel[] = [];
accessor.stub(INotebookService, new class extends mock<INotebookService>() {
setToCopy(toCopy: NotebookCellTextModel[]) { _toCopy = toCopy; }
getToCopy() {
return {
items: _toCopy,
isCopy: true
};
}
});
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 2 }] }, 'model');
assert.ok(runCopyCells(accessor, editor, viewModel.viewCells[0]));
assert.deepStrictEqual(_toCopy, [editor.viewModel.viewCells[0].model, editor.viewModel.viewCells[1].model]);
assert.ok(runCopyCells(accessor, editor, viewModel.viewCells[2]));
assert.deepStrictEqual(_toCopy.length, 1);
assert.deepStrictEqual(_toCopy, [editor.viewModel.viewCells[2].model]);
});
});
test('cut cell from ui still works if the target cell is not part of a selection', async () => {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 1', 'markdown', CellKind.Markdown, [], {}],
['paragraph 2', 'markdown', CellKind.Markdown, [], {}],
['paragraph 3', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
accessor.stub(INotebookService, new class extends mock<INotebookService>() {
setToCopy() { }
getToCopy() {
return { items: [], isCopy: true };
}
});
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 2 }] }, 'model');
assert.ok(runCutCells(accessor, editor, viewModel.viewCells[0]));
assert.strictEqual(viewModel.length, 2);
await viewModel.undo();
assert.strictEqual(viewModel.length, 4);
assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });
assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 2 }]);
assert.ok(runCutCells(accessor, editor, viewModel.viewCells[2]));
assert.strictEqual(viewModel.length, 3);
assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });
assert.strictEqual(viewModel.viewCells[0].getText(), '# header 1');
assert.strictEqual(viewModel.viewCells[1].getText(), 'paragraph 1');
assert.strictEqual(viewModel.viewCells[2].getText(), 'paragraph 3');
await viewModel.undo();
assert.strictEqual(viewModel.length, 4);
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 2, end: 3 }, selections: [{ start: 2, end: 4 }] }, 'model');
assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 });
assert.ok(runCutCells(accessor, editor, viewModel.viewCells[0]));
assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });
assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 3 }]);
});
});
});

View file

@ -27,7 +27,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { EditorsOrder } from 'vs/workbench/common/editor';
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -542,9 +542,10 @@ registerAction2(class extends NotebookAction {
constructor() {
super({
id: EXECUTE_NOTEBOOK_COMMAND_ID,
title: localize('notebookActions.executeNotebook', "Execute Notebook"),
title: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"),
icon: icons.executeAllIcon,
description: {
description: localize('notebookActions.executeNotebook', "Execute Notebook"),
description: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"),
args: [
{
name: 'uri',
@ -553,6 +554,12 @@ registerAction2(class extends NotebookAction {
}
]
},
menu: {
id: MenuId.EditorTitle,
order: -1,
group: 'navigation',
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated())),
}
});
}
@ -589,9 +596,10 @@ registerAction2(class CancelNotebook extends NotebookAction {
constructor() {
super({
id: CANCEL_NOTEBOOK_COMMAND_ID,
title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"),
title: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"),
icon: icons.stopIcon,
description: {
description: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"),
description: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"),
args: [
{
name: 'uri',
@ -600,6 +608,12 @@ registerAction2(class CancelNotebook extends NotebookAction {
}
]
},
menu: {
id: MenuId.EditorTitle,
order: -1,
group: 'navigation',
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL)
}
});
}
@ -626,28 +640,6 @@ MenuRegistry.appendMenuItem(MenuId.EditorContext, {
when: NOTEBOOK_EDITOR_FOCUSED
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: EXECUTE_NOTEBOOK_COMMAND_ID,
title: localize('notebookActions.menu.executeNotebook', "Execute Notebook (Run all cells)"),
icon: icons.executeAllIcon,
},
order: -1,
group: 'navigation',
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()))
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: CANCEL_NOTEBOOK_COMMAND_ID,
title: localize('notebookActions.menu.cancelNotebook', "Stop Notebook Execution"),
icon: icons.stopIcon,
},
order: -1,
group: 'navigation',
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL)
});
registerAction2(class extends NotebookCellAction {
constructor() {
super({

View file

@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { CellEditType, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
suite('Notebook Undo/Redo', () => {
test('Basics', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
const textModelService = accessor.get(ITextModelService);
const viewModel = editor.viewModel;
assert.strictEqual(viewModel.length, 2);
assert.strictEqual(viewModel.getVersionId(), 0);
assert.strictEqual(viewModel.getAlternativeId(), 0);
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 2, cells: []
}], true, undefined, () => undefined, undefined, true);
assert.strictEqual(viewModel.length, 0);
assert.strictEqual(viewModel.getVersionId(), 1);
assert.strictEqual(viewModel.getAlternativeId(), 1);
await viewModel.undo();
assert.strictEqual(viewModel.length, 2);
assert.strictEqual(viewModel.getVersionId(), 2);
assert.strictEqual(viewModel.getAlternativeId(), 0);
await viewModel.redo();
assert.strictEqual(viewModel.length, 0);
assert.strictEqual(viewModel.getVersionId(), 3);
assert.strictEqual(viewModel.getAlternativeId(), 1);
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 0, cells: [
new TestCell(viewModel.viewType, 3, '# header 2', 'markdown', CellKind.Code, [], textModelService),
]
}], true, undefined, () => undefined, undefined, true);
assert.strictEqual(viewModel.getVersionId(), 4);
assert.strictEqual(viewModel.getAlternativeId(), 4);
await viewModel.undo();
assert.strictEqual(viewModel.getVersionId(), 5);
assert.strictEqual(viewModel.getAlternativeId(), 1);
}
);
});
test('Invalid replace count should not throw', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
const textModelService = accessor.get(ITextModelService);
const viewModel = editor.viewModel;
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 2, cells: []
}], true, undefined, () => undefined, undefined, true);
assert.doesNotThrow(() => {
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 2, cells: [
new TestCell(viewModel.viewType, 3, '# header 2', 'markdown', CellKind.Code, [], textModelService),
]
}], true, undefined, () => undefined, undefined, true);
});
}
);
});
test('Replace beyond length', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
],
async (editor) => {
const viewModel = editor.viewModel;
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 1, count: 2, cells: []
}], true, undefined, () => undefined, undefined, true);
assert.deepStrictEqual(viewModel.length, 1);
await viewModel.undo();
assert.deepStrictEqual(viewModel.length, 2);
}
);
});
test('Invalid replace count should not affect undo/redo', async function () {
await withTestNotebook(
[
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
],
async (editor, accessor) => {
const textModelService = accessor.get(ITextModelService);
const viewModel = editor.viewModel;
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 2, cells: []
}], true, undefined, () => undefined, undefined, true);
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 0, count: 2, cells: [
new TestCell(viewModel.viewType, 3, '# header 2', 'markdown', CellKind.Code, [], textModelService),
]
}], true, undefined, () => undefined, undefined, true);
assert.deepStrictEqual(viewModel.length, 1);
await viewModel.undo();
await viewModel.undo();
assert.deepStrictEqual(viewModel.length, 2);
viewModel.notebookDocument.applyEdits([{
editType: CellEditType.Replace, index: 1, count: 2, cells: []
}], true, undefined, () => undefined, undefined, true);
assert.deepStrictEqual(viewModel.length, 1);
}
);
});
});

View file

@ -27,7 +27,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo
import { EditorInput, Extensions as EditorInputExtensions, ICustomEditorInputFactory, IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl';
import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, ExperimentalUseMarkdownRenderer, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
@ -37,7 +37,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { IN_NOTEBOOK_TEXT_DIFF_EDITOR, NotebookEditorOptions, NOTEBOOK_DIFF_EDITOR_ID, NOTEBOOK_EDITOR_ID, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput';
@ -75,6 +75,7 @@ import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions';
// Output renderers registration
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform';
import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl';
/*--------------------------------------------------------------------------------------------- */
@ -731,7 +732,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker
registerSingleton(INotebookService, NotebookService);
registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl);
registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true);
registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServiceImpl, true);
registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true);
registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true);

View file

@ -13,6 +13,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { IReference } from 'vs/base/common/lifecycle';
import { INotebookDiffEditorModel, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { Schemas } from 'vs/base/common/network';
interface NotebookEditorInputOptions {
startDirty?: boolean;
@ -89,7 +90,7 @@ export class NotebookDiffEditorInput extends EditorInput {
}
isUntitled(): boolean {
return this._modifiedTextModel?.object.isUntitled() || false;
return this._modifiedTextModel?.object.resource.scheme === Schemas.untitled;
}
isReadonly() {

View file

@ -18,7 +18,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService';

View file

@ -6,7 +6,7 @@
import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { Event } from 'vs/base/common/event';
import { INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';

View file

@ -8,7 +8,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IEditorGroupsService, IEditorGroup, GroupChangeKind } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { Emitter } from 'vs/base/common/event';

View file

@ -559,6 +559,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return this._notebook.versionId;
}
getAlternativeId() {
return this._notebook.alternativeVersionId;
}
getTrackedRange(id: string): ICellRange | null {
return this._getDecorationRange(id);
}

View file

@ -8,7 +8,6 @@ import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, NotebookRawContentEvent, IOutputDto, ICellOutput, IOutputItemDto, ISelectionState, NullablePartialNotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ITextSnapshot } from 'vs/editor/common/model';
import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement, UndoRedoGroup, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@ -16,44 +15,6 @@ import { ISequence, LcsDiff } from 'vs/base/common/diff/diff';
import { hash } from 'vs/base/common/hash';
import { NotebookCellOutputTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel';
export class NotebookTextModelSnapshot implements ITextSnapshot {
private _index: number = -1;
constructor(private _model: NotebookTextModel) { }
read(): string | null {
if (this._index === -1) {
this._index++;
return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "cells": [`;
}
if (this._index < this._model.cells.length) {
const cell = this._model.cells[this._index];
const data = {
source: cell.getValue(),
metadata: cell.metadata,
cellKind: cell.cellKind,
language: cell.language,
outputs: cell.outputs
};
const rawStr = JSON.stringify(data);
const isLastCell = this._index === this._model.cells.length - 1;
this._index++;
return isLastCell ? rawStr : (rawStr + ',');
} else if (this._index === this._model.cells.length) {
this._index++;
return `]}`;
} else {
return null;
}
}
}
class StackOperation implements IWorkspaceUndoRedoElement {
type: UndoRedoElementType.Workspace;
@ -428,10 +389,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return true;
}
createSnapshot(preserveBOM?: boolean): ITextSnapshot {
return new NotebookTextModelSnapshot(this);
}
private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean, computeUndoRedo: boolean): void {
if (count === 0 && cellDtos.length === 0) {
@ -442,7 +399,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
const oldMap = new Map(this._mapping);
// prepare remove
for (let i = index; i < index + count; i++) {
for (let i = index; i < Math.min(index + count, this._cells.length); i++) {
const cell = this._cells[i];
this._cellListeners.get(cell.handle)?.dispose();
this._cellListeners.delete(cell.handle);

View file

@ -18,7 +18,7 @@ import { IAccessibilityInformation } from 'vs/platform/accessibility/common/acce
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
@ -648,10 +648,9 @@ export interface INotebookEditorModel extends IEditorModel {
readonly notebook: NotebookTextModel | undefined;
isResolved(): this is IResolvedNotebookEditorModel;
isDirty(): boolean;
isUntitled(): boolean;
load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel>;
save(options?: ISaveOptions): Promise<boolean>;
saveAs(target: URI): Promise<boolean>;
saveAs(target: URI): Promise<IEditorInput | undefined>;
revert(options?: IRevertOptions): Promise<void>;
}

View file

@ -14,6 +14,7 @@ import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebo
import { IReference } from 'vs/base/common/lifecycle';
import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ILabelService } from 'vs/platform/label/common/label';
import { Schemas } from 'vs/base/common/network';
interface NotebookEditorInputOptions {
startDirty?: boolean;
@ -47,6 +48,12 @@ export class NotebookEditorInput extends EditorInput {
this._name = labelService.getUriBasenameLabel(resource);
}
dispose() {
this._editorModelReference?.dispose();
this._editorModelReference = null;
super.dispose();
}
getTypeId(): string {
return NotebookEditorInput.ID;
}
@ -63,7 +70,7 @@ export class NotebookEditorInput extends EditorInput {
}
isUntitled(): boolean {
return this._editorModelReference?.object.isUntitled() || false;
return this.resource.scheme === Schemas.untitled;
}
isReadonly() {
@ -122,11 +129,7 @@ ${patterns}
`);
}
if (!await this._editorModelReference.object.saveAs(target)) {
return undefined;
}
return this._move(group, target)?.editor;
return await this._editorModelReference.object.saveAs(target);
}
private async _suggestName(suggestedFilename: string) {
@ -172,6 +175,8 @@ ${patterns}
if (this._editorModelReference.object.isDirty()) {
this._onDidChangeDirty.fire();
}
} else {
this._editorModelReference.object.load();
}
return this._editorModelReference.object;
@ -187,9 +192,5 @@ ${patterns}
return false;
}
dispose() {
this._editorModelReference?.dispose();
this._editorModelReference = null;
super.dispose();
}
}

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { EditorModel, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { EditorModel, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { Emitter, Event } from 'vs/base/common/event';
import { CellEditType, CellKind, ICellEditOperation, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookDataDto, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, ICellEditOperation, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookDataDto, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { IMainNotebookController, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
import { URI } from 'vs/base/common/uri';
@ -23,8 +23,11 @@ import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from
import { assertType } from 'vs/base/common/types';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory, IResolvedFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
import { IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { canceled } from 'vs/base/common/errors';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFileWorkingCopyManager, IFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
//#region --- complex content provider
@ -49,6 +52,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
readonly resource: URI,
readonly viewType: string,
private readonly _contentProvider: IMainNotebookController,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@INotebookService private readonly _notebookService: INotebookService,
@IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService,
@IBackupFileService private readonly _backupFileService: IBackupFileService,
@ -67,7 +71,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
const workingCopyAdapter = new class implements IWorkingCopy {
readonly resource = that._workingCopyResource;
get name() { return that._name; }
readonly capabilities = that.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None;
readonly capabilities = that._isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None;
readonly onDidChangeDirty = that.onDidChangeDirty;
readonly onDidChangeContent = that._onDidChangeContent.event;
isDirty(): boolean { return that.isDirty(); }
@ -102,7 +106,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
return this._dirty;
}
isUntitled(): boolean {
private _isUntitled(): boolean {
return this.resource.scheme === Schemas.untitled;
}
@ -214,16 +218,6 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
conflictingNotebook.dispose();
}
// todo@jrieken@rebornix what about reload?
if (this.resource.scheme === Schemas.untitled && data.data.cells.length === 0) {
data.data.cells.push({
cellKind: CellKind.Code,
language: 'plaintext', //TODO@jrieken unsure what this is
outputs: [],
metadata: undefined,
source: ''
});
}
// this creates and caches a new notebook model so that notebookService.getNotebookTextModel(...)
// will return this one model
@ -343,32 +337,33 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
});
}
async saveAs(targetResource: URI): Promise<boolean> {
async saveAs(targetResource: URI): Promise<IEditorInput | undefined> {
if (!this.isResolved()) {
return false;
return undefined;
}
this._logService.debug(`[notebook editor model] saveAs - enter`, this.resource.toString(true));
const result = await this._assertStat();
if (result === 'none') {
return false;
return undefined;
}
if (result === 'revert') {
await this.revert();
return true;
return undefined;
}
const success = await this._contentProvider.saveAs(this.notebook.uri, targetResource, CancellationToken.None);
this._logService.debug(`[notebook editor model] saveAs - document saved, start updating file stats`, this.resource.toString(true), success);
this._lastResolvedFileStat = await this._resolveStats(this.resource);
if (success) {
this.setDirty(false);
this._onDidSave.fire();
if (!success) {
return undefined;
}
return true;
this.setDirty(false);
this._onDidSave.fire();
return this._instantiationService.createInstance(NotebookEditorInput, targetResource, this.viewType, {});
}
private async _resolveStats(resource: URI) {
@ -393,53 +388,74 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook
export class SimpleNotebookEditorModel extends EditorModel implements INotebookEditorModel {
readonly onDidChangeDirty: Event<void>;
readonly onDidSave: Event<void>;
private readonly _onDidChangeDirty = new Emitter<void>();
private readonly _onDidSave = new Emitter<void>();
readonly resource: URI;
readonly viewType: string;
readonly notebook: NotebookTextModel;
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;
readonly onDidSave: Event<void> = this._onDidSave.event;
private _workingCopy?: IResolvedFileWorkingCopy<NotebookFileWorkingCopyModel>;
private readonly _workingCopyListeners = new DisposableStore();
constructor(
private readonly _workingCopy: IResolvedFileWorkingCopy<NotebookFileWorkingCopyModel>
readonly resource: URI,
readonly viewType: string,
private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel>,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this.resource = _workingCopy.resource;
this.viewType = _workingCopy.model.notebookModel.viewType;
this.notebook = _workingCopy.model.notebookModel;
this.onDidChangeDirty = Event.signal(_workingCopy.onDidChangeDirty);
this.onDidSave = Event.signal(_workingCopy.onDidSave);
}
dispose(): void {
this._workingCopy.dispose();
this._workingCopyListeners.dispose();
this._workingCopy?.dispose();
this._onDidChangeDirty.dispose();
this._onDidSave.dispose();
super.dispose();
}
get notebook(): NotebookTextModel | undefined {
return this._workingCopy?.model.notebookModel;
}
isResolved(): this is IResolvedNotebookEditorModel {
return Boolean(this._workingCopy);
}
isDirty(): boolean {
return this._workingCopy.isDirty();
return this._workingCopy?.isDirty() ?? false;
}
revert(options?: IRevertOptions): Promise<void> {
return this._workingCopy.revert(options);
assertType(this.isResolved());
return this._workingCopy!.revert(options);
}
save(options?: ISaveOptions): Promise<boolean> {
return this._workingCopy.save(options);
}
isUntitled(): boolean {
return this.resource.scheme === Schemas.untitled;
assertType(this.isResolved());
return this._workingCopy!.save(options);
}
async load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel> {
await this._workingCopy.resolve(options);
const workingCopy = await this._workingCopyManager.resolve(this.resource, { reload: { async: !options?.forceReadFromFile } });
if (!this._workingCopy) {
this._workingCopy = <IResolvedFileWorkingCopy<NotebookFileWorkingCopyModel>>workingCopy;
this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), this._workingCopyListeners);
this._workingCopy.onDidSave(() => this._onDidSave.fire(), this._workingCopyListeners);
}
assertType(this.isResolved());
return this;
}
saveAs(target: URI): Promise<boolean> {
throw new Error('Method not implemented.');
async saveAs(target: URI, options?: IFileWorkingCopySaveAsOptions): Promise<IEditorInput | undefined> {
const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target, options);
if (!newWorkingCopy) {
return undefined;
}
assertType(newWorkingCopy.isResolved());
// this is a little hacky because we leave the new working copy alone. BUT
// the newly created editor input will pick it up and claim ownership of it.
return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {});
}
}
@ -514,7 +530,7 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel {
const data = await this._notebookSerializer.dataToNotebook(bytes);
if (token.isCancellationRequested) {
return;
throw canceled();
}
this._notebookModel.metadata = data.metadata;

View file

@ -3,17 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { combinedDisposable, DisposableStore, IDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle';
import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
import { ILogService } from 'vs/platform/log/common/log';
import { Emitter, Event } from 'vs/base/common/event';
import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
import { IResolvedFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IReference } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
export const INotebookEditorModelResolverService = createDecorator<INotebookEditorModelResolverService>('INotebookModelResolverService');
@ -24,131 +18,3 @@ export interface INotebookEditorModelResolverService {
resolve(resource: URI, viewType?: string): Promise<IReference<IResolvedNotebookEditorModel>>;
}
class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> {
private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel>;
private readonly _modelListener = new Map<IResolvedNotebookEditorModel, IDisposable>();
private readonly _onDidSaveNotebook = new Emitter<URI>();
readonly onDidSaveNotebook: Event<URI> = this._onDidSaveNotebook.event;
constructor(
@IInstantiationService readonly _instantiationService: IInstantiationService,
@INotebookService private readonly _notebookService: INotebookService,
@ILogService private readonly _logService: ILogService,
) {
super();
this._workingCopyManager = <any>_instantiationService.createInstance(
FileWorkingCopyManager,
new NotebookFileWorkingCopyModelFactory(_notebookService)
);
}
protected async createReferencedObject(key: string, viewType: string): Promise<IResolvedNotebookEditorModel> {
const uri = URI.parse(key);
const info = await this._notebookService.withNotebookDataProvider(uri, viewType);
let result: IResolvedNotebookEditorModel;
if (info instanceof ComplexNotebookProviderInfo) {
const model = this._instantiationService.createInstance(ComplexNotebookEditorModel, uri, viewType, info.controller);
result = await model.load();
} else if (info instanceof SimpleNotebookProviderInfo) {
const workingCopy = await this._workingCopyManager.resolve(uri);
result = new SimpleNotebookEditorModel(<IResolvedFileWorkingCopy<NotebookFileWorkingCopyModel>>workingCopy);
} else {
throw new Error(`CANNOT open ${key}, no provider found`);
}
this._modelListener.set(result, result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)));
return result;
}
protected destroyReferencedObject(_key: string, object: Promise<IResolvedNotebookEditorModel>): void {
object.then(model => {
this._modelListener.get(model)?.dispose();
this._modelListener.delete(model);
model.dispose();
}).catch(err => {
this._logService.critical('FAILED to destory notebook', err);
});
}
}
export class NotebookModelResolverService implements INotebookEditorModelResolverService {
readonly _serviceBrand: undefined;
private readonly _data: NotebookModelReferenceCollection;
readonly onDidSaveNotebook: Event<URI>;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@INotebookService private readonly _notebookService: INotebookService,
@IExtensionService private readonly _extensionService: IExtensionService,
) {
this._data = instantiationService.createInstance(NotebookModelReferenceCollection);
this.onDidSaveNotebook = this._data.onDidSaveNotebook;
}
async resolve(resource: URI, viewType?: string): Promise<IReference<IResolvedNotebookEditorModel>> {
if (resource.scheme === CellUri.scheme) {
throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${resource.toString()}`);
}
const existingViewType = this._notebookService.getNotebookTextModel(resource)?.viewType;
if (!viewType) {
if (existingViewType) {
viewType = existingViewType;
} else {
await this._extensionService.whenInstalledExtensionsRegistered();
const providers = this._notebookService.getContributedNotebookProviders(resource);
const exclusiveProvider = providers.find(provider => provider.exclusive);
viewType = exclusiveProvider?.id || providers[0]?.id;
}
}
if (!viewType) {
throw new Error(`Missing viewType for '${resource}'`);
}
if (existingViewType && existingViewType !== viewType) {
throw new Error(`A notebook with view type '${existingViewType}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`);
}
const reference = this._data.acquire(resource.toString(), viewType);
const model = await reference.object;
const autoRef = NotebookModelResolverService._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType));
return {
object: model,
dispose() {
reference.dispose();
autoRef.dispose();
}
};
}
private static _autoReferenceDirtyModel(model: IResolvedNotebookEditorModel, ref: () => IDisposable): IDisposable {
const references = new DisposableStore();
const listener = model.onDidChangeDirty(() => {
if (model.isDirty()) {
references.add(ref());
} else {
references.clear();
}
});
const onceListener = Event.once(model.notebook.onWillDispose)(() => {
listener.dispose();
references.dispose();
});
return combinedDisposable(references, listener, onceListener);
}
}

View file

@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { combinedDisposable, DisposableStore, IDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle';
import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
import { ILogService } from 'vs/platform/log/common/log';
import { Emitter, Event } from 'vs/base/common/event';
import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> {
private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel>;
private readonly _modelListener = new Map<IResolvedNotebookEditorModel, IDisposable>();
private readonly _onDidSaveNotebook = new Emitter<URI>();
readonly onDidSaveNotebook: Event<URI> = this._onDidSaveNotebook.event;
constructor(
@IInstantiationService readonly _instantiationService: IInstantiationService,
@INotebookService private readonly _notebookService: INotebookService,
@ILogService private readonly _logService: ILogService,
) {
super();
this._workingCopyManager = <any>_instantiationService.createInstance(
FileWorkingCopyManager,
new NotebookFileWorkingCopyModelFactory(_notebookService)
);
}
protected async createReferencedObject(key: string, viewType: string): Promise<IResolvedNotebookEditorModel> {
const uri = URI.parse(key);
const info = await this._notebookService.withNotebookDataProvider(uri, viewType);
let result: IResolvedNotebookEditorModel;
if (info instanceof ComplexNotebookProviderInfo) {
const model = this._instantiationService.createInstance(ComplexNotebookEditorModel, uri, viewType, info.controller);
result = await model.load();
} else if (info instanceof SimpleNotebookProviderInfo) {
const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, viewType, this._workingCopyManager);
result = await model.load();
} else {
throw new Error(`CANNOT open ${key}, no provider found`);
}
this._modelListener.set(result, result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)));
return result;
}
protected destroyReferencedObject(_key: string, object: Promise<IResolvedNotebookEditorModel>): void {
object.then(model => {
this._modelListener.get(model)?.dispose();
this._modelListener.delete(model);
model.dispose();
}).catch(err => {
this._logService.critical('FAILED to destory notebook', err);
});
}
}
export class NotebookModelResolverServiceImpl implements INotebookEditorModelResolverService {
readonly _serviceBrand: undefined;
private readonly _data: NotebookModelReferenceCollection;
readonly onDidSaveNotebook: Event<URI>;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@INotebookService private readonly _notebookService: INotebookService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IUriIdentityService private readonly _uriIdentService: IUriIdentityService,
) {
this._data = instantiationService.createInstance(NotebookModelReferenceCollection);
this.onDidSaveNotebook = this._data.onDidSaveNotebook;
}
async resolve(resource: URI, viewType?: string): Promise<IReference<IResolvedNotebookEditorModel>> {
if (resource.scheme === CellUri.scheme) {
throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${resource.toString()}`);
}
resource = this._uriIdentService.asCanonicalUri(resource);
const existingViewType = this._notebookService.getNotebookTextModel(resource)?.viewType;
if (!viewType) {
if (existingViewType) {
viewType = existingViewType;
} else {
await this._extensionService.whenInstalledExtensionsRegistered();
const providers = this._notebookService.getContributedNotebookProviders(resource);
const exclusiveProvider = providers.find(provider => provider.exclusive);
viewType = exclusiveProvider?.id || providers[0]?.id;
}
}
if (!viewType) {
throw new Error(`Missing viewType for '${resource}'`);
}
if (existingViewType && existingViewType !== viewType) {
throw new Error(`A notebook with view type '${existingViewType}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`);
}
const reference = this._data.acquire(resource.toString(), viewType);
const model = await reference.object;
const autoRef = NotebookModelResolverServiceImpl._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType));
return {
object: model,
dispose() {
reference.dispose();
autoRef.dispose();
}
};
}
private static _autoReferenceDirtyModel(model: IResolvedNotebookEditorModel, ref: () => IDisposable): IDisposable {
const references = new DisposableStore();
const listener = model.onDidChangeDirty(() => {
if (model.isDirty()) {
references.add(ref());
} else {
references.clear();
}
});
const onceListener = Event.once(model.notebook.onWillDispose)(() => {
listener.dispose();
references.dispose();
});
return combinedDisposable(references, listener, onceListener);
}
}

View file

@ -10,6 +10,7 @@ import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { mock } from 'vs/base/test/common/mock';
import { IFileService } from 'vs/platform/files/common/files';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ILabelService } from 'vs/platform/label/common/label';
import { NullLogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
@ -21,6 +22,7 @@ import { IWorkingCopy, IWorkingCopyService } from 'vs/workbench/services/working
suite('NotebookEditorModel', function () {
const instaService = new InstantiationService();
const notebokService = new class extends mock<INotebookService>() { };
const backupService = new class extends mock<IBackupFileService>() { };
const notificationService = new class extends mock<INotificationService>() { };
@ -49,8 +51,8 @@ suite('NotebookEditorModel', function () {
}
};
new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService);
new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService);
new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService);
new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService);
assert.strictEqual(copies.length, 2);
assert.strictEqual(!isEqual(copies[0].resource, copies[1].resource), true);

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Range } from 'vs/editor/common/core/range';
import { CellKind, CellEditType, NotebookTextModelChangedEvent, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { withTestNotebook, TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
@ -474,4 +475,17 @@ suite('NotebookTextModel', () => {
}
});
});
test('Cell text model update increases notebook model version id #119561', function () {
withTestNotebook([
['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }],
['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }]
], async (editor) => {
const textModel = await editor.viewModel.viewCells[0].resolveTextModel();
assert.ok(textModel !== undefined);
assert.strictEqual(editor.viewModel.getVersionId(), 0);
textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }], true);
assert.strictEqual(editor.viewModel.getVersionId(), 1);
});
});
});

View file

@ -7,7 +7,6 @@ import * as DOM from 'vs/base/browser/dom';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { NotImplementedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService';
@ -15,7 +14,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IListService, ListService } from 'vs/platform/list/browser/listService';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { EditorModel } from 'vs/workbench/common/editor';
import { EditorModel, IEditorInput } from 'vs/workbench/common/editor';
import { ICellViewModel, IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
@ -33,7 +32,8 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { mock } from 'vs/base/test/common/mock';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService';
export class TestCell extends NotebookCellTextModel {
constructor(
@ -92,10 +92,6 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi
return this._dirty;
}
isUntitled() {
return this._notebook.uri.scheme === Schemas.untitled;
}
getNotebook(): NotebookTextModel {
return this._notebook;
}
@ -116,7 +112,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi
return false;
}
saveAs(): Promise<boolean> {
saveAs(): Promise<IEditorInput | undefined> {
throw new NotImplementedError();
}
@ -134,11 +130,12 @@ export function setupInstantiationService() {
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService));
instantiationService.stub(IListService, instantiationService.createInstance(ListService));
instantiationService.stub(IClipboardService, new BrowserClipboardService());
return instantiationService;
}
export async function withTestNotebook<R = any>(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditor, accessor: ServicesAccessor) => Promise<R> | R): Promise<R> {
export async function withTestNotebook<R = any>(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditor, accessor: TestInstantiationService) => Promise<R> | R): Promise<R> {
const instantiationService = setupInstantiationService();
const viewType = 'notebook';
const notebook = instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test'), cells.map(cell => {
@ -173,6 +170,18 @@ export async function withTestNotebook<R = any>(cells: [source: string, lang: st
setHiddenAreas(_ranges: ICellRange[]): boolean {
return cellList.setHiddenAreas(_ranges, true);
}
getActiveCell() {
const elements = cellList.getFocusedElements();
if (elements && elements.length) {
return elements[0];
}
return undefined;
}
hasOutputTextSelection() {
return false;
}
};
const res = await callback(notebookEditor, instantiationService);