Notebook Performance Improvements to Cell Editing/Output Changes/Execution Count Changes (#6867)

* edit perf

* Save multiline source in notebooks

* More merges

* Single, multi line works needs work

* Works with single + multi and recomputes active

* Actual perf improvements this time

* code cleanup

* Calculating output position on the fly

* Hmm can we use brackets to make this simpler?

* monday progress

* output working. lots of improvements.

* First tests working

* 10 tests now, fixed bugs

* Cleanup, add output test

* More fixes

* Need to still fix execution count bug

* Tests pass, added comments

* Cleanup

* PR comments round 1

* Deal with merge issues from master, layering

* Deleting duplicate file

* More PR Comments

* PR Comments
This commit is contained in:
Chris LaFreniere
2019-08-26 10:17:58 -07:00
committed by GitHub
parent 4afa282ef9
commit 84b3e876d7
19 changed files with 1157 additions and 73 deletions

View File

@@ -0,0 +1,19 @@
{
"comments": {
"lineComment": "//",
"blockComment": [ "/*", "*/" ]
},
"brackets": [
["{", "}"],
["[", "]"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}", "notIn": ["string"] },
{ "open": "[", "close": "]", "notIn": ["string"] },
{ "open": "(", "close": ")", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string"] },
{ "open": "/*", "close": "*/", "notIn": ["string"] },
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] }
]
}

View File

