mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
Merge branch 'notebook/dev' into main
This commit is contained in:
commit
06a451d815
|
@ -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) {
|
||||
//
|
||||
}
|
||||
|
|
|
@ -17,9 +17,6 @@ suite('Notebook Editor', function () {
|
|||
);
|
||||
|
||||
}
|
||||
async resolveNotebook(_document: vscode.NotebookDocument, _webview: vscode.NotebookCommunication) {
|
||||
//
|
||||
}
|
||||
async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) {
|
||||
//
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
30
src/vs/vscode.proposed.d.ts
vendored
30
src/vs/vscode.proposed.d.ts
vendored
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue