From 68e7a293ad80ead2da2b1a6d7a7b7cab2650bfb6 Mon Sep 17 00:00:00 2001 From: Hale Rankin Date: Fri, 31 Jul 2020 17:57:46 -0700 Subject: [PATCH] Notebook cell toolbar additions - move cell, convert cell (#11457) * Notebook cell toolbar additions - move up and move down added. Stubbed out related actions. Cleaned up component code. * Added new more actions menu item: Convert cell. * add move cell support in model * Schema Compare cleanup (#11418) * cleanup async and await stuff * remove awaits * remove more awaits * fix (#11437) * Add some unit tests for PyPiClient. (#11442) * handle invalid character in kubectl version output (#11460) * Add tests for azdata extension (#11423) * Add tests for azdata extension * Fail on stderr * Skip test for not implemented logic * Move executeCommand stub * Add missing packages * let semver to parse the version (#11463) * let semver to parse the version * check * Stop hardcoding python3 (#11464) * Add ConnectControllerDialog tests (#11443) * Automatically fix up arc controller URL * wip * Force tests to pass * Refactor * comment * adds role of button to all links that are buttons (#11465) * Merge from vscode 0a7364f00514c46c9caceece15e1f82f82e3712f * bump smoke extensions * bump node version in builds * bump smoke extensions * Add query-history and sql-assessment to recommended extensions (#11477) * First draft of outputProcessor tests (#11368) * First draft of outputProcessor tests * add return type for a function * pr feedback * comments and Spellings, getRandom ==> getRandomElement * pr feedback * pr feedback * Adds support for installing azdata on Linux (#11469) * Large cleanup of AzureCore - Introduction of getAccountSecurityToken and deprecation of getSecurityToken (#11446) * do a large cleanup of azurecore * Fix tests * Rework Device Code * Fix tests * Fix AE scenario * Fix firewall rule - clenaup logging * Shorthand syntax * Fix firewall tests * Start on tests for azureAuth * Add more tests * Address comments * Add a few more important tests * Don't throw error on old code * Fill in todo * Adding button plugin to table component (#10918) * Added delete plugin to table component * Arc - Remove Azure params from Postgres deployment (#11478) Co-authored-by: Brian Bergeron * tests for KernelsDropdown class (#11476) * add return type for a function * tests for KernelsDropdown class * remove inadvertent change * remove inadvertent change * formatting changes * pr feedback * pr feedback * implement review feedback (#11470) * fix sql proj sqlcmd table showing after loading profile when it shouldn't (#11479) * Feature/outer paths for project (#11445) * allow relative paths in project file outside of project folder * Adding some tests * Adding error string to loc strings * Fixed test * fix error message * PR comments and some more fixes * change userName to match what the azure account display name is (#11484) * change userName to match what the azure account display name is * Handle undefined value * Merge from vscode 8c426f9f3b6b18935cc6c2ec8aa6d45ccd88021e * recomment out integration tests * Fix/open book error (#11379) * add isNotebook param and showPreview option * showPreview changes * update OpenNotebookFolder to open a specific path * added test for showPreviewFile * test name typo * remove isNotebook from openBook * Add test coverage for dacpac wizard import flow (#11483) * Adding importConfig onPageEnter() test * Removing redundancy from dacpac wizard pages * promisifying file selection so it can be awaited in the test * removing debug prints * PR feedback * Remove all accounts regardless of failure in one account (#11431) * distro (#11487) * distro * distro * distro * distro * Adding icons to Database Projects' tree view (#11488) * Add images * Splitting to light and dark mode icons * Hooks up icons to treeItems * updating package.json with new icon and vbump * move icon loader before tree view created * Update Arc extension version and fix Controller connectivity status names (#11498) * Update connectivity mode names (cherry picked from commit f0aabcfa86d178cdf74470f9fdeded19718bcea2) * Bump package version (cherry picked from commit e08370539006c638d6e25c2f4f23fa2754a3377d) * deploy to single existing device (#11494) * deploy to single existing device * comments * Add versioning for accounts (#11497) * Add versioning for accounts * deletion value * Changes to getAccountSecurityToken (#11502) * Hook up convert cell * Fix tests * Add convert cell tests Co-authored-by: chlafreniere Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com> Co-authored-by: Maddy <12754347+MaddyDev@users.noreply.github.com> Co-authored-by: Cory Rivera Co-authored-by: Alan Ren Co-authored-by: Charles Gagnon Co-authored-by: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com> Co-authored-by: v-bbrady <60623315+v-bbrady@users.noreply.github.com> Co-authored-by: ADS Merger Co-authored-by: Arvind Ranasaria Co-authored-by: Amir Omidi Co-authored-by: Leila Lali Co-authored-by: Brian Bergeron Co-authored-by: Brian Bergeron Co-authored-by: Udeesha Gautam <46980425+udeeshagautam@users.noreply.github.com> Co-authored-by: Benjin Dubishar --- src/sql/media/icons/common-icons.css | 6 +- .../notebook/browser/cellToolbarActions.ts | 54 +++++++++++ .../cellViews/cellToolbar.component.ts | 30 +++--- .../notebook/browser/notebook.component.ts | 1 + .../test/browser/cellToolbarActions.test.ts | 92 ++++++++++++++++++- .../workbench/contrib/notebook/test/stubs.ts | 8 +- .../services/notebook/browser/models/cell.ts | 8 ++ .../browser/models/modelInterfaces.ts | 16 ++++ .../notebook/browser/models/notebookModel.ts | 56 ++++++++++- 9 files changed, 251 insertions(+), 20 deletions(-) diff --git a/src/sql/media/icons/common-icons.css b/src/sql/media/icons/common-icons.css index 0f8d8d5886..4175f3604b 100644 --- a/src/sql/media/icons/common-icons.css +++ b/src/sql/media/icons/common-icons.css @@ -565,15 +565,13 @@ Includes non-masked style declarations. */ -webkit-mask-image: url('edit.svg'); mask-image: url('edit.svg'); } - -.codicon.masked-icon.move-up { +.codicon.masked-icon.move-up:before { transform: scale(-1); background-image: none; -webkit-mask-image: url('down-arrow-blue.svg'); mask-image: url('down-arrow-blue.svg'); } - -.codicon.masked-icon.move-down { +.codicon.masked-icon.move-down:before { background-image: none; -webkit-mask-image: url('down-arrow-blue.svg'); mask-image: url('down-arrow-blue.svg'); diff --git a/src/sql/workbench/contrib/notebook/browser/cellToolbarActions.ts b/src/sql/workbench/contrib/notebook/browser/cellToolbarActions.ts index 02956d191d..8c51634aa8 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellToolbarActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellToolbarActions.ts @@ -18,6 +18,7 @@ import Severity from 'vs/base/common/severity'; import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { MoveDirection } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; export class EditCellAction extends ToggleableAction { @@ -65,6 +66,35 @@ export class EditCellAction extends ToggleableAction { } } +export class MoveCellAction extends CellActionBase { + constructor( + id: string, + cssClass: string, + label: string, + @INotificationService notificationService: INotificationService + ) { + super(id, label, undefined, notificationService); + this._cssClass = cssClass; + this._tooltip = label; + this._label = ''; + } + + doRun(context: CellContext): Promise { + let moveDirection = this._cssClass.includes('move-down') ? MoveDirection.Down : MoveDirection.Up; + try { + context.model.moveCell(context.cell, moveDirection); + } catch (error) { + let message = getErrorMessage(error); + + this.notificationService.notify({ + severity: Severity.Error, + message: message + }); + } + return Promise.resolve(); + } +} + export class DeleteCellAction extends CellActionBase { constructor( id: string, @@ -101,6 +131,8 @@ export class CellToggleMoreActions { @IInstantiationService private instantiationService: IInstantiationService ) { this._actions.push( + instantiationService.createInstance(ConvertCellAction, 'convertCell', localize('convertCell', "Convert Cell")), + new Separator(), instantiationService.createInstance(RunCellsAction, 'runAllAbove', localize('runAllAbove', "Run Cells Above"), false), instantiationService.createInstance(RunCellsAction, 'runAllBelow', localize('runAllBelow', "Run Cells Below"), true), new Separator(), @@ -151,6 +183,28 @@ export function removeDuplicatedAndStartingSeparators(actions: (Action | CellAct } } +export class ConvertCellAction extends CellActionBase { + constructor(id: string, label: string, + @INotificationService notificationService: INotificationService + ) { + super(id, label, undefined, notificationService); + } + + doRun(context: CellContext): Promise { + try { + context?.model?.convertCellType(context?.cell); + } catch (error) { + let message = getErrorMessage(error); + + this.notificationService.notify({ + severity: Severity.Error, + message: message + }); + } + return Promise.resolve(); + } +} + export class AddCellFromContextAction extends CellActionBase { constructor( id: string, label: string, private cellType: CellType, private isAfter: boolean, diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts index 6d95fe0429..6ed2836b2d 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DeleteCellAction, EditCellAction, CellToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions'; +import { DeleteCellAction, EditCellAction, CellToggleMoreActions, MoveCellAction } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions'; import { AddCellAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu'; @@ -27,10 +27,12 @@ export const CELL_TOOLBAR_SELECTOR: string = 'cell-toolbar-component'; export class CellToolbarComponent { @ViewChild('celltoolbar', { read: ElementRef }) private celltoolbar: ElementRef; - public buttonEdit = localize('buttonEdit', "Edit"); - public buttonClose = localize('buttonClose', "Close"); - public buttonAdd = localize('buttonAdd', "Add new cell"); - public buttonDelete = localize('buttonDelete', "Delete cell"); + public buttonAdd = localize('buttonAdd', "Add cell"); + public optionCodeCell = localize('optionCodeCell', "Code cell"); + public optionTextCell = localize('optionTextCell', "Text cell"); + public buttonMoveDown = localize('buttonMoveDown', "Move cell down"); + public buttonMoveUp = localize('buttonMoveUp', "Move cell up"); + public buttonDelete = localize('buttonDelete', "Delete"); @Input() cellModel: ICellModel; @Input() model: NotebookModel; @@ -64,17 +66,20 @@ export class CellToolbarComponent { let addTextCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddTextCell', localize('textPreview', "Text cell"), 'notebook-button masked-pseudo markdown'); addTextCellButton.cellType = CellTypes.Markdown; - let deleteButton = this.instantiationService.createInstance(DeleteCellAction, 'delete', 'codicon masked-icon delete', localize('delete', "Delete")); + let moveCellDownButton = this.instantiationService.createInstance(MoveCellAction, 'notebook.MoveCellDown', 'masked-icon move-down', this.buttonMoveDown); + let moveCellUpButton = this.instantiationService.createInstance(MoveCellAction, 'notebook.MoveCellUp', 'masked-icon move-up', this.buttonMoveUp); + + let deleteButton = this.instantiationService.createInstance(DeleteCellAction, 'notebook.DeleteCell', 'masked-icon delete', this.buttonDelete); let moreActionsContainer = DOM.$('li.action-item'); this._cellToggleMoreActions = this.instantiationService.createInstance(CellToggleMoreActions); this._cellToggleMoreActions.onInit(moreActionsContainer, context); - this._editCellAction = this.instantiationService.createInstance(EditCellAction, 'notebook.editCell', true, this.cellModel.isEditMode); + this._editCellAction = this.instantiationService.createInstance(EditCellAction, 'notebook.EditCell', true, this.cellModel.isEditMode); this._editCellAction.enabled = true; - let buttonDropdownContainer = DOM.$('li.action-item'); - buttonDropdownContainer.setAttribute('role', 'presentation'); + let addCellDropdownContainer = DOM.$('li.action-item'); + addCellDropdownContainer.setAttribute('role', 'presentation'); let dropdownMenuActionViewItem = new DropdownMenuActionViewItem( addCellsButton, [addCodeCellButton, addTextCellButton], @@ -86,14 +91,17 @@ export class CellToolbarComponent { '', undefined ); - dropdownMenuActionViewItem.render(buttonDropdownContainer); + dropdownMenuActionViewItem.render(addCellDropdownContainer); dropdownMenuActionViewItem.setActionContext(context); let taskbarContent: ITaskbarContent[] = []; if (this.cellModel?.cellType === CellTypes.Markdown) { taskbarContent.push({ action: this._editCellAction }); } - taskbarContent.push({ element: buttonDropdownContainer }, + taskbarContent.push( + { element: addCellDropdownContainer }, + { action: moveCellDownButton }, + { action: moveCellUpButton }, { action: deleteButton }, { element: moreActionsContainer }); diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 866e085419..a70a06f86f 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -319,6 +319,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this._register(model.contentChanged((change) => this.handleContentChanged(change))); this._register(model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider))); this._register(model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs))); + this._register(model.onCellTypeChanged(() => this.detectChanges())); this._model = this._register(model); await this._model.loadContents(trusted); this.setLoading(false); diff --git a/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts index 7204029df7..2ba6af5d17 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts @@ -5,7 +5,7 @@ import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; -import { CellToggleMoreActions, RunCellsAction, removeDuplicatedAndStartingSeparators, AddCellFromContextAction, CollapseCellAction } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions'; +import { CellToggleMoreActions, RunCellsAction, removeDuplicatedAndStartingSeparators, AddCellFromContextAction, CollapseCellAction, ConvertCellAction } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions'; import { NotebookService } from 'sql/workbench/services/notebook/browser/notebookServiceImpl'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -20,6 +20,16 @@ import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuS import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell'; import { IProductService } from 'vs/platform/product/common/productService'; import { Separator } from 'vs/base/common/actions'; +import { INotebookModelOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import { NotebookEditorContentManager } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; +import { URI } from 'vs/base/common/uri'; +import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; +import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; +import { nb } from 'azdata'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { NotebookManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; suite('CellToolbarActions', function (): void { suite('removeDuplicatedAndStartingSeparators', function (): void { @@ -115,7 +125,7 @@ suite('CellToolbarActions', function (): void { cellModelMock.setup(x => x.cellType).returns(() => 'code'); const action = new CellToggleMoreActions(instantiationService); action.onInit(testContainer, contextMock.object); - assert(action['_moreActions']['viewItems'][0]['_action']['_actions'].length === 13, 'Unexpected number of valid elements'); + assert.equal(action['_moreActions']['viewItems'][0]['_action']['_actions'].length, 15, 'Unexpected number of valid elements'); }); test('CellToggleMoreActions with Markdown CellType', function (): void { @@ -124,7 +134,83 @@ suite('CellToolbarActions', function (): void { const action = new CellToggleMoreActions(instantiationService); action.onInit(testContainer, contextMock.object); // Markdown elements don't show the code-cell related actions such as Run Cell - assert(action['_moreActions']['viewItems'][0]['_action']['_actions'].length === 5, 'Unexpected number of valid elements'); + assert.equal(action['_moreActions']['viewItems'][0]['_action']['_actions'].length, 7, 'Unexpected number of valid elements'); + }); + }); + + suite('ConvertCellAction', function (): void { + let convertCellAction: ConvertCellAction; + let notebookModel: NotebookModel; + + suiteSetup(async function (): Promise { + convertCellAction = new ConvertCellAction('id', 'label', undefined); + notebookModel = await createandLoadNotebookModel(); + }); + + test('No notebook model passed in', async function (): Promise { + let cellModel = new CellModel({ cell_type: 'code', source: '' }, { isTrusted: true, notebook: undefined }); + await convertCellAction.doRun({ cell: cellModel, model: undefined }); + assert.equal(cellModel.cellType, 'code', 'Cell type should not be affected'); + }); + + test('Convert to code cell', async function (): Promise { + await notebookModel.loadContents(); + await convertCellAction.doRun({ model: notebookModel, cell: notebookModel.cells[0] }); + assert.equal(notebookModel.cells[0].cellType, 'markdown', 'Cell was not converted correctly'); + }); + + test('Convert to markdown cell', async function (): Promise { + await notebookModel.loadContents(); + notebookModel.cells[0].cellType = 'markdown'; + await convertCellAction.doRun({ model: notebookModel, cell: notebookModel.cells[0] }); + assert.equal(notebookModel.cells[0].cellType, 'code', 'Cell was not converted correctly'); + }); + + test('Convert to code cell and back', async function (): Promise { + await notebookModel.loadContents(); + notebookModel.cells[0].cellType = 'markdown'; + await convertCellAction.doRun({ model: notebookModel, cell: notebookModel.cells[0] }); + assert.equal(notebookModel.cells[0].cellType, 'code', 'Cell was not converted correctly'); + await convertCellAction.doRun({ model: notebookModel, cell: notebookModel.cells[0] }); + assert.equal(notebookModel.cells[0].cellType, 'markdown', 'Cell was not converted correctly second time'); }); }); }); + +async function createandLoadNotebookModel(codeContent?: nb.INotebookContents): Promise { + let defaultCodeContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: [''], + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'python', + language: 'python' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + + let serviceCollection = new ServiceCollection(); + let instantiationService = new InstantiationService(serviceCollection, true); + let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentManager); + mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(codeContent ? codeContent : defaultCodeContent)); + let defaultModelOptions: INotebookModelOptions = { + notebookUri: URI.file('/some/path.ipynb'), + factory: new ModelFactory(instantiationService), + notebookManagers: [new NotebookManagerStub()], + contentManager: mockContentManager.object, + notificationService: undefined, + connectionService: undefined, + providerId: 'SQL', + cellMagicMapper: undefined, + defaultKernel: undefined, + layoutChanged: undefined, + capabilitiesService: undefined + }; + return new NotebookModel(defaultModelOptions, undefined, undefined, undefined, undefined); +} diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 6abcfb0bf0..a2e26e1061 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -5,7 +5,7 @@ import { nb, IConnectionProfile } from 'azdata'; import * as vsEvent from 'vs/base/common/event'; -import { INotebookModel, ICellModel, IClientSession, NotebookContentChange, ISingleNotebookEditOperation } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { INotebookModel, ICellModel, IClientSession, NotebookContentChange, ISingleNotebookEditOperation, MoveDirection } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { INotebookFindModel } from 'sql/workbench/contrib/notebook/browser/models/notebookFindModel'; import { NotebookChangeType, CellType } from 'sql/workbench/services/notebook/common/contracts'; import { INotebookManager, INotebookService, INotebookEditor, ILanguageMagic, INotebookProvider, INavigationProvider, INotebookParams, INotebookSection, ICellEditorProvider, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; @@ -94,6 +94,9 @@ export class NotebookModelStub implements INotebookModel { addCell(cellType: CellType, index?: number): void { throw new Error('Method not implemented.'); } + moveCell(cellModel: ICellModel, direction: MoveDirection): void { + throw new Error('Method not implemented.'); + } deleteCell(cellModel: ICellModel): void { throw new Error('Method not implemented.'); } @@ -118,6 +121,9 @@ export class NotebookModelStub implements INotebookModel { get onActiveCellChanged(): vsEvent.Event { throw new Error('Method not implemented.'); } + get onCellTypeChanged(): vsEvent.Event { + throw new Error('method not implemented.'); + } updateActiveCell(cell: ICellModel) { throw new Error('Method not implemented.'); } diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index fdb2ddff7e..7f03f73279 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -225,6 +225,14 @@ export class CellModel extends Disposable implements ICellModel { return this._cellType; } + public set cellType(type: CellType) { + if (type !== this._cellType) { + this._cellType = type; + // Regardless, get rid of outputs; this matches Jupyter behavior + this._outputs = []; + } + } + public get source(): string | string[] { return this._source; } diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index 55fa0e7e58..1f2f29c141 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -312,6 +312,11 @@ export interface INotebookModel { */ readonly onActiveCellChanged: Event; + /** + * Event fired on cell type change + */ + readonly onCellTypeChanged: Event; + /** * The trusted mode of the Notebook */ @@ -343,6 +348,12 @@ export interface INotebookModel { */ addCell(cellType: CellType, index?: number): void; + /** + * Moves a cell up/down + */ + moveCell(cellModel: ICellModel, direction: MoveDirection): void; + + /** * Deletes a cell */ @@ -424,6 +435,11 @@ export enum CellExecutionState { Error = 3 } +export enum MoveDirection { + Up, + Down +} + export interface IOutputChangedEvent { outputs: ReadonlyArray; shouldScroll: boolean; diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index fd30ac1e87..664c999c95 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IClientSession, INotebookModel, INotebookModelOptions, ICellModel, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { IClientSession, INotebookModel, INotebookModelOptions, ICellModel, NotebookContentChange, MoveDirection } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookChangeType, CellType, CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { nbversion } from 'sql/workbench/services/notebook/common/notebookConstants'; import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils'; @@ -59,6 +59,7 @@ export class NotebookModel extends Disposable implements INotebookModel { private _onProviderIdChanged = new Emitter(); private _trustedMode: boolean; private _onActiveCellChanged = new Emitter(); + private _onCellTypeChanged = new Emitter(); private _cells: ICellModel[]; private _defaultLanguageInfo: nb.ILanguageInfo; @@ -271,6 +272,10 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._onActiveCellChanged.event; } + public get onCellTypeChanged(): Event { + return this._onCellTypeChanged.event; + } + public get standardKernels(): notebookUtils.IStandardKernelWithProvider[] { return this._standardKernels; } @@ -387,6 +392,40 @@ export class NotebookModel extends Disposable implements INotebookModel { return cell; } + moveCell(cell: ICellModel, direction: MoveDirection): void { + if (this.inErrorState) { + return null; + } + let index = this.findCellIndex(cell); + + if ((index === 0 && direction === MoveDirection.Up) || ((index === this._cells.length - 1 && direction === MoveDirection.Down))) { + // Nothing to do + return; + } + + if (direction === MoveDirection.Down) { + this._cells.splice(index, 1); + if (index + 1 < this._cells.length) { + this._cells.splice(index + 1, 0, cell); + } else { + this._cells.push(cell); + } + } else { + this._cells.splice(index, 1); + this._cells.splice(index - 1, 0, cell); + } + + index = this.findCellIndex(cell); + + // Set newly created cell as active cell + this.updateActiveCell(cell); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellsModified, + cells: [cell], + cellIndex: index + }); + } + public updateActiveCell(cell: ICellModel): void { if (this._activeCell) { this._activeCell.active = false; @@ -398,6 +437,21 @@ export class NotebookModel extends Disposable implements INotebookModel { this._onActiveCellChanged.fire(cell); } + public convertCellType(cell: ICellModel): void { + if (cell) { + let index = this.findCellIndex(cell); + if (index > -1) { + cell.cellType = cell.cellType === CellTypes.Markdown ? CellTypes.Code : CellTypes.Markdown; + this._onCellTypeChanged.fire(cell); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellsModified, + cells: [cell], + cellIndex: index + }); + } + } + } + private createCell(cellType: CellType): ICellModel { let singleCell: nb.ICellContents = { cell_type: cellType,