@@ -149,7 +149,8 @@
], ],
"aliases": [ "aliases": [
"Notebook" "Notebook"
] ],
"configuration": "./language-configuration.json"
} }
], ],
"menus": { "menus": {

1
src/sql/azdata.d.ts vendored
View File

@@ -4448,6 +4448,7 @@ declare module 'azdata' {
source: string | string[]; source: string | string[];
metadata?: { metadata?: {
language?: string; language?: string;
azdata_cell_guid?: string;
}; };
execution_count?: number; execution_count?: number;
outputs?: ICellOutput[]; outputs?: ICellOutput[];

View File

@@ -626,6 +626,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellOutputUpdated:
case NotebookChangeType.CellSourceUpdated: case NotebookChangeType.CellSourceUpdated:
case NotebookChangeType.DirtyStateChanged: case NotebookChangeType.DirtyStateChanged:
case NotebookChangeType.CellOutputCleared:
return NotebookChangeKind.ContentUpdated; return NotebookChangeKind.ContentUpdated;
case NotebookChangeType.KernelChanged: case NotebookChangeType.KernelChanged:
case NotebookChangeType.TrustChanged: case NotebookChangeType.TrustChanged:
@@ -654,7 +655,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
cell_type: cell.cellType, cell_type: cell.cellType,
execution_count: cell.executionCount, execution_count: cell.executionCount,
metadata: { metadata: {
language: cell.language language: cell.language,
azdata_cell_guid: cell.cellGuid
}, },
source: undefined, source: undefined,
outputs: [...cell.outputs] outputs: [...cell.outputs]
@@ -669,7 +671,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
cell_type: cells.cellType, cell_type: cells.cellType,
execution_count: undefined, execution_count: undefined,
metadata: { metadata: {
language: cells.language language: cells.language,
azdata_cell_guid: cells.cellGuid
}, },
source: undefined source: undefined
} }

View File

@@ -221,6 +221,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
this._register(this._editorInput); this._register(this._editorInput);
this._register(this._editorModel.onDidChangeContent(e => { this._register(this._editorModel.onDidChangeContent(e => {
this._editor.setHeightToScrollHeight(); this._editor.setHeightToScrollHeight();
this.cellModel.modelContentChangedEvent = e;
this.cellModel.source = this._editorModel.getValue(); this.cellModel.source = this._editorModel.getValue();
this.onContentChanged.emit(); this.onContentChanged.emit();
this.checkForLanguageMagics(); this.checkForLanguageMagics();

View File

@@ -30,7 +30,7 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges {
@HostListener('document:keydown.escape', ['$event']) @HostListener('document:keydown.escape', ['$event'])
handleKeyboardEvent() { handleKeyboardEvent() {
this.cellModel.active = false; this.cellModel.active = false;
this._model.activeCell = undefined; this._model.updateActiveCell(undefined);
} }
private _model: NotebookModel; private _model: NotebookModel;

View File

@@ -16,7 +16,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces'; import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { Range } from 'vs/editor/common/core/range';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { ITextFileService, ISaveOptions, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextFileService, ISaveOptions, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
@@ -26,12 +25,17 @@ import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorIn
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts'; import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { Deferred } from 'sql/base/common/promise';
import { NotebookTextFileModel } from 'sql/workbench/parts/notebook/common/models/notebookTextFileModel';
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>; export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
export class NotebookEditorModel extends EditorModel { export class NotebookEditorModel extends EditorModel {
private dirty: boolean; private _dirty: boolean;
private _changeEventsHookedUp: boolean = false;
private _notebookTextFileModel: NotebookTextFileModel = new NotebookTextFileModel();
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>()); private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
private _lastEditFullReplacement: boolean;
constructor(public readonly notebookUri: URI, constructor(public readonly notebookUri: URI,
private textEditorModel: TextFileEditorModel | UntitledEditorModel, private textEditorModel: TextFileEditorModel | UntitledEditorModel,
@INotebookService private notebookService: INotebookService, @INotebookService private notebookService: INotebookService,
@@ -41,9 +45,17 @@ export class NotebookEditorModel extends EditorModel {
this._register(this.notebookService.onNotebookEditorAdd(notebook => { this._register(this.notebookService.onNotebookEditorAdd(notebook => {
if (notebook.id === this.notebookUri.toString()) { if (notebook.id === this.notebookUri.toString()) {
// Hook to content change events // Hook to content change events
notebook.modelReady.then(() => { notebook.modelReady.then((model) => {
this._register(notebook.model.kernelChanged(e => this.updateModel())); if (!this._changeEventsHookedUp) {
this._register(notebook.model.contentChanged(e => this.updateModel(e))); this._changeEventsHookedUp = true;
this._register(model.kernelChanged(e => this.updateModel(undefined, NotebookChangeType.KernelChanged)));
this._register(model.contentChanged(e => this.updateModel(e, e.changeType)));
this._register(notebook.model.onActiveCellChanged((cell) => {
if (cell) {
this._notebookTextFileModel.activeCellGuid = cell.cellGuid;
}
}));
}
}, err => undefined); }, err => undefined);
} }
})); }));
@@ -58,7 +70,7 @@ export class NotebookEditorModel extends EditorModel {
} }
})); }));
} }
this.dirty = this.textEditorModel.isDirty(); this._dirty = this.textEditorModel.isDirty();
} }
public get contentString(): string { public get contentString(): string {
@@ -66,15 +78,19 @@ export class NotebookEditorModel extends EditorModel {
return model.getValue(); return model.getValue();
} }
public get lastEditFullReplacement(): boolean {
return this._lastEditFullReplacement;
}
isDirty(): boolean { isDirty(): boolean {
return this.textEditorModel.isDirty(); return this.textEditorModel.isDirty();
} }
public setDirty(dirty: boolean): void { public setDirty(dirty: boolean): void {
if (this.dirty === dirty) { if (this._dirty === dirty) {
return; return;
} }
this.dirty = dirty; this._dirty = dirty;
this._onDidChangeDirty.fire(); this._onDidChangeDirty.fire();
} }
@@ -92,7 +108,8 @@ export class NotebookEditorModel extends EditorModel {
} }
} }
public updateModel(contentChange?: NotebookContentChange): void { public updateModel(contentChange?: NotebookContentChange, type?: NotebookChangeType): void {
this._lastEditFullReplacement = false;
if (contentChange && contentChange.changeType === NotebookChangeType.Saved) { if (contentChange && contentChange.changeType === NotebookChangeType.Saved) {
// We send the saved events out, so ignore. Otherwise we double-count this as a change // We send the saved events out, so ignore. Otherwise we double-count this as a change
// and cause the text to be reapplied // and cause the text to be reapplied
@@ -104,22 +121,42 @@ export class NotebookEditorModel extends EditorModel {
// Request serialization so trusted state is preserved but don't update the model // Request serialization so trusted state is preserved but don't update the model
this.sendNotebookSerializationStateChange(); this.sendNotebookSerializationStateChange();
} else { } else {
// For all other changes, update the backing model with the latest contents
let notebookModel = this.getNotebookModel(); let notebookModel = this.getNotebookModel();
let editAppliedSuccessfully = false;
if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) { if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) {
let content = JSON.stringify(notebookModel.toJSON(), undefined, ' '); if (contentChange && contentChange.cells && contentChange.cells[0]) {
let model = this.textEditorModel.textEditorModel; if (type === NotebookChangeType.CellSourceUpdated) {
let endLine = model.getLineCount(); if (this._notebookTextFileModel.transformAndApplyEditForSourceUpdate(contentChange, this.textEditorModel)) {
let endCol = model.getLineMaxColumn(endLine); editAppliedSuccessfully = true;
}
this.textEditorModel.textEditorModel.applyEdits([{ } else if (type === NotebookChangeType.CellOutputUpdated) {
range: new Range(1, 1, endLine, endCol), if (this._notebookTextFileModel.transformAndApplyEditForOutputUpdate(contentChange, this.textEditorModel)) {
text: content editAppliedSuccessfully = true;
}]); }
} else if (type === NotebookChangeType.CellOutputCleared) {
if (this._notebookTextFileModel.transformAndApplyEditForClearOutput(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
} else if (type === NotebookChangeType.CellExecuted) {
if (this._notebookTextFileModel.transformAndApplyEditForCellUpdated(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
}
}
// If edit was already applied, skip replacing entire text model
if (editAppliedSuccessfully) {
return;
}
this.replaceEntireTextEditorModel(notebookModel, type);
this._lastEditFullReplacement = true;
} }
} }
} }
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType) {
this._notebookTextFileModel.replaceEntireTextEditorModel(notebookModel, type, this.textEditorModel);
}
private sendNotebookSerializationStateChange(): void { private sendNotebookSerializationStateChange(): void {
let notebookModel = this.getNotebookModel(); let notebookModel = this.getNotebookModel();
if (notebookModel) { if (notebookModel) {
@@ -142,6 +179,10 @@ export class NotebookEditorModel extends EditorModel {
get onDidChangeDirty(): Event<void> { get onDidChangeDirty(): Event<void> {
return this._onDidChangeDirty.event; return this._onDidChangeDirty.event;
} }
get editorModel() {
return this.textEditorModel;
}
} }
export class NotebookInput extends EditorInput { export class NotebookInput extends EditorInput {
@@ -161,6 +202,8 @@ export class NotebookInput extends EditorInput {
private _providersLoaded: Promise<void>; private _providersLoaded: Promise<void>;
private _dirtyListener: IDisposable; private _dirtyListener: IDisposable;
private _notebookEditorOpenedTimestamp: number; private _notebookEditorOpenedTimestamp: number;
private _modelResolveInProgress: boolean = false;
private _modelResolved: Deferred<void> = new Deferred<void>();
constructor(private _title: string, constructor(private _title: string,
private resource: URI, private resource: URI,
@@ -283,6 +326,12 @@ export class NotebookInput extends EditorInput {
} }
async resolve(): Promise<NotebookEditorModel> { async resolve(): Promise<NotebookEditorModel> {
if (!this._modelResolveInProgress) {
this._modelResolveInProgress = true;
} else {
await this._modelResolved;
return this._model;
}
if (this._model) { if (this._model) {
return Promise.resolve(this._model); return Promise.resolve(this._model);
} else { } else {
@@ -296,6 +345,7 @@ export class NotebookInput extends EditorInput {
} }
this._model = this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel); this._model = this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel);
this.hookDirtyListener(this._model.onDidChangeDirty, () => this._onDidChangeDirty.fire()); this.hookDirtyListener(this._model.onDidChangeDirty, () => this._onDidChangeDirty.fire());
this._modelResolved.resolve();
return this._model; return this._model;
} }
} }

View File

@@ -176,14 +176,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
} }
if (cell !== this.model.activeCell) { this.model.updateActiveCell(cell);
if (this.model.activeCell) { this.detectChanges();
this.model.activeCell.active = false;
}
this._model.activeCell = cell;
this._model.activeCell.active = true;
this.detectChanges();
}
} }
//Saves scrollTop value on scroll change //Saves scrollTop value on scroll change
@@ -192,10 +186,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
} }
public unselectActiveCell() { public unselectActiveCell() {
if (this.model && this.model.activeCell) { this.model.updateActiveCell(undefined);
this.model.activeCell.active = false;
this.model.activeCell = undefined;
}
this.detectChanges(); this.detectChanges();
} }
@@ -311,10 +302,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp
}, this.profile, this.logService, this.notificationService, this.telemetryService); }, this.profile, this.logService, this.notificationService, this.telemetryService);
let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty()); let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty());
model.onError((errInfo: INotification) => this.handleModelError(errInfo)); this._register(model.onError((errInfo: INotification) => this.handleModelError(errInfo)));
model.contentChanged((change) => this.handleContentChanged(change)); this._register(model.contentChanged((change) => this.handleContentChanged()));
model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider)); this._register(model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider)));
model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs)); this._register(model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs)));
this._model = this._register(model); this._model = this._register(model);
await this._model.loadContents(trusted); await this._model.loadContents(trusted);
this.setLoading(false); this.setLoading(false);
@@ -382,7 +373,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this.notificationService.notify(notification); this.notificationService.notify(notification);
} }
private handleContentChanged(change: NotebookContentChange) { private handleContentChanged() {
// Note: for now we just need to set dirty state and refresh the UI. // Note: for now we just need to set dirty state and refresh the UI.
this.detectChanges(); this.detectChanges();
} }

