Notebook Views Models (#13884)

* Add notebook editor

Introduce notebook editor component to allow for separate notebook displays in order to accomodate notebook views

* Localize notebook views configuration title

* Refactor view mode and remove the views configuration while it is unused

* Only fire view mode changed event when the value has been changed

* Remove notebook views contribution

* Add metadata capabilities

* Notebook views definitions

* Add notebook views models

* Views test

* Rename type arguments

* Additional tests

* Fix unused import

* Update resize cell test
This commit is contained in:
Daniel Grajeda
2021-01-05 16:28:30 -07:00
committed by GitHub
parent ab6d11596e
commit 16943c68c6
12 changed files with 867 additions and 0 deletions

View File

@@ -128,6 +128,15 @@ export class CellModel extends Disposable implements ICellModel {
return this._onCellModeChanged.event;
}
public set metadata(data: any) {
this._metadata = data;
this.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated);
}
public get metadata(): any {
return this._metadata;
}
public get isEditMode(): boolean {
return this._isEditMode;
}

View File

@@ -345,6 +345,16 @@ export interface INotebookModel {
*/
viewMode: ViewMode;
/**
* Add custom metadata values to the notebook
*/
setMetaValue(key: string, value: any);
/**
* Get a custom metadata value from the notebook
*/
getMetaValue(key: string): any;
/**
* Change the current kernel from the Kernel dropdown
* @param displayName kernel name (as displayed in Kernel dropdown)
@@ -476,6 +486,7 @@ export interface ICellModel {
source: string | string[];
cellType: CellType;
trustedMode: boolean;
metadata: any | undefined;
active: boolean;
hover: boolean;
executionCount: number | undefined;

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotebookModel, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts';
export class NotebookExtension<TNotebookMeta, TCellMeta> {
readonly version = 1;
readonly extensionName = 'azuredatastudio';
readonly extensionNamespace = 'extensions';
public getNotebookMetadata(notebook: INotebookModel): TNotebookMeta {
const metadata = notebook.getMetaValue(this.extensionNamespace) || {};
return metadata[this.extensionName] as TNotebookMeta;
}
public setNotebookMetadata(notebook: INotebookModel, metadata: TNotebookMeta) {
const meta = {};
meta[this.extensionName] = metadata;
notebook.setMetaValue(this.extensionNamespace, meta);
notebook.serializationStateChanged(NotebookChangeType.MetadataChanged);
}
public getCellMetadata(cell: ICellModel): TCellMeta {
const namespaceMeta = cell.metadata[this.extensionNamespace] || {};
return namespaceMeta[this.extensionName] as TCellMeta;
}
public setCellMetadata(cell: ICellModel, metadata: TCellMeta) {
const meta = {};
meta[this.extensionName] = metadata;
cell.metadata[this.extensionNamespace] = meta;
cell.sendChangeToNotebook(NotebookChangeType.CellsModified);
}
}

View File

@@ -284,6 +284,26 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._viewMode;
}
/**
* Add custom metadata values to the notebook
*/
public setMetaValue(key: string, value: any) {
this._existingMetadata[key] = value;
let changeInfo: NotebookContentChange = {
changeType: NotebookChangeType.MetadataChanged,
isDirty: true,
cells: [],
};
this._contentChangedEmitter.fire(changeInfo);
}
/**
* Get a custom metadata value from the notebook
*/
public getMetaValue(key: string): any {
return this._existingMetadata[key];
}
public set viewMode(mode: ViewMode) {
if (mode !== this._viewMode) {
this._viewMode = mode;

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { INotebookView, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { generateUuid } from 'vs/base/common/uuid';
export const DEFAULT_VIEW_CARD_HEIGHT = 4;
export const DEFAULT_VIEW_CARD_WIDTH = 12;
export class ViewNameTakenError extends Error { }
export class NotebookViewModel implements INotebookView {
private _onDeleted = new Emitter<INotebookView>();
public readonly guid: string;
public readonly onDeleted = this._onDeleted.event;
constructor(
protected _name: string,
private _notebookViews: NotebookViewsExtension
) {
this.guid = generateUuid();
}
public initialize(): void {
const cells = this._notebookViews.notebook.cells;
cells.forEach((cell, idx) => { this.initializeCell(cell, idx); });
}
protected initializeCell(cell: ICellModel, idx: number) {
let meta = this._notebookViews.getCellMetadata(cell);
if (!meta) {
this._notebookViews.initializeCell(cell);
meta = this._notebookViews.getCellMetadata(cell);
}
meta.views.push({
guid: this.guid,
hidden: false,
y: idx * DEFAULT_VIEW_CARD_HEIGHT,
x: 0,
});
}
public get name(): string {
return this._name;
}
public set name(name: string) {
if (this.name !== name && this._notebookViews.viewNameIsTaken(name)) {
throw new ViewNameTakenError(localize('notebookView.nameTaken', 'A view with the name {0} already exists in this notebook.', name));
}
this._name = name;
}
public nameAvailable(name: string): boolean {
return !this._notebookViews.viewNameIsTaken(name);
}
public getCellMetadata(cell: ICellModel): INotebookViewCell {
const meta = this._notebookViews.getCellMetadata(cell);
return meta?.views?.find(view => view.guid === this.guid);
}
public get hiddenCells(): Readonly<ICellModel[]> {
return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden);
}
public get cells(): Readonly<ICellModel[]> {
return this._notebookViews.notebook.cells;
}
public getCell(guid: string): Readonly<ICellModel> {
return this._notebookViews.notebook.cells.find(cell => cell.cellGuid === guid);
}
public insertCell(cell: ICellModel) {
this._notebookViews.updateCell(cell, this, { hidden: false });
}
public hideCell(cell: ICellModel) {
this._notebookViews.updateCell(cell, this, { hidden: true });
}
public moveCell(cell: ICellModel, x: number, y: number) {
this._notebookViews.updateCell(cell, this, { x, y });
}
public resizeCell(cell: ICellModel, width: number, height: number) {
this._notebookViews.updateCell(cell, this, { width, height });
}
public save() {
this._notebookViews.commit();
}
public delete() {
this._notebookViews.removeView(this.guid);
this._onDeleted.fire(this);
}
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { Event } from 'vs/base/common/event';
export type CellChangeEventType = 'hide' | 'insert' | 'active';
export type CellChangeEvent = {
cell: ICellModel,
event: CellChangeEventType
};
export interface INotebookView {
readonly guid: string;
readonly onDeleted: Event<INotebookView>;
cells: Readonly<ICellModel[]>;
hiddenCells: Readonly<ICellModel[]>;
name: string;
initialize(): void;
nameAvailable(name: string): boolean;
getCellMetadata(cell: ICellModel): INotebookViewCell;
hideCell(cell: ICellModel): void;
moveCell(cell: ICellModel, x: number, y: number): void;
resizeCell(cell: ICellModel, width: number, height: number): void;
getCell(guid: string): Readonly<ICellModel>;
insertCell(cell: ICellModel): void;
save(): void;
delete(): void;
}
export interface INotebookViewCell {
readonly guid?: string;
hidden?: boolean;
x?: number;
y?: number;
width?: number;
height?: number;
}
/*
* Represents the metadata that will be stored for the
* view at the notebook level.
*/
export interface INotebookViewMetadata {
version: number;
activeView: string;
views: INotebookView[];
}
/*
* Represents the metadata that will be stored for the
* view at the cell level.
*/
export interface INotebookViewCellMetadata {
views: INotebookViewCell[];
}

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotebookModel, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { generateUuid } from 'vs/base/common/uuid';
import { Emitter, Event } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { NotebookViewModel } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
import { NotebookExtension } from 'sql/workbench/services/notebook/browser/models/notebookExtension';
import { INotebookView, INotebookViewCell, INotebookViewCellMetadata, INotebookViewMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetadata, INotebookViewCellMetadata> {
static readonly defaultViewName = localize('notebookView.untitledView', "Untitled View");
readonly maxNameIterationAttempts = 100;
readonly extension = 'azuredatastudio';
readonly version = 1;
protected _metadata: INotebookViewMetadata;
private _onViewDeleted = new Emitter<void>();
constructor(protected _notebook: INotebookModel) {
super();
this.loadOrInitialize();
}
public loadOrInitialize() {
this._metadata = this.getNotebookMetadata(this._notebook);
if (!this._metadata) {
this.initializeNotebook();
this.initializeCells();
this.commit();
}
}
protected initializeNotebook() {
this._metadata = {
version: this.version,
activeView: undefined,
views: []
};
}
protected initializeCells() {
const cells = this._notebook.cells;
cells.forEach((cell) => {
this.initializeCell(cell);
});
}
public initializeCell(cell: ICellModel) {
const meta: INotebookViewCellMetadata = {
views: []
};
this.setCellMetadata(cell, meta);
}
public createNewView(name?: string): INotebookView {
const viewName = name || this.generateDefaultViewName();
const view = new NotebookViewModel(viewName, this);
view.initialize();
this._metadata.views.push(view);
return view;
}
public removeView(guid: string) {
let viewToRemove = this._metadata.views.findIndex(view => view.guid === guid);
if (viewToRemove !== -1) {
let removedView = this._metadata.views.splice(viewToRemove, 1);
// Remove view data for each cell
if (removedView.length) {
this._notebook?.cells.forEach((cell) => {
let meta = this.getCellMetadata(cell);
meta.views.splice(viewToRemove, 1);
this.setCellMetadata(cell, meta);
});
}
this.setNotebookMetadata(this.notebook, this._metadata);
}
if (guid === this._metadata.activeView) {
this._metadata.activeView = undefined;
}
this._onViewDeleted.fire();
this.commit();
}
public generateDefaultViewName(): string {
let i = 1;
let name = NotebookViewsExtension.defaultViewName;
while (this.viewNameIsTaken(name) && i <= this.maxNameIterationAttempts) {
name = `${NotebookViewsExtension.defaultViewName} ${i++}`;
}
return i <= this.maxNameIterationAttempts ? name : generateUuid();
}
public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCell, override: boolean = false) {
const cellMetadata = this.getCellMetadata(cell);
const viewToUpdate = cellMetadata.views.findIndex(view => view.guid === currentView.guid);
if (viewToUpdate >= 0) {
cellMetadata.views[viewToUpdate] = override ? cellData : { ...cellMetadata.views[viewToUpdate], ...cellData };
this.setCellMetadata(cell, cellMetadata);
}
}
public get notebook(): INotebookModel {
return this._notebook;
}
public getViews(): INotebookView[] {
return this._metadata.views;
}
public getCells(): INotebookViewCellMetadata[] {
return this._notebook.cells.map(cell => this.getCellMetadata(cell));
}
public getActiveView(): INotebookView {
return this.getViews().find(view => view.guid === this._metadata.activeView);
}
public setActiveView(view: INotebookView) {
this._metadata.activeView = view.guid;
}
public commit() {
this.setNotebookMetadata(this._notebook, this._metadata);
}
public viewNameIsTaken(name: string): boolean {
return !!this.getViews().find(v => v.name.toLowerCase() === name.toLowerCase());
}
public get onViewDeleted(): Event<void> {
return this._onViewDeleted.event;
}
}