Diff for cells and outputs in exthost.

This commit is contained in:
rebornix
2020-02-18 16:31:22 -08:00
parent 25fc954e0c
commit 796c2baa80
5 changed files with 190 additions and 120 deletions

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol';
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, NotebookCellsSplice, NotebookCellOutputsSplice } from '../common/extHost.protocol';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService';
@@ -60,6 +60,14 @@ export class MainThreadCell implements ICell {
});
}
spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void {
splices.reverse().forEach(splice => {
this.outputs.splice(splice[0], splice[1], ...splice[2]);
});
this._onDidChangeOutputs.fire();
}
save() {
this._isDirty = false;
}
@@ -110,39 +118,6 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook
});
}
updateCell(cell: ICell) {
let mcell = this._mapping.get(cell.handle);
if (mcell) {
mcell.outputs = cell.outputs;
}
}
updateCells(newCells: ICell[]) {
// todo, handle cell insertion and deletion
if (this.cells.length === 0) {
newCells.forEach(cell => {
let mainCell = new MainThreadCell(this, cell.handle, cell.source, cell.language, cell.cell_type, cell.outputs);
this._mapping.set(cell.handle, mainCell);
this.cells.push(mainCell);
let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => {
this.isDirty = this.isDirty || cellState;
});
this._cellListeners.set(cell.handle, dirtyStateListener);
});
} else {
newCells.forEach(newCell => {
let cell = this._mapping.get(newCell.handle);
if (cell) {
cell.outputs = newCell.outputs;
}
});
}
this._onDidChangeCells.fire();
}
updateActiveCell(handle: number) {
this.activeCell = this._mapping.get(handle);
}
@@ -190,6 +165,31 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook
return ret;
}
spliceNotebookCells(splices: NotebookCellsSplice[]): void {
splices.reverse().forEach(splice => {
let cellDtos = splice[2];
let newCells = cellDtos.map(cell => {
let mainCell = new MainThreadCell(this, cell.handle, cell.source, cell.language, cell.cell_type, cell.outputs || []);
this._mapping.set(cell.handle, mainCell);
let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => {
this.isDirty = this.isDirty || cellState;
});
this._cellListeners.set(cell.handle, dirtyStateListener);
return mainCell;
});
this.cells.splice(splice[0], splice[1], ...newCells);
});
// @TODO, support incremental insertion/deletion instead of simple list relayout.
this._onDidChangeCells.fire();
}
spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void {
let cell = this._mapping.get(cellHandle);
cell?.spliceNotebookCellOutputs(splices);
}
dispose() {
this._onWillDispose.fire();
super.dispose();
@@ -265,14 +265,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
return;
}
async $updateNotebookCells(viewType: string, resource: UriComponents, cells: ICell[], renderers: number[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
if (controller) {
controller.updateNotebookCells(resource, cells, renderers);
}
}
async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
@@ -286,6 +278,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
return handle;
}
async $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
controller?.spliceNotebookCells(resource, splices);
}
async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise<void> {
let controller = this._notebookProviders.get(viewType);
controller?.spliceNotebookCellOutputs(resource, cellHandle, splices);
}
async executeNotebook(viewType: string, uri: URI): Promise<void> {
return this._proxy.$executeNotebook(viewType, uri, undefined);
}
@@ -318,6 +320,16 @@ export class MainThreadNotebookController implements IMainNotebookController {
return undefined;
}
spliceNotebookCells(resource: UriComponents, splices: NotebookCellsSplice[]): void {
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
mainthreadNotebook?.spliceNotebookCells(splices);
}
spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): void {
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices);
}
async executeNotebook(viewType: string, uri: URI): Promise<void> {
this._mainThreadNotebook.executeNotebook(viewType, uri);
}
@@ -328,22 +340,11 @@ export class MainThreadNotebookController implements IMainNotebookController {
this._mapping.set(URI.revive(resource).toString(), document);
}
updateNotebook(resource: UriComponents, notebook: INotebook): void {
let document = this._mapping.get(URI.from(resource).toString());
document?.updateCells(notebook.cells);
}
updateLanguages(resource: UriComponents, languages: string[]) {
let document = this._mapping.get(URI.from(resource).toString());
document?.updateLanguages(languages);
}
updateNotebookCells(resource: UriComponents, cells: ICell[], renderers: number[]): void {
let document = this._mapping.get(URI.from(resource).toString());
document?.updateRenderers(renderers);
document?.updateCells(cells);
}
updateNotebookRenderers(resource: UriComponents, renderers: number[]): void {
let document = this._mapping.get(URI.from(resource).toString());
document?.updateRenderers(renderers);

View File

@@ -640,17 +640,29 @@ export interface ICellDto {
language: string;
cell_type: 'markdown' | 'code';
outputs: IOutput[];
isDirty: boolean;
}
export type NotebookCellsSplice = [
number /* start */,
number /* delete count */,
ICellDto[]
];
export type NotebookCellOutputsSplice = [
number /* start */,
number /* delete count */,
IOutput[]
];
export interface MainThreadNotebookShape extends IDisposable {
$registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise<void>;
$unregisterNotebookProvider(viewType: string): Promise<void>;
$registerNotebookRenderer(extension: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void>;
$unregisterNotebookRenderer(handle: number): Promise<void>;
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
$updateNotebookCells(viewType: string, resource: UriComponents, cells: ICellDto[], renderers: number[]): Promise<void>;
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
$spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[]): Promise<void>;
$spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise<void>;
}
export interface MainThreadUrlsShape extends IDisposable {

View File

@@ -14,22 +14,85 @@ import { readonly } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ISplice } from 'vs/base/common/sequence';
interface ExtHostOutputDisplayOrder {
defaultOrder: glob.ParsedPattern[];
userOrder?: glob.ParsedPattern[];
}
interface IMutableSplice<T> extends ISplice<T> {
deleteCount: number;
}
function diff<T>(before: T[], after: T[], contains: (a: T) => boolean): ISplice<T>[] {
const result: IMutableSplice<T>[] = [];
function pushSplice(start: number, deleteCount: number, toInsert: T[]): void {
if (deleteCount === 0 && toInsert.length === 0) {
return;
}
const latest = result[result.length - 1];
if (latest && latest.start + latest.deleteCount === start) {
latest.deleteCount += deleteCount;
latest.toInsert.push(...toInsert);
} else {
result.push({ start, deleteCount, toInsert });
}
}
let beforeIdx = 0;
let afterIdx = 0;
while (true) {
if (beforeIdx === before.length) {
pushSplice(beforeIdx, 0, after.slice(afterIdx));
break;
}
if (afterIdx === after.length) {
pushSplice(beforeIdx, before.length - beforeIdx, []);
break;
}
const beforeElement = before[beforeIdx];
const afterElement = after[afterIdx];
if (beforeElement === afterElement) {
// equal
beforeIdx += 1;
afterIdx += 1;
continue;
}
if (contains(afterElement)) {
// `afterElement` exists before, which means some elements before `afterElement` are deleted
pushSplice(beforeIdx, 1, []);
beforeIdx += 1;
} else {
// `afterElement` added
pushSplice(beforeIdx, 0, [afterElement]);
afterIdx += 1;
}
}
return result;
}
export class ExtHostCell implements vscode.NotebookCell {
private static _handlePool: number = 0;
readonly handle = ExtHostCell._handlePool++;
public source: string[];
private _outputs: any[];
private _onDidChangeOutputs = new Emitter<void>();
onDidChangeOutputs: Event<void> = this._onDidChangeOutputs.event;
private _onDidChangeOutputs = new Emitter<ISplice<vscode.CellOutput>[]>();
onDidChangeOutputs: Event<ISplice<vscode.CellOutput>[]> = this._onDidChangeOutputs.event;
private _textDocument: vscode.TextDocument | undefined;
private _initalVersion: number = -1;
private _outputMapping = new Set<vscode.CellOutput>();
constructor(
private _content: string,
@@ -46,8 +109,22 @@ export class ExtHostCell implements vscode.NotebookCell {
}
set outputs(newOutputs: any[]) {
let diffs = diff<vscode.CellOutput>(this._outputs, newOutputs, (a) => {
return this._outputMapping.has(a);
});
diffs.forEach(diff => {
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
this._outputMapping.delete(this._outputs[i]);
}
diff.toInsert.forEach(output => {
this._outputMapping.add(output);
});
});
this._outputs = newOutputs;
this._onDidChangeOutputs.fire();
this._onDidChangeOutputs.fire(diffs);
}
getContent(): string {
@@ -73,6 +150,7 @@ export class ExtHostCell implements vscode.NotebookCell {
}
}
export class ExtHostNotebookDocument implements vscode.NotebookDocument {
private static _handlePool: number = 0;
readonly handle = ExtHostNotebookDocument._handlePool++;
@@ -86,17 +164,26 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument {
}
set cells(newCells: ExtHostCell[]) {
this._cells = newCells;
this._cells.forEach(cell => {
if (!this._cellDisposableMapping.has(cell.handle)) {
this._cellDisposableMapping.set(cell.handle, new DisposableStore());
let diffs = diff<ExtHostCell>(this._cells, newCells, (a) => {
return this._cellDisposableMapping.has(a.handle);
});
diffs.forEach(diff => {
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
this._cellDisposableMapping.get(this._cells[i].handle)?.clear();
this._cellDisposableMapping.delete(this._cells[i].handle);
}
let store = this._cellDisposableMapping.get(cell.handle)!;
store.add(cell.onDidChangeOutputs(() => {
this.triggerCellUpdates([cell]);
}));
diff.toInsert.forEach(cell => {
this._cellDisposableMapping.set(cell.handle, new DisposableStore());
this._cellDisposableMapping.get(cell.handle)?.add(cell.onDidChangeOutputs((outputDiffs) => {
this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDiffs.map(diff => [diff.start, diff.deleteCount, diff.toInsert]));
}));
});
});
this._cells = newCells;
this.eventuallyUpdateCells(diffs);
}
private _languages: string[] = [];
@@ -139,8 +226,20 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument {
get isDirty() { return false; }
async $updateCells(): Promise<void> {
return await this.triggerCellUpdates(this.cells);
eventuallyUpdateCells(diffs: ISplice<ExtHostCell>[]) {
this._proxy.$spliceNotebookCells(
this.viewType,
this.uri,
diffs.map(diff =>
[diff.start, diff.deleteCount, diff.toInsert.map(cell => ({
handle: cell.handle,
source: cell.source,
language: cell.language,
cell_type: cell.cell_type,
outputs: cell.outputs
}))]
)
);
}
insertCell(index: number, cell: ExtHostCell) {
@@ -152,9 +251,11 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument {
let store = this._cellDisposableMapping.get(cell.handle)!;
store.add(cell.onDidChangeOutputs(() => {
this.triggerCellUpdates([cell]);
store.add(cell.onDidChangeOutputs((diffs) => {
this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, diffs.map(diff => [diff.start, diff.deleteCount, diff.toInsert]));
}));
this.eventuallyUpdateCells([{ start: index, deleteCount: 0, toInsert: [cell] }]);
}
deleteCell(index: number): boolean {
@@ -170,51 +271,6 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument {
return true;
}
private async triggerCellUpdates(cells: ExtHostCell[]) {
// @TODO peng, diffing
let renderers = new Set<number>();
let cellDtos = this.cells.map(cell => {
let outputs = cell.outputs;
if (outputs && outputs.length) {
outputs = outputs.map(output => {
let richestMimeType: string | undefined = undefined;
if (this.renderingHandler.outputDisplayOrder?.userOrder || this._parsedDisplayOrder.length > 0) {
richestMimeType = this.findRichestMimeType(output);
}
let transformedOutput: vscode.CellOutput | undefined = undefined;
if (richestMimeType) {
let handler = this.renderingHandler.findBestMatchedRenderer(richestMimeType);
if (handler) {
renderers.add(handler.handle);
transformedOutput = handler?.render(this, cell, output);
output = transformedOutput;
output.pickedMimeType = richestMimeType;
}
}
return output;
});
}
return {
handle: cell.handle,
source: cell.source,
language: cell.language,
cell_type: cell.cell_type,
outputs: outputs,
isDirty: false
};
}
);
return await this._proxy.$updateNotebookCells(this.viewType, this.uri, cellDtos, Array.from(renderers));
}
findRichestMimeType(output: vscode.CellOutput) {
if (output.output_type === 'display_data' || output.output_type === 'execute_result') {
let mimeTypes = Object.keys(output.data);
@@ -476,7 +532,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
this._editors.set(URI.revive(uri).toString(), editor);
await provider.provider.resolveNotebook(editor);
await editor.document.$updateCells();
// await editor.document.$updateCells();
return editor.document.handle;
}
@@ -514,8 +570,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
source: rawCell.source,
language: rawCell.language,
cell_type: rawCell.cell_type,
outputs: rawCell.outputs,
isDirty: false
outputs: rawCell.outputs
};
}