View File

@@ -20,12 +20,15 @@ import { Schemas } from 'vs/base/common/network';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService'; import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { optional } from 'vs/platform/instantiation/common/instantiation'; import { optional } from 'vs/platform/instantiation/common/instantiation';
import { getErrorMessage } from 'vs/base/common/errors'; import { getErrorMessage } from 'vs/base/common/errors';
import { generateUuid } from 'vs/base/common/uuid';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
let modelId = 0; let modelId = 0;
export class CellModel implements ICellModel { export class CellModel implements ICellModel {
private _cellType: nb.CellType; private _cellType: nb.CellType;
private _source: string | string[]; private _source: string | string[];
private _language: string; private _language: string;
private _cellGuid: string;
private _future: FutureInternal; private _future: FutureInternal;
private _outputs: nb.ICellOutput[] = []; private _outputs: nb.ICellOutput[] = [];
private _isEditMode: boolean; private _isEditMode: boolean;
@@ -43,7 +46,8 @@ export class CellModel implements ICellModel {
private _onCellLoaded = new Emitter<string>(); private _onCellLoaded = new Emitter<string>();
private _loaded: boolean; private _loaded: boolean;
private _stdInVisible: boolean; private _stdInVisible: boolean;
private _metadata: { language?: string; }; private _metadata: { language?: string, cellGuid?: string; };
private _modelContentChangedEvent: IModelContentChangedEvent;
constructor(cellData: nb.ICellContents, constructor(cellData: nb.ICellContents,
private _options: ICellModelOptions, private _options: ICellModelOptions,
@@ -64,6 +68,8 @@ export class CellModel implements ICellModel {
} else { } else {
this._isTrusted = false; this._isTrusted = false;
} }
// if the fromJson() method was already called and _cellGuid was previously set, don't generate another UUID unnecessarily
this._cellGuid = this._cellGuid || generateUuid();
this.createUri(); this.createUri();
} }
@@ -165,6 +171,15 @@ export class CellModel implements ICellModel {
this._source = newSource; this._source = newSource;
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated); this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
} }
this._modelContentChangedEvent = undefined;
}
public get modelContentChangedEvent(): IModelContentChangedEvent {
return this._modelContentChangedEvent;
}
public set modelContentChangedEvent(e: IModelContentChangedEvent) {
this._modelContentChangedEvent = e;
} }
public get language(): string { public get language(): string {
@@ -177,6 +192,10 @@ export class CellModel implements ICellModel {
return this.options.notebook.language; return this.options.notebook.language;
} }
public get cellGuid(): string {
return this._cellGuid;
}
public setOverrideLanguage(newLanguage: string) { public setOverrideLanguage(newLanguage: string) {
this._language = newLanguage; this._language = newLanguage;
} }
@@ -214,7 +233,7 @@ export class CellModel implements ICellModel {
private notifyExecutionComplete(): void { private notifyExecutionComplete(): void {
if (this._notebookService) { if (this._notebookService) {
this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted); this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted, this);
} }
} }
@@ -232,11 +251,8 @@ export class CellModel implements ICellModel {
public async runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean> { public async runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean> {
try { try {
if (!this.active && this !== this.notebookModel.activeCell) { if (!this.active && this !== this.notebookModel.activeCell) {
if (this.notebookModel.activeCell) { this.notebookModel.updateActiveCell(this);
this.notebookModel.activeCell.active = false;
}
this.active = true; this.active = true;
this.notebookModel.activeCell = this;
} }
if (connectionManagementService) { if (connectionManagementService) {
@@ -266,7 +282,7 @@ export class CellModel implements ICellModel {
} }
} }
let content = this.source; let content = this.source;
if (content) { if ((Array.isArray(content) && content.length > 0) || (!Array.isArray(content) && content)) {
// requestExecute expects a string for the code parameter // requestExecute expects a string for the code parameter
content = Array.isArray(content) ? content.join('') : content; content = Array.isArray(content) ? content.join('') : content;
let future = await kernel.requestExecute({ let future = await kernel.requestExecute({
@@ -369,7 +385,11 @@ export class CellModel implements ICellModel {
shouldScroll: !!shouldScroll shouldScroll: !!shouldScroll
}; };
this._onOutputsChanged.fire(outputEvent); this._onOutputsChanged.fire(outputEvent);
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated); if (this.outputs.length !== 0) {
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated);
} else {
this.sendChangeToNotebook(NotebookChangeType.CellOutputCleared);
}
} }
private sendChangeToNotebook(change: NotebookChangeType): void { private sendChangeToNotebook(change: NotebookChangeType): void {
@@ -521,9 +541,10 @@ export class CellModel implements ICellModel {
source: this._source, source: this._source,
metadata: this._metadata || {} metadata: this._metadata || {}
}; };
cellJson.metadata.azdata_cell_guid = this._cellGuid;
if (this._cellType === CellTypes.Code) { if (this._cellType === CellTypes.Code) {
cellJson.metadata.language = this._language, cellJson.metadata.language = this._language;
cellJson.outputs = this._outputs; cellJson.outputs = this._outputs;
cellJson.execution_count = this.executionCount ? this.executionCount : 0; cellJson.execution_count = this.executionCount ? this.executionCount : 0;
} }
return cellJson as nb.ICellContents; return cellJson as nb.ICellContents;
@@ -537,6 +558,7 @@ export class CellModel implements ICellModel {
this.executionCount = cell.execution_count; this.executionCount = cell.execution_count;
this._source = this.getMultilineSource(cell.source); this._source = this.getMultilineSource(cell.source);
this._metadata = cell.metadata; this._metadata = cell.metadata;
this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid();
this.setLanguageFromContents(cell); this.setLanguageFromContents(cell);
if (cell.outputs) { if (cell.outputs) {
for (let output of cell.outputs) { for (let output of cell.outputs) {
@@ -600,8 +622,10 @@ export class CellModel implements ICellModel {
if (typeof source === 'string') { if (typeof source === 'string') {
let sourceMultiline = source.split('\n'); let sourceMultiline = source.split('\n');
// If source is one line (i.e. no '\n'), return it immediately // If source is one line (i.e. no '\n'), return it immediately
if (sourceMultiline.length <= 1) { if (sourceMultiline.length === 1) {
return source; return [source];
} else if (sourceMultiline.length === 0) {
return [];
} }
// Otherwise, add back all of the newlines here // Otherwise, add back all of the newlines here
// Note: for Windows machines that require '/r/n', // Note: for Windows machines that require '/r/n',

View File

@@ -44,5 +44,6 @@ export enum NotebookChangeType {
KernelChanged, KernelChanged,
TrustChanged, TrustChanged,
Saved, Saved,
CellExecuted CellExecuted,
CellOutputCleared
} }

View File

@@ -22,6 +22,7 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel'; import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { mssqlProviderName } from 'sql/platform/connection/common/constants'; import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
export interface IClientSessionOptions { export interface IClientSessionOptions {
notebookUri: URI; notebookUri: URI;
@@ -338,6 +339,11 @@ export interface INotebookModel {
*/ */
readonly onProviderIdChange: Event<string>; readonly onProviderIdChange: Event<string>;
/**
* Event fired on active cell change
*/
readonly onActiveCellChanged: Event<ICellModel>;
/** /**
* The trusted mode of the Notebook * The trusted mode of the Notebook
*/ */
@@ -377,7 +383,7 @@ export interface INotebookModel {
/** /**
* Serialize notebook cell content to JSON * Serialize notebook cell content to JSON
*/ */
toJSON(): nb.INotebookContents; toJSON(type?: NotebookChangeType): nb.INotebookContents;
/** /**
* Notifies the notebook of a change in the cell * Notifies the notebook of a change in the cell
@@ -403,9 +409,15 @@ export interface INotebookModel {
/** Event fired once we get call back from ConfigureConnection method in sqlops extension */ /** Event fired once we get call back from ConfigureConnection method in sqlops extension */
readonly onValidConnectionSelected: Event<boolean>; readonly onValidConnectionSelected: Event<boolean>;
serializationStateChanged(changeType: NotebookChangeType): void; serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void;
standardKernels: IStandardKernelWithProvider[]; standardKernels: IStandardKernelWithProvider[];
/**
* Updates the model's view of an active cell to the new active cell
* @param cell New active cell
*/
updateActiveCell(cell: ICellModel);
} }
export interface NotebookContentChange { export interface NotebookContentChange {
@@ -426,6 +438,11 @@ export interface NotebookContentChange {
* Optional value indicating if the notebook is in a dirty or clean state after this change * Optional value indicating if the notebook is in a dirty or clean state after this change
*/ */
isDirty?: boolean; isDirty?: boolean;
/**
* Text content changed event for cell edits
*/
modelContentChangedEvent?: IModelContentChangedEvent;
} }
export interface ICellModelOptions { export interface ICellModelOptions {
@@ -449,6 +466,7 @@ export interface ICellModel {
cellUri: URI; cellUri: URI;
id: string; id: string;
readonly language: string; readonly language: string;
readonly cellGuid: string;
source: string | string[]; source: string | string[];
cellType: CellType; cellType: CellType;
trustedMode: boolean; trustedMode: boolean;
@@ -470,6 +488,7 @@ export interface ICellModel {
loaded: boolean; loaded: boolean;
stdInVisible: boolean; stdInVisible: boolean;
readonly onLoaded: Event<string>; readonly onLoaded: Event<string>;
modelContentChangedEvent: IModelContentChangedEvent;
} }
export interface FutureInternal extends nb.IFuture { export interface FutureInternal extends nb.IFuture {
@@ -533,3 +552,10 @@ export namespace notebookConstants {
display_name: sqlKernel display_name: sqlKernel
}); });
} }
export interface INotebookContentsEditable {
cells: nb.ICellContents[];
metadata: nb.INotebookMetadata;
nbformat: number;
nbformat_minor: number;
}

View File

@@ -9,7 +9,7 @@ import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, NotebookContentChange, notebookConstants } from 'sql/workbench/parts/notebook/common/models/modelInterfaces'; import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, NotebookContentChange, notebookConstants, INotebookContentsEditable } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookChangeType, CellType, CellTypes } from 'sql/workbench/parts/notebook/common/models/contracts'; import { NotebookChangeType, CellType, CellTypes } from 'sql/workbench/parts/notebook/common/models/contracts';
import { nbversion } from 'sql/workbench/parts/notebook/common/models/notebookConstants'; import { nbversion } from 'sql/workbench/parts/notebook/common/models/notebookConstants';
import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils'; import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils';
@@ -56,6 +56,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
private _onProviderIdChanged = new Emitter<string>(); private _onProviderIdChanged = new Emitter<string>();
private _activeContexts: IDefaultConnection; private _activeContexts: IDefaultConnection;
private _trustedMode: boolean; private _trustedMode: boolean;
private _onActiveCellChanged = new Emitter<ICellModel>();
private _cells: ICellModel[]; private _cells: ICellModel[];
private _defaultLanguageInfo: nb.ILanguageInfo; private _defaultLanguageInfo: nb.ILanguageInfo;
@@ -268,6 +269,10 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._onValidConnectionSelected.event; return this._onValidConnectionSelected.event;
} }
public get onActiveCellChanged(): Event<ICellModel> {
return this._onActiveCellChanged.event;
}
public get standardKernels(): notebookUtils.IStandardKernelWithProvider[] { public get standardKernels(): notebookUtils.IStandardKernelWithProvider[] {
return this._standardKernels; return this._standardKernels;
} }
@@ -359,12 +364,15 @@ export class NotebookModel extends Disposable implements INotebookModel {
return cell; return cell;
} }
private updateActiveCell(cell: ICellModel) { public updateActiveCell(cell: ICellModel) {
if (this._activeCell) { if (this._activeCell) {
this._activeCell.active = false; this._activeCell.active = false;
} }
this._activeCell = cell; this._activeCell = cell;
this._activeCell.active = true; if (cell) {
this._activeCell.active = true;
}
this._onActiveCellChanged.fire(cell);
} }
private createCell(cellType: CellType): ICellModel { private createCell(cellType: CellType): ICellModel {
@@ -999,8 +1007,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
switch (change) { switch (change) {
case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellOutputUpdated:
case NotebookChangeType.CellSourceUpdated: case NotebookChangeType.CellSourceUpdated:
changeInfo.changeType = NotebookChangeType.DirtyStateChanged;
changeInfo.isDirty = true; changeInfo.isDirty = true;
changeInfo.modelContentChangedEvent = cell.modelContentChangedEvent;
break; break;
default: default:
// Do nothing for now // Do nothing for now
@@ -1008,10 +1016,10 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._contentChangedEmitter.fire(changeInfo); this._contentChangedEmitter.fire(changeInfo);
} }
serializationStateChanged(changeType: NotebookChangeType): void { serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void {
let changeInfo: NotebookContentChange = { let changeInfo: NotebookContentChange = {
changeType: changeType, changeType: changeType,
cells: undefined cells: [cell]
}; };
this._contentChangedEmitter.fire(changeInfo); this._contentChangedEmitter.fire(changeInfo);

View File

@@ -0,0 +1,258 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range, IRange } from 'vs/editor/common/core/range';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { FindMatch } from 'vs/editor/common/model';
import { NotebookContentChange, INotebookModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
export class NotebookTextFileModel {
// save active cell's line/column in editor model for the beginning of the source property
private _sourceBeginRange: Range;
// save active cell's line/column in editor model for the beginning of the output property
private _outputBeginRange: Range;
// save active cell guid
private _activeCellGuid: string;
constructor() {
}
public get activeCellGuid(): string {
return this._activeCellGuid;
}
public set activeCellGuid(guid: string) {
if (this._activeCellGuid !== guid) {
this._sourceBeginRange = undefined;
this._outputBeginRange = undefined;
this._activeCellGuid = guid;
}
}
public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
// convert the range to leverage offsets in the json
if (contentChange && contentChange.modelContentChangedEvent && areRangePropertiesPopulated(cellGuidRange)) {
contentChange.modelContentChangedEvent.changes.forEach(change => {
let convertedRange: IRange = {
startLineNumber: change.range.startLineNumber + cellGuidRange.startLineNumber - 1,
endLineNumber: change.range.endLineNumber + cellGuidRange.startLineNumber - 1,
startColumn: change.range.startColumn + cellGuidRange.startColumn,
endColumn: change.range.endColumn + cellGuidRange.startColumn
};
// Need to subtract one because we're going from 1-based to 0-based
let startSpaces: string = ' '.repeat(cellGuidRange.startColumn - 1);
// The text here transforms a string from 'This is a string\n this is another string' to:
// This is a string
// this is another string
textEditorModel.textEditorModel.applyEdits([{
range: new Range(convertedRange.startLineNumber, convertedRange.startColumn, convertedRange.endLineNumber, convertedRange.endColumn),
text: change.text.split('\n').join('\\n\",\n'.concat(startSpaces).concat('\"'))
}]);
});
} else {
return false;
}
return true;
}
public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) {
let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' ');
if (contentChange.cells[0].outputs.length > 1) {
newOutput = ', '.concat(newOutput);
} else {
newOutput = '\n'.concat(newOutput).concat('\n');
}
let range = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
if (range) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn),
text: newOutput
}]);
}
} else {
return false;
}
return true;
}
public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
if (executionCountMatch && executionCountMatch.range) {
// Execution count can be between 0 and n characters long
let beginExecutionCountColumn = executionCountMatch.range.endColumn;
let endExecutionCountColumn = beginExecutionCountColumn + 1;
let lineContent = textEditorModel.textEditorModel.getLineContent(executionCountMatch.range.endLineNumber);
while (lineContent[endExecutionCountColumn - 1]) {
endExecutionCountColumn++;
}
if (contentChange.cells[0].executionCount) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(executionCountMatch.range.startLineNumber, beginExecutionCountColumn, executionCountMatch.range.endLineNumber, endExecutionCountColumn),
text: contentChange.cells[0].executionCount.toString()
}]);
} else {
// This is a special case when cells are canceled; there will be no execution count included
return true;
}
} else {
return false;
}
return true;
}
public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) {
return false;
}
if (!this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid)) {
this.updateOutputBeginRange(textEditorModel, contentChange.cells[0].cellGuid);
}
let outputEndRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
let outputStartRange = this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
if (outputStartRange && outputEndRange) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(outputStartRange.startLineNumber, outputStartRange.endColumn, outputEndRange.endLineNumber, outputEndRange.endColumn),
text: ''
}]);
return true;
}
return false;
}
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: TextFileEditorModel | UntitledEditorModel) {
let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' ');
let model = textEditorModel.textEditorModel;
let endLine = model.getLineCount();
let endCol = model.getLineMaxColumn(endLine);
textEditorModel.textEditorModel.applyEdits([{
range: new Range(1, 1, endLine, endCol),
text: content
}]);
}
// Find the beginning of a cell's source in the text editor model
private updateSourceBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
if (!cellGuid) {
return;
}
this._sourceBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let sourceBefore = textEditorModel.textEditorModel.findPreviousMatch('"source": [', { lineNumber: cellGuidMatches[0].range.startLineNumber, column: cellGuidMatches[0].range.startColumn }, false, true, undefined, true);
if (!sourceBefore || !sourceBefore.range) {
return;
}
let firstQuoteOfSource = textEditorModel.textEditorModel.findNextMatch('"', { lineNumber: sourceBefore.range.startLineNumber, column: sourceBefore.range.endColumn }, false, true, undefined, true);
this._sourceBeginRange = firstQuoteOfSource.range;
} else {
return;
}
}
// Find the beginning of a cell's outputs in the text editor model
private updateOutputBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
if (!cellGuid) {
return undefined;
}
this._outputBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let outputsBegin = textEditorModel.textEditorModel.findNextMatch('"outputs": [', { lineNumber: cellGuidMatches[0].range.endLineNumber, column: cellGuidMatches[0].range.endColumn }, false, true, undefined, true);
if (!outputsBegin || !outputsBegin.range) {
return undefined;
}
this._outputBeginRange = outputsBegin.range;
} else {
return undefined;
}
}
// Find the end of a cell's outputs in the text editor model
// This will be used as a starting point for any future outputs
private getEndOfOutputs(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
let outputsBegin;
if (this._activeCellGuid === cellGuid) {
outputsBegin = this._outputBeginRange;
}
if (!outputsBegin || !textEditorModel.textEditorModel.getLineContent(outputsBegin.startLineNumber).trim().includes('output')) {
this.updateOutputBeginRange(textEditorModel, cellGuid);
outputsBegin = this._outputBeginRange;
if (!outputsBegin) {
return undefined;
}
}
let outputsEnd = textEditorModel.textEditorModel.matchBracket({ column: outputsBegin.endColumn - 1, lineNumber: outputsBegin.endLineNumber });
if (!outputsEnd || outputsEnd.length < 2) {
return undefined;
}
// single line output [i.e. no outputs exist for a cell]
if (outputsBegin.endLineNumber === outputsEnd[1].startLineNumber) {
// Adding 1 to startColumn to replace text starting one character after '['
return {
startColumn: outputsEnd[0].startColumn + 1,
startLineNumber: outputsEnd[0].startLineNumber,
endColumn: outputsEnd[0].endColumn,
endLineNumber: outputsEnd[0].endLineNumber
};
} else {
// Last 2 lines in multi-line output will look like the following:
// " }"
// " ],"
if (textEditorModel.textEditorModel.getLineContent(outputsEnd[1].endLineNumber - 1).trim() === '}') {
return {
startColumn: textEditorModel.textEditorModel.getLineFirstNonWhitespaceColumn(outputsEnd[1].endLineNumber - 1) + 1,
startLineNumber: outputsEnd[1].endLineNumber - 1,
endColumn: outputsEnd[1].endColumn - 1,
endLineNumber: outputsEnd[1].endLineNumber
};
}
return undefined;
}
}
// Determine what text needs to be replaced when execution counts are updated
private getExecutionCountRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid);
if (endOutputRange && endOutputRange.endLineNumber) {
return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true);
}
return undefined;
}
// Find a cell's location, given its cellGuid
// If it doesn't exist (e.g. it's not the active cell), attempt to find it
private getCellNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
if (this._activeCellGuid !== guid || !this._sourceBeginRange) {
this.updateSourceBeginRange(textEditorModel, guid);
}
return this._sourceBeginRange;
}
private getOutputNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
if (this._activeCellGuid !== guid) {
this.updateOutputBeginRange(textEditorModel, guid);
}
return this._outputBeginRange;
}
}
function areRangePropertiesPopulated(range: Range) {
return range && range.startLineNumber && range.startColumn && range.endLineNumber && range.endColumn;
}
function findOrSetCellGuidMatch(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): FindMatch[] {
if (!textEditorModel || !cellGuid) {
return undefined;
}
return textEditorModel.textEditorModel.findMatches(cellGuid, false, false, true, undefined, true);
}

