未验证 提交 a6710757 编写于 作者: P Peng Lyu 提交者: GitHub

Merge pull request #97043 from microsoft/rebornix/hotexit

Notebook document creation from UI side and hot exit
......@@ -212,7 +212,7 @@ suite('notebook dirty state', () => {
await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' });
// await vscode.commands.executeCommand('workbench.action.files.save');
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
......@@ -272,3 +272,51 @@ suite('notebook undo redo', () => {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
});
suite('notebook working copy', () => {
test('notebook revert on close', async function () {
const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
await waitFor(500);
await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow');
assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, '');
await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove');
await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' });
// close active editor from command will revert the file
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
await waitFor(500);
assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true);
assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true);
assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection);
assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test');
await vscode.commands.executeCommand('workbench.action.files.save');
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
test('notebook revert', async function () {
const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
await waitFor(500);
await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow');
assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, '');
await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove');
await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' });
await vscode.commands.executeCommand('workbench.action.files.revert');
assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true);
assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true);
assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection);
assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 1);
assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test');
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
});
......@@ -6,12 +6,22 @@
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext): any {
context.subscriptions.push(vscode.notebook.registerNotebookProvider('notebookCoreTest', {
resolveNotebook: async (editor: vscode.NotebookEditor) => {
await editor.edit(eb => {
eb.insert(0, 'test', 'typescript', vscode.CellKind.Code, [], {});
});
return;
context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', {
onDidChangeNotebook: new vscode.EventEmitter<void>().event,
openNotebook: async (_resource: vscode.Uri) => {
return {
languages: ['typescript'],
metadata: {},
cells: [
{
source: 'test',
language: 'typescript',
cellKind: vscode.CellKind.Code,
outputs: [],
metadata: {}
}
]
};
},
executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => {
if (!_cell) {
......@@ -26,8 +36,11 @@ export function activate(context: vscode.ExtensionContext): any {
}];
return;
},
save: async (_document: vscode.NotebookDocument) => {
return true;
saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => {
return;
},
saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => {
return;
}
}));
}
......@@ -1640,12 +1640,6 @@ declare module 'vscode' {
edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable<boolean>;
}
export interface NotebookProvider {
resolveNotebook(editor: NotebookEditor): Promise<void>;
executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise<void>;
save(document: NotebookDocument): Promise<boolean>;
}
export interface NotebookOutputSelector {
type: string;
subTypes?: string[];
......@@ -1708,11 +1702,6 @@ declare module 'vscode' {
provider: NotebookContentProvider
): Disposable;
export function registerNotebookProvider(
notebookType: string,
provider: NotebookProvider
): Disposable;
export function registerNotebookOutputRenderer(
type: string,
outputSelector: NotebookOutputSelector,
......
......@@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx
import { Disposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService';
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
......@@ -84,7 +84,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
registerListeners() {
this._register(this._notebookService.onDidChangeActiveEditor(e => {
this._proxy.$updateActiveEditor(e.viewType, e.uri);
this._proxy.$acceptDocumentAndEditorsDelta({
newActiveEditor: e.uri
});
}));
const updateOrder = () => {
......@@ -129,16 +131,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
return;
}
async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> {
let controller = this._notebookProviders.get(viewType);
if (controller) {
controller.createNotebookDocument(handle, viewType, resource);
}
return;
}
async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
......@@ -163,11 +155,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
}
}
async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> {
let handle = await this._proxy.$resolveNotebook(viewType, uri);
return handle;
}
async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers);
......@@ -195,6 +182,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
export class MainThreadNotebookController implements IMainNotebookController {
private _mapping: Map<string, MainThreadNotebookDocument> = new Map();
static documentHandle: number = 0;
constructor(
private readonly _proxy: ExtHostNotebookShape,
......@@ -203,26 +191,44 @@ export class MainThreadNotebookController implements IMainNotebookController {
) {
}
async resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined> {
// TODO: resolve notebook should wait for all notebook document destory operations to finish.
async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise<NotebookTextModel | undefined> {
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
if (mainthreadNotebook) {
if (forceReload) {
const data = await this._proxy.$resolveNotebookData(viewType, uri);
if (!data) {
return;
}
mainthreadNotebook.textModel.languages = data.languages;
mainthreadNotebook.textModel.metadata = data.metadata;
mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [
{ editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 },
{ editType: CellEditType.Insert, index: 0, cells: data.cells }
]);
}
return mainthreadNotebook.textModel;
}
let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri);
if (notebookHandle !== undefined) {
mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
if (mainthreadNotebook && mainthreadNotebook.textModel.cells.length === 0) {
// it's empty, we should create an empty template one
const mainCell = mainthreadNotebook.textModel.createCellTextModel([''], mainthreadNotebook.textModel.languages.length ? mainthreadNotebook.textModel.languages[0] : '', CellKind.Code, [], undefined);
mainthreadNotebook.textModel.insertTemplateCell(mainCell);
}
return mainthreadNotebook?.textModel;
let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri);
await this.createNotebookDocument(document);
if (forBackup) {
return document.textModel;
}
// open notebook document
const data = await this._proxy.$resolveNotebookData(viewType, uri);
if (!data) {
return;
}
return undefined;
document.textModel.languages = data.languages;
document.textModel.metadata = data.metadata;
document.textModel.initialize(data!.cells);
return document.textModel;
}
async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise<boolean> {
......@@ -250,12 +256,32 @@ export class MainThreadNotebookController implements IMainNotebookController {
this._proxy.$onDidReceiveMessage(uri, message);
}
// Methods for ExtHost
async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> {
let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource));
this._mapping.set(URI.revive(resource).toString(), document);
async createNotebookDocument(document: MainThreadNotebookDocument): Promise<void> {
this._mapping.set(document.uri.toString(), document);
await this._proxy.$acceptDocumentAndEditorsDelta({
addedDocuments: [{
viewType: document.viewType,
handle: document.handle,
uri: document.uri
}]
});
}
async removeNotebookDocument(notebook: INotebookTextModel): Promise<void> {
let document = this._mapping.get(URI.from(notebook.uri).toString());
if (!document) {
return;
}
await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] });
document.dispose();
this._mapping.delete(URI.from(notebook.uri).toString());
}
// Methods for ExtHost
updateLanguages(resource: UriComponents, languages: string[]) {
let document = this._mapping.get(URI.from(resource).toString());
document?.textModel.updateLanguages(languages);
......@@ -280,21 +306,12 @@ export class MainThreadNotebookController implements IMainNotebookController {
return this._proxy.$executeNotebook(this._viewType, uri, handle, token);
}
async destoryNotebookDocument(notebook: INotebookTextModel): Promise<void> {
let document = this._mapping.get(URI.from(notebook.uri).toString());
if (!document) {
return;
}
let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri);
if (removeFromExtHost) {
document.dispose();
this._mapping.delete(URI.from(notebook.uri).toString());
}
}
async save(uri: URI, token: CancellationToken): Promise<boolean> {
return this._proxy.$saveNotebook(this._viewType, uri, token);
}
async saveAs(uri: URI, target: URI, token: CancellationToken): Promise<boolean> {
return this._proxy.$saveNotebookAs(this._viewType, uri, target, token);
}
}
......@@ -910,10 +910,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension);
return extHostNotebook.onDidCloseNotebookDocument;
},
registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => {
checkProposedApiEnabled(extension);
return extHostNotebook.registerNotebookProvider(extension, viewType, provider);
},
registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => {
checkProposedApiEnabled(extension);
return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider);
......
......@@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
import { revive } from 'vs/base/common/marshalling';
import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { Dto } from 'vs/base/common/types';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
......@@ -691,7 +691,6 @@ export interface MainThreadNotebookShape extends IDisposable {
$unregisterNotebookProvider(viewType: string): Promise<void>;
$registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void>;
$unregisterNotebookRenderer(handle: number): Promise<void>;
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
$tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise<boolean>;
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
$updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise<void>;
......@@ -1538,16 +1537,31 @@ export interface INotebookEditorPropertiesChangeData {
selections: INotebookSelectionChangeEvent | null;
}
export interface INotebookModelAddedData {
uri: UriComponents;
handle: number;
// versionId: number;
viewType: string;
}
export interface INotebookDocumentsAndEditorsDelta {
removedDocuments?: UriComponents[];
addedDocuments?: INotebookModelAddedData[];
// removedEditors?: string[];
// addedEditors?: ITextEditorAddData[];
newActiveEditor?: UriComponents | null;
}
export interface ExtHostNotebookShape {
$resolveNotebook(viewType: string, uri: UriComponents): Promise<number | undefined>;
$resolveNotebookData(viewType: string, uri: UriComponents): Promise<NotebookDataDto | undefined>;
$executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise<void>;
$saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean>;
$updateActiveEditor(viewType: string, uri: UriComponents): Promise<void>;
$destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean>;
$saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean>;
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void;
$onDidReceiveMessage(uri: UriComponents, message: any): void;
$acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void;
$acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void;
$acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise<void>;
}
export interface ExtHostStorageShape {
......
......@@ -10,10 +10,10 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc
import { ISplice } from 'vs/base/common/sequence';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol';
import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { Disposable as VSCodeDisposable } from './extHostTypes';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
......@@ -345,8 +345,6 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
transformMimeTypes(output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto {
let mimeTypes = Object.keys(output.data);
// TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side.
let coreDisplayOrder = this.renderingHandler.outputDisplayOrder;
const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []);
......@@ -415,7 +413,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
}
}
export class NotebookEditorCellEdit {
export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit {
private _finalized: boolean = false;
private readonly _documentVersionId: number;
private _collectedEdits: ICellEditOperation[] = [];
......@@ -526,13 +524,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
}));
}
edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable<boolean> {
const edit = new NotebookEditorCellEdit(this);
edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable<boolean> {
const edit = new NotebookEditorCellEditBuilder(this);
callback(edit);
return this._applyEdit(edit);
}
private _applyEdit(editBuilder: NotebookEditorCellEdit): Promise<boolean> {
private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise<boolean> {
const editData = editBuilder.finalize();
// return when there is nothing to do
......@@ -625,7 +623,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
private static _handlePool: number = 0;
private readonly _proxy: MainThreadNotebookShape;
private readonly _notebookProviders = new Map<string, { readonly provider: vscode.NotebookProvider, readonly extension: IExtensionDescription; }>();
private readonly _notebookContentProviders = new Map<string, { readonly provider: vscode.NotebookContentProvider, readonly extension: IExtensionDescription; }>();
private readonly _documents = new Map<string, ExtHostNotebookDocument>();
private readonly _editors = new Map<string, { editor: ExtHostNotebookEditor, onDidReceiveMessage: Emitter<any>; }>();
......@@ -706,31 +703,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
return matches;
}
registerNotebookProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.NotebookProvider,
): vscode.Disposable {
if (this._notebookProviders.has(viewType)) {
throw new Error(`Notebook provider for '${viewType}' already registered`);
}
this._notebookProviders.set(viewType, { extension, provider });
this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType);
return new VSCodeDisposable(() => {
this._notebookProviders.delete(viewType);
this._proxy.$unregisterNotebookProvider(viewType);
});
}
registerNotebookContentProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.NotebookContentProvider,
): vscode.Disposable {
if (this._notebookProviders.has(viewType)) {
if (this._notebookContentProviders.has(viewType)) {
throw new Error(`Notebook provider for '${viewType}' already registered`);
}
......@@ -742,97 +721,99 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
});
}
async _resolveNotebookFromContentProvider(viewType: string, uri: UriComponents): Promise<number | undefined> {
async $resolveNotebookData(viewType: string, uri: UriComponents): Promise<NotebookDataDto | undefined> {
let provider = this._notebookContentProviders.get(viewType);
let document = this._documents.get(URI.revive(uri).toString());
if (provider) {
const revivedUri = URI.revive(uri);
if (!this._documents.has(revivedUri.toString())) {
let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this);
await this._proxy.$createNotebookDocument(
document.handle,
viewType,
uri
);
this._documents.set(revivedUri.toString(), document);
}
if (provider && document) {
const rawCells = await provider.provider.openNotebook(URI.revive(uri));
const renderers = new Set<number>();
const dto = {
metadata: {
...notebookDocumentMetadataDefaults,
...rawCells.metadata
},
languages: rawCells.languages,
cells: rawCells.cells.map(cell => {
let transformedOutputs = cell.outputs.map(output => {
if (output.outputKind === CellOutputKind.Rich) {
// TODO display string[]
const ret = this._transformMimeTypes(document!, (rawCells.metadata.displayOrder as string[]) || [], output);
if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) {
renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!);
}
return ret;
} else {
return output as IStreamOutput | IErrorOutput;
}
});
const onDidReceiveMessage = new Emitter<any>();
let editor = new ExtHostNotebookEditor(
viewType,
`${ExtHostNotebookController._handlePool++}`,
revivedUri,
this._proxy,
onDidReceiveMessage,
this._documents.get(revivedUri.toString())!,
this._documentsAndEditors
);
this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage });
const data = await provider.provider.openNotebook(revivedUri);
editor.document.languages = data.languages;
editor.document.metadata = {
...notebookDocumentMetadataDefaults,
...data.metadata
return {
language: cell.language,
cellKind: cell.cellKind,
metadata: cell.metadata,
source: cell.source,
outputs: transformedOutputs
};
})
};
await editor.edit(editBuilder => {
for (let i = 0; i < data.cells.length; i++) {
const cell = data.cells[i];
editBuilder.insert(0, cell.source, cell.language, cell.cellKind, cell.outputs, cell.metadata);
}
});
this._onDidOpenNotebookDocument.fire(editor.document);
return editor.document.handle;
} else {
return Promise.resolve(undefined);
return dto;
}
return;
}
async $resolveNotebook(viewType: string, uri: UriComponents): Promise<number | undefined> {
let notebookFromNotebookContentProvider = await this._resolveNotebookFromContentProvider(viewType, uri);
private _transformMimeTypes(document: ExtHostNotebookDocument, displayOrder: string[], output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto {
let mimeTypes = Object.keys(output.data);
let coreDisplayOrder = this.outputDisplayOrder;
const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], displayOrder, coreDisplayOrder?.defaultOrder || []);
if (notebookFromNotebookContentProvider !== undefined) {
return notebookFromNotebookContentProvider;
}
let orderMimeTypes: IOrderedMimeType[] = [];
let provider = this._notebookProviders.get(viewType);
sorted.forEach(mimeType => {
let handlers = this.findBestMatchedRenderer(mimeType);
if (provider) {
if (!this._documents.has(URI.revive(uri).toString())) {
let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, URI.revive(uri), this);
await this._proxy.$createNotebookDocument(
document.handle,
viewType,
uri
);
if (handlers.length) {
let renderedOutput = handlers[0].render(document, output, mimeType);
this._documents.set(URI.revive(uri).toString(), document);
}
orderMimeTypes.push({
mimeType: mimeType,
isResolved: true,
rendererId: handlers[0].handle,
output: renderedOutput
});
const onDidReceiveMessage = new Emitter<any>();
let editor = new ExtHostNotebookEditor(
viewType,
`${ExtHostNotebookController._handlePool++}`,
URI.revive(uri),
this._proxy,
onDidReceiveMessage,
this._documents.get(URI.revive(uri).toString())!,
this._documentsAndEditors
);
this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage });
await provider.provider.resolveNotebook(editor);
// await editor.document.$updateCells();
return editor.document.handle;
}
for (let i = 1; i < handlers.length; i++) {
orderMimeTypes.push({
mimeType: mimeType,
isResolved: false,
rendererId: handlers[i].handle
});
}
return Promise.resolve(undefined);
if (mimeTypeSupportedByCore(mimeType)) {
orderMimeTypes.push({
mimeType: mimeType,
isResolved: false,
rendererId: -1
});
}
} else {
orderMimeTypes.push({
mimeType: mimeType,
isResolved: false
});
}
});
return {
outputKind: output.outputKind,
data: output.data,
orderedMimeTypes: orderMimeTypes,
pickedMimeTypeIndex: 0
};
}
async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise<void> {
......@@ -847,15 +828,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
return this._notebookContentProviders.get(viewType)!.provider.executeCell(document, cell, token);
}
let provider = this._notebookProviders.get(viewType);
if (!provider) {
return;
}
let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
return provider.provider.executeCell(document!, cell, token);
}
async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean> {
......@@ -874,44 +846,26 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
return true;
}
let provider = this._notebookProviders.get(viewType);
if (provider && document) {
return await provider.provider.save(document);
}
return false;
}
async $updateActiveEditor(viewType: string, uri: UriComponents): Promise<void> {
this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString());
this._activeNotebookEditor = this._editors.get(URI.revive(uri).toString())?.editor;
}
async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean> {
let provider = this._notebookProviders.get(viewType);
if (!provider) {
return false;
}
async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean> {
let document = this._documents.get(URI.revive(uri).toString());
if (document) {
document.dispose();
this._documents.delete(URI.revive(uri).toString());
this._onDidCloseNotebookDocument.fire(document);
if (!document) {
return false;
}
let editor = this._editors.get(URI.revive(uri).toString());
if (this._notebookContentProviders.has(viewType)) {
try {
await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token);
} catch (e) {
return false;
}
if (editor) {
editor.editor.dispose();
editor.onDidReceiveMessage.dispose();
this._editors.delete(URI.revive(uri).toString());
return true;
}
return true;
return false;
}
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void {
......@@ -957,4 +911,60 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
}
}
}
async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) {
if (delta.removedDocuments) {
delta.removedDocuments.forEach((uri) => {
let document = this._documents.get(URI.revive(uri).toString());
if (document) {
document.dispose();
this._documents.delete(URI.revive(uri).toString());
this._onDidCloseNotebookDocument.fire(document);
}
let editor = this._editors.get(URI.revive(uri).toString());
if (editor) {
editor.editor.dispose();
editor.onDidReceiveMessage.dispose();
this._editors.delete(URI.revive(uri).toString());
}
});
}
if (delta.addedDocuments) {
delta.addedDocuments.forEach(modelData => {
const revivedUri = URI.revive(modelData.uri);
const viewType = modelData.viewType;
if (!this._documents.has(revivedUri.toString())) {
let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this);
this._documents.set(revivedUri.toString(), document);
}
const onDidReceiveMessage = new Emitter<any>();
const document = this._documents.get(revivedUri.toString())!;
let editor = new ExtHostNotebookEditor(
viewType,
`${ExtHostNotebookController._handlePool++}`,
revivedUri,
this._proxy,
onDidReceiveMessage,
document,
this._documentsAndEditors
);
this._onDidOpenNotebookDocument.fire(document);
// TODO, does it already exist?
this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage });
});
}
if (delta.newActiveEditor) {
this._activeNotebookDocument = this._documents.get(URI.revive(delta.newActiveEditor).toString());
this._activeNotebookEditor = this._editors.get(URI.revive(delta.newActiveEditor).toString())?.editor;
}
}
}
......@@ -21,6 +21,10 @@
overflow: visible !important;
} */
.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible {
z-index: 100;
}
.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div {
z-index: 600 !important;
/* @rebornix: larger than the editor title bar */
......
......@@ -3,13 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor';
import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
import { isEqual, basename } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
export class NotebookEditorInput extends EditorInput {
......@@ -39,7 +40,10 @@ export class NotebookEditorInput extends EditorInput {
public name: string,
public readonly viewType: string | undefined,
@INotebookService private readonly notebookService: INotebookService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
// @IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
}
......@@ -85,6 +89,48 @@ export class NotebookEditorInput extends EditorInput {
return undefined;
}
async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
if (!this.textModel) {
return undefined;
}
const dialogPath = this.textModel.resource;
const target = await this.fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems);
if (!target) {
return undefined; // save cancelled
}
if (!await this.textModel.saveAs(target)) {
return undefined;
}
return this._move(group, target)?.editor;
}
move(group: GroupIdentifier, target: URI): IMoveResult | undefined {
if (this.textModel) {
const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target);
if (contributedNotebookProviders.find(provider => provider.id === this.textModel!.viewType)) {
return this._move(group, target);
}
}
return undefined;
}
_move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined {
const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType);
return { editor: editorInput };
}
async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
if (this.textModel) {
await this.textModel.revert(options);
}
return;
}
async resolve(): Promise<NotebookEditorModel> {
if (!await this.notebookService.canResolve(this.viewType!)) {
throw new Error(`Cannot open notebook of type '${this.viewType}'`);
......@@ -96,6 +142,10 @@ export class NotebookEditorInput extends EditorInput {
this._onDidChangeDirty.fire();
}));
if (this.textModel.isDirty()) {
this._onDidChangeDirty.fire();
}
return this.textModel;
}
......
......@@ -11,7 +11,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from '
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter, Event } from 'vs/base/common/event';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer';
import { Iterable } from 'vs/base/common/iterator';
......@@ -202,13 +202,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return;
}
async resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined> {
async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise<NotebookTextModel | undefined> {
const provider = this._notebookProviders.get(viewType);
if (!provider) {
return undefined;
}
const notebookModel = await provider.controller.resolveNotebook(viewType, uri);
const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false);
if (!notebookModel) {
return undefined;
}
......@@ -219,6 +219,39 @@ export class NotebookService extends Disposable implements INotebookService, ICu
notebookModel,
(model) => this._onWillDispose(model),
);
this._models[modelId] = modelData;
notebookModel.metadata = metadata;
notebookModel.languages = languages;
notebookModel.applyEdit(notebookModel.versionId, [
{
editType: CellEditType.Insert,
index: 0,
cells: cells
}
]);
return modelData.model;
}
async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise<NotebookTextModel | undefined> {
const provider = this._notebookProviders.get(viewType);
if (!provider) {
return undefined;
}
let notebookModel: NotebookTextModel | undefined;
notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload);
// new notebook model created
const modelId = MODEL_ID(uri);
const modelData = new ModelData(
notebookModel!,
(model) => this._onWillDispose(model),
);
this._models[modelId] = modelData;
return modelData.model;
}
......@@ -265,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu
let provider = this._notebookProviders.get(viewType);
if (provider) {
provider.controller.destoryNotebookDocument(notebook);
provider.controller.removeNotebookDocument(notebook);
}
}
......@@ -291,6 +324,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return false;
}
async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise<boolean> {
let provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.controller.saveAs(resource, target, token);
}
return false;
}
onDidReceiveMessage(viewType: string, uri: URI, message: any): void {
let provider = this._notebookProviders.get(viewType);
......
......@@ -157,7 +157,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
......@@ -177,7 +177,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
......@@ -299,7 +299,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
......@@ -316,6 +316,18 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
splice2(start: number, deleteCount: number, elements: CellViewModel[] = []): void {
// we need to convert start and delete count based on hidden ranges
super.splice(start, deleteCount, elements);
const selectionsLeft = [];
this._viewModel!.selectionHandles.forEach(handle => {
if (this._viewModel!.hasCell(handle)) {
selectionsLeft.push(handle);
}
});
if (!selectionsLeft.length && this._viewModel!.viewCells) {
// after splice, the selected cells are deleted
this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle];
}
}
getViewIndex(cell: ICellViewModel) {
......
......@@ -268,7 +268,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
});
diffs.reverse().forEach(diff => {
this._viewCells.splice(diff[0], diff[1], ...diff[2]);
const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]);
deletedCells.forEach(cell => {
this._handleToViewCellMapping.delete(cell.handle);
});
diff[2].forEach(cell => {
this._handleToViewCellMapping.set(cell.handle, cell);
this._localStore.add(cell);
......@@ -456,8 +461,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return index + 1;
}
hasCell(cell: ICellViewModel) {
return this._handleToViewCellMapping.has(cell.handle);
hasCell(handle: number) {
return this._handleToViewCellMapping.has(handle);
}
getVersionId() {
......@@ -586,7 +591,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
this._viewCells.splice(deleteIndex, 1);
this._handleToViewCellMapping.delete(deleteCell.handle);
this._notebook.removeCell(deleteIndex);
this._notebook.removeCell(deleteIndex, 1);
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] });
}
......@@ -638,7 +643,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
this._viewCells.splice(index, 1);
this._handleToViewCellMapping.delete(viewCell.handle);
this._notebook.removeCell(index);
this._notebook.removeCell(index, 1);
let endSelections: number[] = [];
if (this.selectionHandles.length) {
......
......@@ -7,7 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, 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, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ITextSnapshot } from 'vs/editor/common/model';
function compareRangesUsingEnds(a: [number, number], b: [number, number]): number {
if (a[1] === b[1]) {
......@@ -17,6 +18,49 @@ function compareRangesUsingEnds(a: [number, number], b: [number, number]): numbe
return a[1] - b[1];
}
export class NotebookTextModelSnapshot implements ITextSnapshot {
// private readonly _pieces: Ce[] = [];
private _index: number = -1;
constructor(private _model: NotebookTextModel) {
// for (let i = 0; i < this._model.cells.length; i++) {
// const cell = this._model.cells[i];
// this._pieces.push(this._model.cells[i].textBuffer.createSnapshot(true));
// }
}
read(): string | null {
if (this._index === -1) {
this._index++;
return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "languages": ${JSON.stringify(this._model.languages)}, "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
};
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;
}
}
}
export class NotebookTextModel extends Disposable implements INotebookTextModel {
private static _cellhandlePool: number = 0;
......@@ -77,6 +121,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata);
}
initialize(cells: ICellDto2[]) {
this.cells = [];
this._versionId = 0;
const mainCells = cells.map(cell => {
const cellHandle = NotebookTextModel._cellhandlePool++;
const cellUri = CellUri.generate(this.uri, cellHandle);
return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata);
});
this.insertNewCell(0, mainCells);
}
applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[]): boolean {
if (modelVersionId !== this._versionId) {
return false;
......@@ -127,7 +183,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
this.insertNewCell(insertEdit.index, mainCells);
break;
case CellEditType.Delete:
this.removeCell(operations[i].index);
this.removeCell(operations[i].index, operations[i].end - operations[i].start);
break;
}
}
......@@ -142,6 +198,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return true;
}
createSnapshot(preserveBOM?: boolean): ITextSnapshot {
return new NotebookTextModelSnapshot(this);
}
private _increaseVersionId(): void {
this._versionId = this._versionId + 1;
}
......@@ -250,17 +310,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return;
}
removeCell(index: number) {
removeCell(index: number, count: number) {
this._isUntitled = false;
let cell = this.cells[index];
this._cellListeners.get(cell.handle)?.dispose();
this._cellListeners.delete(cell.handle);
this.cells.splice(index, 1);
for (let i = index; i < index + count; i++) {
let cell = this.cells[i];
this._cellListeners.get(cell.handle)?.dispose();
this._cellListeners.delete(cell.handle);
}
this.cells.splice(index, count);
this._onDidChangeContent.fire();
this._increaseVersionId();
this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, 1, []]] });
this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] });
}
moveCellToIdx(index: number, newIdx: number) {
......
......@@ -273,7 +273,7 @@ export enum CellEditType {
}
export interface ICellDto2 {
source: string[];
source: string | string[];
language: string;
cellKind: CellKind;
outputs: IOutput[];
......@@ -300,6 +300,13 @@ export interface INotebookEditData {
renderers: number[];
}
export interface NotebookDataDto {
readonly cells: ICellDto2[];
readonly languages: string[];
readonly metadata: NotebookDocumentMetadata;
}
export namespace CellUri {
export const scheme = 'vscode-notebook';
......
......@@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri';
import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { basename } from 'vs/base/common/resources';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model';
export interface INotebookEditorModelManager {
models: NotebookEditorModel[];
......@@ -24,6 +26,13 @@ export interface INotebookEditorModelManager {
get(resource: URI): NotebookEditorModel | undefined;
}
export interface INotebookRevertOptions {
/**
* Go to disk bypassing any cache of the model if any.
*/
forceReadFromDisk?: boolean;
}
export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel {
private _dirty = false;
......@@ -47,7 +56,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
public readonly resource: URI,
public readonly viewType: string,
@INotebookService private readonly notebookService: INotebookService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IBackupFileService private readonly backupFileService: IBackupFileService
) {
super();
this._register(this.workingCopyService.registerWorkingCopy(this));
......@@ -56,28 +66,91 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
capabilities = 0;
async backup(): Promise<IWorkingCopyBackup> {
return {};
return { content: this._notebook.createSnapshot(true) };
}
async revert(options?: IRevertOptions | undefined): Promise<void> {
if (options?.soft) {
await this.backupFileService.discardBackup(this.resource);
return;
}
await this.load({ forceReadFromDisk: true });
this._dirty = false;
this._onDidChangeDirty.fire();
return;
}
async load(): Promise<NotebookEditorModel> {
const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource);
async load(options?: INotebookRevertOptions): Promise<NotebookEditorModel> {
if (options?.forceReadFromDisk) {
return this.loadFromProvider(true);
}
if (this.isResolved()) {
return this;
}
const backup = await this.backupFileService.resolve(this.resource);
if (this.isResolved()) {
return this; // Make sure meanwhile someone else did not succeed in loading
}
if (backup) {
try {
return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF));
} catch (error) {
// this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below
}
}
return this.loadFromProvider(false);
}
private async loadFromBackup(content: ITextBuffer): Promise<NotebookEditorModel> {
const fullRange = content.getRangeAt(0, content.getLength());
const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF));
const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells);
this._notebook = notebook!;
this._name = basename(this._notebook!.uri);
this._register(this._notebook.onDidChangeContent(() => {
this._dirty = true;
this._onDidChangeDirty.fire();
this.setDirty(true);
this._onDidChangeContent.fire();
}));
await this.backupFileService.discardBackup(this.resource);
this.setDirty(true);
return this;
}
private async loadFromProvider(forceReloadFromDisk: boolean) {
const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk);
this._notebook = notebook!;
this._name = basename(this._notebook!.uri);
this._register(this._notebook.onDidChangeContent(() => {
this.setDirty(true);
this._onDidChangeContent.fire();
}));
return this;
}
isResolved(): boolean {
return !!this._notebook;
}
setDirty(newState: boolean) {
if (this._dirty !== newState) {
this._dirty = newState;
this._onDidChangeDirty.fire();
}
}
isDirty() {
return this._dirty;
}
......@@ -89,6 +162,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
this._onDidChangeDirty.fire();
return true;
}
async saveAs(targetResource: URI): Promise<boolean> {
const tokenSource = new CancellationTokenSource();
await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token);
this._dirty = false;
this._onDidChangeDirty.fire();
return true;
}
}
export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager {
......
......@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Event } from 'vs/base/common/event';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CancellationToken } from 'vs/base/common/cancellation';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
......@@ -17,12 +17,13 @@ import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/commo
export const INotebookService = createDecorator<INotebookService>('notebookService');
export interface IMainNotebookController {
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise<NotebookTextModel | undefined>;
executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise<void>;
onDidReceiveMessage(uri: URI, message: any): void;
executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise<void>;
destoryNotebookDocument(notebook: INotebookTextModel): Promise<void>;
removeNotebookDocument(notebook: INotebookTextModel): Promise<void>;
save(uri: URI, token: CancellationToken): Promise<boolean>;
saveAs(uri: URI, target: URI, token: CancellationToken): Promise<boolean>;
}
export interface INotebookService {
......@@ -35,7 +36,8 @@ export interface INotebookService {
registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void;
unregisterNotebookRenderer(handle: number): void;
getRendererInfo(handle: number): INotebookRendererInfo | undefined;
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise<NotebookTextModel | undefined>;
createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise<NotebookTextModel | undefined>;
executeNotebook(viewType: string, uri: URI): Promise<void>;
executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise<void>;
......@@ -45,6 +47,7 @@ export interface INotebookService {
destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void;
updateActiveNotebookDocument(viewType: string, resource: URI): void;
save(viewType: string, resource: URI, token: CancellationToken): Promise<boolean>;
saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise<boolean>;
onDidReceiveMessage(viewType: string, uri: URI, message: any): void;
setToCopy(items: NotebookCellTextModel[]): void;
getToCopy(): NotebookCellTextModel[] | undefined;
......
......@@ -41,15 +41,20 @@ suite('NotebookConcatDocument', function () {
rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock<MainThreadNotebookShape>() {
async $registerNotebookProvider() { }
async $unregisterNotebookProvider() { }
async $createNotebookDocument() { }
});
extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService());
extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors);
extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors);
let reg = extHostNotebooks.registerNotebookProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookProvider>() {
async resolveNotebook() { }
let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() {
// async openNotebook() { }
});
await extHostNotebooks.$acceptDocumentAndEditorsDelta({
addedDocuments: [{
handle: 0,
uri: notebookUri,
viewType: 'test'
}]
});
await extHostNotebooks.$resolveNotebook('test', notebookUri);
extHostNotebooks.$acceptModelChanged(notebookUri, {
kind: NotebookCellsChangeType.ModelChange,
versionId: 0,
......@@ -62,7 +67,7 @@ suite('NotebookConcatDocument', function () {
outputs: [],
}]]]
});
await extHostNotebooks.$updateActiveEditor('test', notebookUri);
await extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: notebookUri });
notebook = extHostNotebooks.activeNotebookDocument!;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册