diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index 07f1261abe..0cee9939a0 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -231,7 +231,6 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { this._register(this._editorInput); this._register(this._editorModel.onDidChangeContent(e => { this.cellModel.modelContentChangedEvent = e; - let originalSourceLength = this.cellModel.source.length; this.cellModel.source = this._editorModel.getValue(); if (this._cellModel.isCollapsed && originalSourceLength !== this.cellModel.source.length) { @@ -269,6 +268,10 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } this._layoutEmitter.fire(); })); + this._register(this.cellModel.onCellModeChanged((isEditMode) => { + this.onCellModeChanged(isEditMode); + })); + this.layout(); if (this._cellModel.isCollapsed) { @@ -421,4 +424,16 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } this._editor.setHeightToScrollHeight(false, isCollapsed); } + + private onCellModeChanged(isEditMode: boolean): void { + if (this.cellModel.id === this._activeCellId || this._activeCellId === '') { + if (isEditMode) { + this._editor.getContainer().contentEditable = 'true'; + this._editor.getControl().focus(); + } else { + this._editor.getContainer().contentEditable = 'false'; + (document.activeElement as HTMLElement).blur(); + } + } + } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts index 4e9b428db8..6ae60823b6 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts @@ -13,7 +13,6 @@ import { ICellEditorProvider } from 'sql/workbench/services/notebook/browser/not import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component'; import { OutputAreaComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/outputArea.component'; - export const CODE_SELECTOR: string = 'code-cell-component'; @Component({ @@ -32,10 +31,9 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { this._activeCellId = value; } - @HostListener('document:keydown.escape', ['$event']) - handleKeyboardEvent() { - this.cellModel.active = false; - this._model.updateActiveCell(undefined); + // On click to edit code cell in notebook + @HostListener('click', ['$event']) onClick() { + this.toggleEditMode(); } private _activeCellId: string; @@ -60,6 +58,11 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { this._register(this.cellModel.onOutputsChanged(() => { this._changeRef.detectChanges(); })); + this._register(this.cellModel.onCellModeChanged(mode => { + if (mode !== this.cellModel.isEditMode) { + this._changeRef.detectChanges(); + } + })); // Register request handler, cleanup on dispose of this component this.cellModel.setStdInHandler({ handle: (msg) => this.handleStdIn(msg) }); this._register({ dispose: () => this.cellModel.setStdInHandler(undefined) }); @@ -99,6 +102,11 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { } + public toggleEditMode(): void { + this.cellModel.isEditMode = !this.cellModel.isEditMode; + this._changeRef.detectChanges(); + } + handleStdIn(msg: nb.IStdinMessage): void | Thenable { if (msg) { this.stdIn = msg; diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts index 7b25278415..0e03ed2be4 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts @@ -53,15 +53,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { this._activeCellId = value; } - @HostListener('document:keydown.escape', ['$event']) - handleKeyboardEvent() { - if (this.isEditMode) { - this.toggleEditMode(false); - } - this.cellModel.active = false; - this._model.updateActiveCell(undefined); - } - // Double click to edit text cell in notebook @HostListener('dblclick', ['$event']) onDblClick() { this.enableActiveCellEditOnDoubleClick(); @@ -70,8 +61,8 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { @HostListener('document:keydown', ['$event']) onkeydown(e: KeyboardEvent) { if (DOM.getActiveElement() === this.output?.nativeElement && this.isActive() && this.cellModel?.currentMode === CellEditModes.WYSIWYG) { - // Select all text const keyEvent = new StandardKeyboardEvent(e); + // Select all text if ((keyEvent.ctrlKey || keyEvent.metaKey) && keyEvent.keyCode === KeyCode.KEY_A) { preventDefaultAndExecCommand(e, 'selectAll'); } else if ((keyEvent.metaKey && keyEvent.shiftKey && keyEvent.keyCode === KeyCode.KEY_Z) || (keyEvent.ctrlKey && keyEvent.keyCode === KeyCode.KEY_Y) && !this.markdownMode) { @@ -557,7 +548,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { if (!this.isEditMode && this.doubleClickEditEnabled) { this.toggleEditMode(true); } - this.cellModel.active = true; this._model.updateActiveCell(this.cellModel); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.html b/src/sql/workbench/contrib/notebook/browser/notebook.component.html index bb5e45885b..8ec254e04b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.html @@ -14,7 +14,7 @@ - + diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 90ff072dff..9279c5fac3 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -55,6 +55,8 @@ import { MaskedLabeledMenuItemActionItem } from 'sql/platform/actions/browser/me import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Emitter } from 'vs/base/common/event'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; const PRIORITY = 105; @@ -131,6 +133,40 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } return false; })); + this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + let event = new StandardKeyboardEvent(e); + if (this.isActive() && this.model.activeCell) { + if (!this.model.activeCell?.isEditMode) { + if (event.keyCode === KeyCode.DownArrow) { + let next = (this.findCellIndex(this.model.activeCell) + 1) % this.cells.length; + this.selectCell(this.cells[next]); + this.scrollToActiveCell(); + } else if (event.keyCode === KeyCode.UpArrow) { + let index = this.findCellIndex(this.model.activeCell); + if (index === 0) { + index = this.cells.length; + } + this.selectCell(this.cells[--index]); + this.scrollToActiveCell(); + } else if (event.keyCode === KeyCode.Escape) { + // unselects active cell and removes the focus from code cells + this.unselectActiveCell(); + (document.activeElement as HTMLElement).blur(); + } + else if (event.keyCode === KeyCode.Enter) { + // prevents adding a newline to the cell source + e.preventDefault(); + // show edit toolbar + this.setActiveCellEditActionMode(true); + this.toggleEditMode(); + } + } else if (event.keyCode === KeyCode.Escape) { + // first time hitting escape removes the cursor from code cell and changes toolbar in text cells and changes edit mode to false + this.toggleEditMode(); + this.setActiveCellEditActionMode(false); + } + } + })); } ngOnInit() { @@ -248,6 +284,23 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } } + private scrollToActiveCell(): void { + // Get active cell from active notebook editor + const activeCellElement = document.querySelector(`.editor-group-container.active .notebook-cell.active`); + activeCellElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + } + + private toggleEditMode(): void { + let selectedCell: TextCellComponent | CodeCellComponent = undefined; + if (this.model.activeCell.cellType !== CellTypes.Code) { + selectedCell = this.textCells.find(c => c.cellModel.id === this.activeCellId); + } else { + selectedCell = this.codeCells.find(c => c.cellModel.id === this.activeCellId); + } + selectedCell.toggleEditMode(); + } + //Saves scrollTop value on scroll change public scrollHandler(event: Event) { this._scrollTop = (event.srcElement).scrollTop; @@ -261,16 +314,18 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe // Handles double click to edit icon change // See textcell.component.ts for changing edit behavior - public enableActiveCellIconOnDoubleClick() { + public enableActiveCellEditIconOnDoubleClick() { if (this.doubleClickEditEnabled) { - const toolbarComponent = (this.cellToolbar.first); - const toolbarEditCellAction = toolbarComponent.getEditCellAction(); - if (!toolbarEditCellAction.editMode) { - toolbarEditCellAction.editMode = !toolbarEditCellAction.editMode; - } + this.setActiveCellEditActionMode(true); } } + public setActiveCellEditActionMode(editMode: boolean) { + const toolbarComponent = (this.cellToolbar.first); + const toolbarEditCellAction = toolbarComponent.getEditCellAction(); + toolbarEditCellAction.editMode = editMode; + } + // Add cell based on cell type public addCell(cellType: CellType, index?: number, event?: Event) { if (event) { diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts index 332c115afb..e96b37973e 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts @@ -769,9 +769,9 @@ suite('Cell Model', function (): void { cell.trustedMode = true; assert.strictEqual(cell.trustedMode, true, 'Cell should be trusted after manually setting trustedMode'); - assert.strictEqual(cell.isEditMode, true, 'Code cells should be editable by default'); - cell.isEditMode = false; - assert.strictEqual(cell.isEditMode, false, 'Cell should not be editable after manually setting isEditMode'); + assert.strictEqual(cell.isEditMode, false, 'Code cells should not be editable by default'); + cell.isEditMode = true; + assert.strictEqual(cell.isEditMode, true, 'Cell should be editable after manually setting isEditMode'); cell.hover = true; assert.strictEqual(cell.hover, true, 'Cell should be hovered after manually setting hover=true'); diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 15abfa9fe6..04af171484 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -107,7 +107,7 @@ export class CellModel extends Disposable implements ICellModel { this._source = ''; } - this._isEditMode = this._cellType !== CellTypes.Markdown; + this._isEditMode = false; this._stdInVisible = false; if (_options && _options.isTrusted) { this._isTrusted = true;