View File

@@ -65,7 +65,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
this.toggleEditMode(false); this.toggleEditMode(false);
} }
this.cellModel.active = false; this.cellModel.active = false;
this._model.activeCell = undefined; this._model.updateActiveCell(undefined);
} }
private _content: string | string[]; private _content: string | string[];

View File

@@ -0,0 +1,644 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
import { ConnectionManagementService } from 'sql/platform/connection/common/connectionManagementService';
import { CellModel } from 'sql/workbench/parts/notebook/common/models/cell';
import { CellTypes, NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { ModelFactory } from 'sql/workbench/parts/notebook/common/models/modelFactory';
import { INotebookModelOptions, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookEditorModel } from 'sql/workbench/parts/notebook/browser/models/notebookInput';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { NotebookService } from 'sql/workbench/services/notebook/common/notebookServiceImpl';
import { URI } from 'vs/base/common/uri';
import { toResource } from 'vs/base/test/common/utils';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { NullLogService } from 'vs/platform/log/common/log';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { Memento } from 'vs/workbench/common/memento';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { TestEnvironmentService, TestLifecycleService, TestStorageService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
import { Range } from 'vs/editor/common/core/range';
import { nb } from 'azdata';
import { Emitter } from 'vs/base/common/event';
import { INotebookEditor, INotebookManager } from 'sql/workbench/services/notebook/common/notebookService';
class ServiceAccessor {
constructor(
@IEditorService public editorService: IEditorService,
@ITextFileService public textFileService: TestTextFileService,
@IModelService public modelService: IModelService
) {
}
}
class NotebookManagerStub implements INotebookManager {
providerId: string;
contentManager: nb.ContentManager;
sessionManager: nb.SessionManager;
serverManager: nb.ServerManager;
}
let defaultUri = URI.file('/some/path.ipynb');
// Note: these tests are intentionally written to be extremely brittle and break on any changes to notebook/cell serialization changes.
// If any of these tests fail, it is likely that notebook editor rehydration will fail with cryptic JSON messages.
suite('Notebook Editor Model', function (): void {
let notebookManagers = [new NotebookManagerStub()];
let notebookModel: NotebookModel;
const instantiationService: IInstantiationService = workbenchInstantiationService();
let accessor: ServiceAccessor;
let defaultModelOptions: INotebookModelOptions;
const logService = new NullLogService();
const notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
let memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, '');
memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0);
const queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService());
queryConnectionService.callBase = true;
const capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService);
let mockModelFactory = TypeMoq.Mock.ofType(ModelFactory);
mockModelFactory.callBase = true;
mockModelFactory.setup(f => f.createCell(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
return new CellModel({
cell_type: CellTypes.Code,
source: '',
outputs: [
<nb.IDisplayData>{
output_type: 'display_data',
data: {
'text/html': [
'<div>',
'</div>'
]
}
}
]
}, undefined, undefined);
});
let mockNotebookService: TypeMoq.Mock<NotebookService>;
mockNotebookService = TypeMoq.Mock.ofType(NotebookService, undefined, new TestLifecycleService(), undefined, undefined, undefined, instantiationService, new MockContextKeyService(),
undefined, undefined, undefined, undefined, undefined, undefined, TestEnvironmentService);
mockNotebookService.setup(s => s.findNotebookEditor(TypeMoq.It.isAny())).returns(() => {
return {
cells: undefined,
id: '0',
notebookParams: undefined,
modelReady: undefined,
model: notebookModel,
isDirty: undefined,
isActive: undefined,
isVisible: undefined,
runAllCells: undefined,
runCell: undefined,
clearAllOutputs: undefined,
clearOutput: undefined,
executeEdits: undefined,
getSections: undefined,
navigateToSection: undefined
};
});
let mockOnNotebookEditorAddEvent = new Emitter<INotebookEditor>();
mockNotebookService.setup(s => s.onNotebookEditorAdd).returns(() => mockOnNotebookEditorAddEvent.event);
setup(() => {
accessor = instantiationService.createInstance(ServiceAccessor);
defaultModelOptions = {
notebookUri: defaultUri,
factory: new ModelFactory(instantiationService),
notebookManagers,
contentManager: undefined,
notificationService: notificationService.object,
connectionService: queryConnectionService.object,
providerId: 'SQL',
cellMagicMapper: undefined,
defaultKernel: undefined,
layoutChanged: undefined,
capabilitiesService: capabilitiesService.object
};
});
teardown(() => {
if (accessor && accessor.textFileService && accessor.textFileService.models) {
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
}
});
test('should replace entire text model if NotebookChangeType is undefined', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
should(notebookEditorModel.editorModel.textEditorModel.getLineCount()).equal(6);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(5)).equal(' "cells": []');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(2)).equal(' "metadata": {},');
});
test('should replace entire text model for add cell (0 -> 1 cells)', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(true);
});
test('should not replace entire text model for execution count change', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 0');
newCell.executionCount = 1;
contentChange = {
changeType: NotebookChangeType.CellExecuted,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellExecuted);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 1');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
newCell.executionCount = 10;
contentChange = {
changeType: NotebookChangeType.CellExecuted,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellExecuted);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 10');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
newCell.executionCount = 15;
contentChange = {
changeType: NotebookChangeType.CellExecuted,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellExecuted);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 15');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
newCell.executionCount = 105;
contentChange = {
changeType: NotebookChangeType.CellExecuted,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellExecuted);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 105');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for clear output', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
contentChange = {
changeType: NotebookChangeType.CellOutputCleared,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellOutputCleared);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(15)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(16)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for multiline source change', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 1), rangeLength: 0, rangeOffset: 0, text: 'This is a test\nLine 2 test\nLine 3 test' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 2
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' "This is a test\\n",');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' "Line 2 test\\n",');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(11)).equal(' "Line 3 test"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(16)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(27)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(28)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for single line source change', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 1), rangeLength: 0, rangeOffset: 0, text: 'This is a test' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 2
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' "This is a test"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for single line source change then delete', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' ""');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 1), rangeLength: 0, rangeOffset: 0, text: 'This is a test' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 2
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.lastEditFullReplacement).equal(false);
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 15), rangeLength: 14, rangeOffset: 0, text: '' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 3
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' ""');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for multiline source delete', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 1), rangeLength: 0, rangeOffset: 0, text: 'This is a test\nLine 2 test\nLine 3 test' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 2
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.lastEditFullReplacement).equal(false);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' "This is a test\\n",');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' "Line 2 test\\n",');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(11)).equal(' "Line 3 test"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
cellIndex: 0,
modelContentChangedEvent: {
changes: [{ range: new Range(1, 2, 3, 11), rangeLength: 36, rangeOffset: 1, text: '' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 3
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.lastEditFullReplacement).equal(false);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9)).equal(' "Tt"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
});
test('should not replace entire text model and affect only edited cell', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell;
let contentChange: NotebookContentChange;
for (let i = 0; i < 10; i++) {
let cell;
if (i === 7) {
newCell = notebookModel.addCell(CellTypes.Code);
cell = newCell;
} else {
cell = notebookModel.addCell(CellTypes.Code);
}
contentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [cell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
}
contentChange = {
changeType: NotebookChangeType.CellSourceUpdated,
cells: [newCell],
modelContentChangedEvent: {
changes: [{ range: new Range(1, 1, 1, 1), rangeLength: 0, rangeOffset: 0, text: 'This is a test' }],
eol: '\n',
isFlush: false,
isRedoing: false,
isUndoing: false,
versionId: 2
}
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellSourceUpdated);
should(notebookEditorModel.lastEditFullReplacement).equal(false);
for (let i = 0; i < 10; i++) {
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8 + i * 21)).equal(' "source": [');
if (i === 7) {
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9 + i * 21)).equal(' "This is a test"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12 + i * 21)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
} else {
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(9 + i * 21)).equal(' ""');
}
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(10 + i * 21)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14 + i * 21)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25 + i * 21)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26 + i * 21)).startWith(' }');
}
});
test('should not replace entire text model for output changes', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
newCell[<any>'_outputs'] = newCell.outputs.concat(newCell.outputs);
contentChange = {
changeType: NotebookChangeType.CellOutputUpdated,
cells: [newCell]
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellOutputUpdated);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(23)).equal(' }, {');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(31)).equal('}');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(32)).equal(' ],');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(33)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(34)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
test('should not replace entire text model for output changes (1st update)', async function (): Promise<void> {
await createNewNotebookModel();
let notebookEditorModel = await createTextEditorModel(this);
notebookEditorModel.replaceEntireTextEditorModel(notebookModel, undefined);
let newCell = notebookModel.addCell(CellTypes.Code);
let previousOutputs = newCell.outputs;
// clear outputs
newCell[<any>'_outputs'] = [];
let contentChange: NotebookContentChange = {
changeType: NotebookChangeType.CellsModified,
cells: [newCell],
cellIndex: 0
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellsModified);
should(notebookEditorModel.lastEditFullReplacement).equal(true);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [],');
// add output
newCell[<any>'_outputs'] = previousOutputs;
contentChange = {
changeType: NotebookChangeType.CellOutputUpdated,
cells: [newCell]
};
notebookEditorModel.updateModel(contentChange, NotebookChangeType.CellOutputUpdated);
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(8)).equal(' "source": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(12)).equal(' "azdata_cell_guid": "' + newCell.cellGuid + '"');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(14)).equal(' "outputs": [');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(23)).equal('}');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(25)).equal(' "execution_count": 0');
should(notebookEditorModel.editorModel.textEditorModel.getLineContent(26)).equal(' }');
should(notebookEditorModel.lastEditFullReplacement).equal(false);
});
async function createNewNotebookModel() {
let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, <Partial<INotebookModelOptions>><unknown>{
factory: mockModelFactory.object
});
notebookModel = new NotebookModel(options, undefined, logService, undefined, undefined);
await notebookModel.loadContents();
}
async function createTextEditorModel(self: Mocha.ITestCallbackContext): Promise<NotebookEditorModel> {
let textFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(self, defaultUri.toString()), 'utf8', undefined);
(<TextFileEditorModelManager>accessor.textFileService.models).add(textFileEditorModel.getResource(), textFileEditorModel);
await textFileEditorModel.load();
return new NotebookEditorModel(defaultUri, textFileEditorModel, mockNotebookService.object, accessor.textFileService);
}
});

View File

@@ -37,7 +37,7 @@ suite('Cell Model', function (): void {
cell.setOverrideLanguage('sql'); cell.setOverrideLanguage('sql');
should(cell.language).equal('sql'); should(cell.language).equal('sql');
cell.source = 'abcd'; cell.source = 'abcd';
should(cell.source).equal('abcd'); should(JSON.stringify(cell.source)).equal(JSON.stringify(['abcd']));
}); });
test('Should match ICell values if defined', async function (): Promise<void> { test('Should match ICell values if defined', async function (): Promise<void> {
@@ -55,7 +55,7 @@ suite('Cell Model', function (): void {
}; };
let cell = factory.createCell(cellData, undefined); let cell = factory.createCell(cellData, undefined);
should(cell.cellType).equal(cellData.cell_type); should(cell.cellType).equal(cellData.cell_type);
should(cell.source).equal(cellData.source); should(JSON.stringify(cell.source)).equal(JSON.stringify([cellData.source]));
should(cell.outputs).have.length(1); should(cell.outputs).have.length(1);
should(cell.outputs[0].output_type).equal('stream'); should(cell.outputs[0].output_type).equal('stream');
should((<nb.IStreamResult>cell.outputs[0]).text).equal('Some output'); should((<nb.IStreamResult>cell.outputs[0]).text).equal('Some output');
@@ -163,8 +163,8 @@ suite('Cell Model', function (): void {
mimetype: '' mimetype: ''
}); });
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(Array.isArray(cell.source)).equal(false); should(Array.isArray(cell.source)).equal(true);
should(cell.source).equal('print(1)'); should(JSON.stringify(cell.source)).equal(JSON.stringify(['print(1)']));
}); });
test('Should allow source of type string with newline and split it', async function (): Promise<void> { test('Should allow source of type string with newline and split it', async function (): Promise<void> {
@@ -241,8 +241,8 @@ suite('Cell Model', function (): void {
mimetype: '' mimetype: ''
}); });
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(Array.isArray(cell.source)).equal(false); should(Array.isArray(cell.source)).equal(true);
should(cell.source).equal(''); should(JSON.stringify(cell.source)).equal(JSON.stringify(['']));
}); });
suite('Model Future handling', function (): void { suite('Model Future handling', function (): void {
@@ -422,6 +422,56 @@ suite('Cell Model', function (): void {
oldFuture.verify(f => f.dispose(), TypeMoq.Times.once()); oldFuture.verify(f => f.dispose(), TypeMoq.Times.once());
}); });
test('should include cellGuid', async () => {
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: ''
});
let cell = factory.createCell(undefined, { notebook: notebookModel, isTrusted: false });
should(cell.cellGuid).not.be.undefined();
should(cell.cellGuid.length).equal(36);
let cellJson = cell.toJSON();
should(cellJson.metadata.azdata_cell_guid).not.be.undefined();
});
test('should include azdata_cell_guid in metadata', async () => {
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: ''
});
let cell = factory.createCell(undefined, { notebook: notebookModel, isTrusted: false });
let cellJson = cell.toJSON();
should(cellJson.metadata.azdata_cell_guid).not.be.undefined();
});
// This is critical for the notebook editor model to parse changes correctly
// If this test fails, please ensure that the notebookEditorModel tests still pass
test('should stringify in the correct order', async () => {
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: ''
});
let cell = factory.createCell(undefined, { notebook: notebookModel, isTrusted: false });
let content = JSON.stringify(cell.toJSON(), undefined, ' ');
let contentSplit = content.split('\n');
should(contentSplit.length).equal(9);
should(contentSplit[0].trim().startsWith('{')).equal(true);
should(contentSplit[1].trim().startsWith('"cell_type": "code",')).equal(true);
should(contentSplit[2].trim().startsWith('"source": ""')).equal(true);
should(contentSplit[3].trim().startsWith('"metadata": {')).equal(true);
should(contentSplit[4].trim().startsWith('"azdata_cell_guid": "')).equal(true);
should(contentSplit[5].trim().startsWith('}')).equal(true);
should(contentSplit[6].trim().startsWith('"outputs": []')).equal(true);
should(contentSplit[7].trim().startsWith('"execution_count": 0')).equal(true);
should(contentSplit[8].trim().startsWith('}')).equal(true);
});
}); });
}); });

View File

@@ -106,6 +106,13 @@ export class NotebookModelStub implements INotebookModel {
serializationStateChanged(changeType: NotebookChangeType): void { serializationStateChanged(changeType: NotebookChangeType): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
get onActiveCellChanged(): Event<ICellModel> {
throw new Error('Method not implemented.');
}
updateActiveCell(cell: ICellModel) {
throw new Error('Method not implemented.');
}
} }
export class NotebookManagerStub implements INotebookManager { export class NotebookManagerStub implements INotebookManager {
@@ -131,4 +138,4 @@ export class ServerManagerStub implements nb.ServerManager {
this.calledEnd = true; this.calledEnd = true;
return this.result; return this.result;
} }
} }

View File

@@ -99,7 +99,7 @@ export interface INotebookService {
* sent to listeners that can act on the point-in-time notebook state * sent to listeners that can act on the point-in-time notebook state
* @param notebookUri the URI identifying a notebook * @param notebookUri the URI identifying a notebook
*/ */
serializeNotebookStateChange(notebookUri: URI, changeType: NotebookChangeType): void; serializeNotebookStateChange(notebookUri: URI, changeType: NotebookChangeType, cell?: ICellModel): void;
/** /**
* *

View File

@@ -30,7 +30,7 @@ import { NotebookEditor } from 'sql/workbench/parts/notebook/browser/notebookEdi
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerNotebookThemes } from 'sql/workbench/parts/notebook/browser/notebookStyles'; import { registerNotebookThemes } from 'sql/workbench/parts/notebook/browser/notebookStyles';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement'; import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import { notebookConstants } from 'sql/workbench/parts/notebook/common/models/modelInterfaces'; import { notebookConstants, ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { SqlNotebookProvider } from 'sql/workbench/services/notebook/common/sql/sqlNotebookProvider'; import { SqlNotebookProvider } from 'sql/workbench/services/notebook/common/sql/sqlNotebookProvider';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
@@ -578,7 +578,7 @@ export class NotebookService extends Disposable implements INotebookService {
} }
} }
serializeNotebookStateChange(notebookUri: URI, changeType: NotebookChangeType): void { serializeNotebookStateChange(notebookUri: URI, changeType: NotebookChangeType, cell?: ICellModel): void {
if (notebookUri.scheme !== Schemas.untitled) { if (notebookUri.scheme !== Schemas.untitled) {
// Conditions for saving: // Conditions for saving:
// 1. Not untitled. They're always trusted as we open them // 1. Not untitled. They're always trusted as we open them
@@ -598,7 +598,7 @@ export class NotebookService extends Disposable implements INotebookService {
let editor = this.findNotebookEditor(notebookUri); let editor = this.findNotebookEditor(notebookUri);
if (editor && editor.model) { if (editor && editor.model) {
editor.model.serializationStateChanged(changeType); editor.model.serializationStateChanged(changeType, cell);
// TODO add history notification if a non-untitled notebook has a state change // TODO add history notification if a non-untitled notebook has a state change
} }
} }