diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 57092f376c..116df5609c 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -4536,6 +4536,7 @@ declare module 'azdata' { export interface INotebookMetadata { kernelspec: IKernelInfo; language_info?: ILanguageInfo; + tags?: string[]; } export interface IKernelInfo { @@ -4567,6 +4568,7 @@ declare module 'azdata' { source: string | string[]; metadata?: { language?: string; + tags?: string[]; azdata_cell_guid?: string; }; execution_count?: number; diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index ef20d65753..0c391691c7 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -631,6 +631,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellSourceUpdated: case NotebookChangeType.DirtyStateChanged: + case NotebookChangeType.CellInputVisibilityChanged: case NotebookChangeType.CellOutputCleared: return NotebookChangeKind.ContentUpdated; case NotebookChangeType.KernelChanged: diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 2d0d83d610..dc1a3cd85a 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -127,7 +127,7 @@ export class QueryTextEditor extends BaseTextEditor { return editorWidget.getScrollHeight(); } - public setHeightToScrollHeight(configChanged?: boolean): void { + public setHeightToScrollHeight(configChanged?: boolean, isEditorCollapsed?: boolean, ) { let editorWidget = this.getControl() as ICodeEditor; let layoutInfo = editorWidget.getLayoutInfo(); if (!this._scrollbarHeight) { @@ -138,7 +138,12 @@ export class QueryTextEditor extends BaseTextEditor { // Not ready yet return; } - let lineCount = editorWidgetModel.getLineCount(); + let lineCount: number; + if (!!isEditorCollapsed) { + lineCount = 1; + } else { + lineCount = editorWidgetModel.getLineCount(); + } // Need to also keep track of lines that wrap; if we just keep into account line count, then the editor's height would not be // tall enough and we would need to show a scrollbar. Unfortunately, it looks like there isn't any metadata saved in a ICodeEditor // around max column length for an editor (which we could leverage to see if we need to loop through every line to determine diff --git a/src/sql/workbench/parts/notebook/browser/cellToggleMoreActions.ts b/src/sql/workbench/parts/notebook/browser/cellToggleMoreActions.ts index 04bb050169..234eedd9b4 100644 --- a/src/sql/workbench/parts/notebook/browser/cellToggleMoreActions.ts +++ b/src/sql/workbench/parts/notebook/browser/cellToggleMoreActions.ts @@ -36,7 +36,9 @@ export class CellToggleMoreActions { instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', "Insert Text After"), CellTypes.Markdown, true), instantiationService.createInstance(RunCellsAction, 'runAllBefore', localize('runAllBefore', "Run Cells Before"), false), instantiationService.createInstance(RunCellsAction, 'runAllAfter', localize('runAllAfter', "Run Cells After"), true), - instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', "Clear Output")) + instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', "Clear Output")), + instantiationService.createInstance(CollapseCellAction, 'collapseCell', localize('collapseCell', "Collapse Cell"), true), + instantiationService.createInstance(CollapseCellAction, 'expandCell', localize('expandCell', "Expand Cell"), false) ); } @@ -182,3 +184,41 @@ export class RunCellsAction extends CellActionBase { return Promise.resolve(); } } + +export class CollapseCellAction extends CellActionBase { + constructor(id: string, + label: string, + private collapseCell: boolean, + @INotificationService notificationService: INotificationService + ) { + super(id, label, undefined, notificationService); + } + + public canRun(context: CellContext): boolean { + return context.cell && context.cell.cellType === CellTypes.Code; + } + + async doRun(context: CellContext): Promise { + try { + let cell = context.cell || context.model.activeCell; + if (cell) { + if (this.collapseCell) { + if (!cell.isCollapsed) { + cell.isCollapsed = true; + } + } else { + if (cell.isCollapsed) { + cell.isCollapsed = false; + } + } + } + } catch (error) { + let message = getErrorMessage(error); + this.notificationService.notify({ + severity: Severity.Error, + message: message + }); + } + return Promise.resolve(); + } +} diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/code.component.html b/src/sql/workbench/parts/notebook/browser/cellViews/code.component.html index 750987538c..840f9628ce 100644 --- a/src/sql/workbench/parts/notebook/browser/cellViews/code.component.html +++ b/src/sql/workbench/parts/notebook/browser/cellViews/code.component.html @@ -7,7 +7,9 @@
-
+
+
+
diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/parts/notebook/browser/cellViews/code.component.ts index b0722466ce..cfbb9cc1b4 100644 --- a/src/sql/workbench/parts/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/parts/notebook/browser/cellViews/code.component.ts @@ -33,6 +33,8 @@ import * as notebookUtils from 'sql/workbench/parts/notebook/browser/models/note import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { ILogService } from 'vs/platform/log/common/log'; +import { CollapseComponent } from 'sql/workbench/parts/notebook/browser/cellViews/collapse.component'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; export const CODE_SELECTOR: string = 'code-component'; const MARKDOWN_CLASS = 'markdown'; @@ -45,6 +47,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; @ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef; @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; + @ViewChild(CollapseComponent) private collapseComponent: CollapseComponent; public get cellModel(): ICellModel { return this._cellModel; @@ -81,7 +84,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange this.cellModel.hover = value; if (!this.isActive()) { // Only make a change if we're not active, since this has priority - this.toggleMoreActionsButton(this.cellModel.hover); + this.toggleActionsVisibility(this.cellModel.hover); } } @@ -112,7 +115,6 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange (() => this.layout())); // Handle disconnect on removal of the cell, if it was the active cell this._register({ dispose: () => this.updateConnectionState(false) }); - } ngOnInit() { @@ -129,7 +131,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange let changedProp = changes[propName]; let isActive = this.cellModel.id === changedProp.currentValue; this.updateConnectionState(isActive); - this.toggleMoreActionsButton(isActive); + this.toggleActionsVisibility(isActive); if (this._editor) { this._editor.toggleEditorSelected(isActive); } @@ -191,21 +193,24 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange this._editor.setVisible(true); this._editor.setMinimumHeight(this._minimumHeight); this._editor.setMaximumHeight(this._maximumHeight); + let uri = this.cellModel.cellUri; let cellModelSource: string; cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source; this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, cellModelSource, ''); await this._editor.setInput(this._editorInput, undefined); this.setFocusAndScroll(); + let untitledEditorModel: UntitledEditorModel = await this._editorInput.resolve(); this._editorModel = untitledEditorModel.textEditorModel; + let isActive = this.cellModel.id === this._activeCellId; this._editor.toggleEditorSelected(isActive); + // For markdown cells, don't show line numbers unless we're using editor defaults let overrideEditorSetting = this._configurationService.getValue(OVERRIDE_EDITOR_THEMING_SETTING); this._editor.hideLineNumbers = (overrideEditorSetting && this.cellModel.cellType === CellTypes.Markdown); - if (this.destroyed) { // At this point, we may have been disposed (scenario: restoring markdown cell in preview mode). // Exiting early to avoid warnings on registering already disposed items, which causes some churning @@ -216,15 +221,21 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange this._register(this._editor); this._register(this._editorInput); this._register(this._editorModel.onDidChangeContent(e => { - this._editor.setHeightToScrollHeight(); 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) { + this._cellModel.isCollapsed = false; + } + this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed); + this.onContentChanged.emit(); this.checkForLanguageMagics(); })); this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.fontSize')) { - this._editor.setHeightToScrollHeight(true); + this._editor.setHeightToScrollHeight(true, this._cellModel.isCollapsed); } })); this._register(this.model.layoutChanged(() => this._layoutEmitter.fire(), this)); @@ -233,14 +244,22 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange this.setFocusAndScroll(); } })); + this._register(this.cellModel.onCollapseStateChanged(isCollapsed => { + this.onCellCollapse(isCollapsed); + })); + this.layout(); + + if (this._cellModel.isCollapsed) { + this.onCellCollapse(true); + } } public layout(): void { this._editor.layout(new DOM.Dimension( DOM.getContentWidth(this.codeElement.nativeElement), DOM.getContentHeight(this.codeElement.nativeElement))); - this._editor.setHeightToScrollHeight(); + this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed); } protected initActionBar() { @@ -317,7 +336,29 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange return this.cellModel && this.cellModel.id === this.activeCellId; } - protected toggleMoreActionsButton(isActiveOrHovered: boolean) { + protected toggleActionsVisibility(isActiveOrHovered: boolean) { this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered); + + if (this.collapseComponent) { + this.collapseComponent.toggleIconVisibility(isActiveOrHovered); + } + } + + private onCellCollapse(isCollapsed: boolean): void { + let editorWidget = this._editor.getControl() as ICodeEditor; + if (isCollapsed) { + let model = editorWidget.getModel(); + let totalLines = model.getLineCount(); + let endColumn = model.getLineMaxColumn(totalLines); + editorWidget.setHiddenAreas([{ + startLineNumber: 2, + startColumn: 1, + endLineNumber: totalLines, + endColumn: endColumn + }]); + } else { + editorWidget.setHiddenAreas([]); + } + this._editor.setHeightToScrollHeight(false, isCollapsed); } } diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/code.css b/src/sql/workbench/parts/notebook/browser/cellViews/code.css index 7bf89e4172..bd2c3c6fc9 100644 --- a/src/sql/workbench/parts/notebook/browser/cellViews/code.css +++ b/src/sql/workbench/parts/notebook/browser/cellViews/code.css @@ -60,6 +60,7 @@ code-component .toolbarIconStop { code-component .editor { padding: 5px 0px 5px 0px } + /* overview ruler */ code-component .monaco-editor .decorationsOverviewRuler { visibility: hidden; @@ -86,7 +87,34 @@ code-component .carbon-taskbar .codicon.hideIcon.execCountHundred { margin-left: -6px; } -code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container -{ +code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container { padding-left: 10px } + +code-component .hide-component-button { + height: 16px; + width: 100%; + box-sizing: border-box; + border-width: 0px; + background-repeat: no-repeat; + background-position: center; + background-color: inherit; +} + +code-component .hide-component-button.icon-hide-cell { + background-image: url("./media/light/chevron_up.svg"); +} + +code-component .hide-component-button.icon-show-cell { + background-image: url("./media/light/chevron_down.svg"); +} + +.vs-dark code-component .hide-component-button.icon-hide-cell, +.hc-black code-component .hide-component-button.icon-hide-cell { + background-image: url("./media/dark/chevron_up_inverse.svg"); +} + +.vs-dark code-component .hide-component-button.icon-show-cell, +.hc-black code-component .hide-component-button.icon-show-cell { + background-image: url("./media/dark/chevron_down_inverse.svg"); +} diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.html b/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.html index 8ad467106a..f6a716b678 100644 --- a/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.html +++ b/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.html @@ -9,8 +9,8 @@
- +
-
\ No newline at end of file + diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.ts b/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.ts index f53670e437..c94bc66dc6 100644 --- a/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.ts +++ b/src/sql/workbench/parts/notebook/browser/cellViews/codeCell.component.ts @@ -47,6 +47,9 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { ngOnInit() { if (this.cellModel) { + this._register(this.cellModel.onCollapseStateChanged((state) => { + this._changeRef.detectChanges(); + })); this._register(this.cellModel.onOutputsChanged(() => { this._changeRef.detectChanges(); })); @@ -73,6 +76,7 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { get activeCellId(): string { return this._activeCellId; } + public layout() { } diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.html b/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.html new file mode 100644 index 0000000000..cdef1f0f20 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.html @@ -0,0 +1,10 @@ + +
+ + +
diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.ts b/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.ts new file mode 100644 index 0000000000..b35427dd48 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/collapse.component.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./code'; + +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, SimpleChange, OnChanges } from '@angular/core'; +import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces'; +import { ICellModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces'; + +export const COLLAPSE_SELECTOR: string = 'collapse-component'; + +@Component({ + selector: COLLAPSE_SELECTOR, + templateUrl: decodeURI(require.toUrl('./collapse.component.html')) +}) + +export class CollapseComponent extends CellView implements OnInit, OnChanges { + @ViewChild('collapseCellButton', { read: ElementRef }) private collapseCellButtonElement: ElementRef; + @ViewChild('expandCellButton', { read: ElementRef }) private expandCellButtonElement: ElementRef; + + @Input() cellModel: ICellModel; + @Input() activeCellId: string; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + ) { + super(); + } + + ngOnInit() { + } + + ngOnChanges(changes: { [propKey: string]: SimpleChange }) { + } + + ngAfterContentInit() { + this._register(this.cellModel.onCollapseStateChanged(isCollapsed => { + this.handleCellCollapse(isCollapsed); + })); + this.handleCellCollapse(this.cellModel.isCollapsed); + if (this.activeCellId === this.cellModel.id) { + this.toggleIconVisibility(true); + } + } + + private handleCellCollapse(isCollapsed: boolean): void { + let collapseButton = this.collapseCellButtonElement.nativeElement; + let expandButton = this.expandCellButtonElement.nativeElement; + if (isCollapsed) { + collapseButton.style.display = 'none'; + expandButton.style.display = 'block'; + } else { + collapseButton.style.display = 'block'; + expandButton.style.display = 'none'; + } + } + + public toggleCollapsed(event?: Event): void { + if (event) { + event.stopPropagation(); + } + this.cellModel.isCollapsed = !this.cellModel.isCollapsed; + } + + public layout() { + + } + + public toggleIconVisibility(isActiveOrHovered: boolean) { + let collapseButton = this.collapseCellButtonElement.nativeElement; + let buttonClass = 'icon-hide-cell'; + if (isActiveOrHovered) { + collapseButton.classList.add(buttonClass); + } else { + collapseButton.classList.remove(buttonClass); + } + } +} diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_down_inverse.svg b/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_down_inverse.svg new file mode 100644 index 0000000000..b09a9f4e6e --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_down_inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_up_inverse.svg b/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_up_inverse.svg new file mode 100644 index 0000000000..f59d76d752 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/media/dark/chevron_up_inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_down.svg b/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_down.svg new file mode 100644 index 0000000000..3a454b37ab --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_up.svg b/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_up.svg new file mode 100644 index 0000000000..b3fbc2bbe6 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/cellViews/media/light/chevron_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/parts/notebook/browser/media/dark/hide_code_inverse.svg b/src/sql/workbench/parts/notebook/browser/media/dark/hide_code_inverse.svg new file mode 100644 index 0000000000..594cede979 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/media/dark/hide_code_inverse.svg @@ -0,0 +1,5 @@ + + opac_command_icons_bv + + + diff --git a/src/sql/workbench/parts/notebook/browser/media/dark/show_code_inverse.svg b/src/sql/workbench/parts/notebook/browser/media/dark/show_code_inverse.svg new file mode 100644 index 0000000000..9421211f70 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/media/dark/show_code_inverse.svg @@ -0,0 +1,5 @@ + + opac_command_icons_bv + + + diff --git a/src/sql/workbench/parts/notebook/browser/media/light/hide_code.svg b/src/sql/workbench/parts/notebook/browser/media/light/hide_code.svg new file mode 100644 index 0000000000..61e938ae25 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/media/light/hide_code.svg @@ -0,0 +1,5 @@ + + opac_command_icons_bv + + + diff --git a/src/sql/workbench/parts/notebook/browser/media/light/show_code.svg b/src/sql/workbench/parts/notebook/browser/media/light/show_code.svg new file mode 100644 index 0000000000..6cf95b4505 --- /dev/null +++ b/src/sql/workbench/parts/notebook/browser/media/light/show_code.svg @@ -0,0 +1,5 @@ + + opac_command_icons_bv + + + diff --git a/src/sql/workbench/parts/notebook/browser/models/cell.ts b/src/sql/workbench/parts/notebook/browser/models/cell.ts index 902fff8d4a..4d9a0d5a6c 100644 --- a/src/sql/workbench/parts/notebook/browser/models/cell.ts +++ b/src/sql/workbench/parts/notebook/browser/models/cell.ts @@ -24,8 +24,9 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; let modelId = 0; - export class CellModel implements ICellModel { + public id: string; + private _cellType: nb.CellType; private _source: string | string[]; private _language: string; @@ -41,15 +42,18 @@ export class CellModel implements ICellModel { private _hover: boolean; private _executionCount: number | undefined; private _cellUri: URI; - public id: string; private _connectionManagementService: IConnectionManagementService; private _stdInHandler: nb.MessageHandler; private _onCellLoaded = new Emitter(); private _loaded: boolean; private _stdInVisible: boolean; - private _metadata: { language?: string, cellGuid?: string; }; + private _metadata: { language?: string; tags?: string[]; cellGuid?: string; }; + private _isCollapsed: boolean; + private _onCollapseStateChanged = new Emitter(); private _modelContentChangedEvent: IModelContentChangedEvent; + private readonly _hideInputTag = 'hide_input'; + constructor(cellData: nb.ICellContents, private _options: ICellModelOptions, @optional(INotebookService) private _notebookService?: INotebookService @@ -78,6 +82,10 @@ export class CellModel implements ICellModel { return other && other.id === this.id; } + public get onCollapseStateChanged(): Event { + return this._onCollapseStateChanged.event; + } + public get onOutputsChanged(): Event { return this._onOutputsChanged.event; } @@ -94,6 +102,38 @@ export class CellModel implements ICellModel { return this._future; } + public get isCollapsed() { + return this._isCollapsed; + } + + public set isCollapsed(value: boolean) { + let stateChanged = this._isCollapsed !== value; + this._isCollapsed = value; + + let tagIndex = -1; + if (this._metadata.tags) { + tagIndex = this._metadata.tags.findIndex(tag => tag === this._hideInputTag); + } + + if (this._isCollapsed) { + if (tagIndex === -1) { + if (!this._metadata.tags) { + this._metadata.tags = []; + } + this._metadata.tags.push(this._hideInputTag); + } + } else { + if (tagIndex > -1) { + this._metadata.tags.splice(tagIndex, 1); + } + } + + if (stateChanged) { + this._onCollapseStateChanged.fire(this._isCollapsed); + this.sendChangeToNotebook(NotebookChangeType.CellInputVisibilityChanged); + } + } + public set isEditMode(isEditMode: boolean) { this._isEditMode = isEditMode; this._onCellModeChanged.fire(this._isEditMode); @@ -255,6 +295,9 @@ export class CellModel implements ICellModel { this.notebookModel.updateActiveCell(this); this.active = true; } + if (this.isCollapsed) { + this.isCollapsed = false; + } if (connectionManagementService) { this._connectionManagementService = connectionManagementService; @@ -540,14 +583,16 @@ export class CellModel implements ICellModel { } public toJSON(): nb.ICellContents { + let metadata = this._metadata || {}; let cellJson: Partial = { cell_type: this._cellType, source: this._source, - metadata: this._metadata || {} + metadata: metadata }; cellJson.metadata.azdata_cell_guid = this._cellGuid; if (this._cellType === CellTypes.Code) { cellJson.metadata.language = this._language; + cellJson.metadata.tags = metadata.tags; cellJson.outputs = this._outputs; cellJson.execution_count = this.executionCount ? this.executionCount : 0; } @@ -561,7 +606,14 @@ export class CellModel implements ICellModel { this._cellType = cell.cell_type; this.executionCount = cell.execution_count; this._source = this.getMultilineSource(cell.source); - this._metadata = cell.metadata; + this._metadata = cell.metadata || {}; + + if (this._metadata.tags && this._metadata.tags.includes(this._hideInputTag)) { + this._isCollapsed = true; + } else { + this._isCollapsed = false; + } + this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid(); this.setLanguageFromContents(cell); if (cell.outputs) { diff --git a/src/sql/workbench/parts/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/parts/notebook/browser/models/modelInterfaces.ts index 4d2cc490ad..e50264694a 100644 --- a/src/sql/workbench/parts/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/parts/notebook/browser/models/modelInterfaces.ts @@ -488,6 +488,8 @@ export interface ICellModel { loaded: boolean; stdInVisible: boolean; readonly onLoaded: Event; + isCollapsed: boolean; + readonly onCollapseStateChanged: Event; modelContentChangedEvent: IModelContentChangedEvent; } diff --git a/src/sql/workbench/parts/notebook/browser/models/notebookModel.ts b/src/sql/workbench/parts/notebook/browser/models/notebookModel.ts index 9e7f6a4065..cda7519a53 100644 --- a/src/sql/workbench/parts/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/parts/notebook/browser/models/notebookModel.ts @@ -60,6 +60,7 @@ export class NotebookModel extends Disposable implements INotebookModel { private _cells: ICellModel[]; private _defaultLanguageInfo: nb.ILanguageInfo; + private _tags: string[]; private _language: string; private _onErrorEmitter = new Emitter(); private _savedKernelInfo: nb.IKernelInfo; @@ -557,6 +558,9 @@ export class NotebookModel extends Disposable implements INotebookModel { return undefined; } + public get tags(): string[] { + return this._tags; + } public get languageInfo(): nb.ILanguageInfo { return this._defaultLanguageInfo; @@ -991,6 +995,7 @@ export class NotebookModel extends Disposable implements INotebookModel { // TODO update language and kernel when these change metadata.kernelspec = this._savedKernelInfo; metadata.language_info = this.languageInfo; + metadata.tags = this._tags; return { metadata, nbformat_minor: this._nbformatMinor, @@ -1007,6 +1012,7 @@ export class NotebookModel extends Disposable implements INotebookModel { switch (change) { case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellSourceUpdated: + case NotebookChangeType.CellInputVisibilityChanged: changeInfo.isDirty = true; changeInfo.modelContentChangedEvent = cell.modelContentChangedEvent; break; diff --git a/src/sql/workbench/parts/notebook/browser/notebook.component.ts b/src/sql/workbench/parts/notebook/browser/notebook.component.ts index ac58407284..b8be36359c 100644 --- a/src/sql/workbench/parts/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/parts/notebook/browser/notebook.component.ts @@ -32,7 +32,7 @@ import * as notebookUtils from 'sql/workbench/parts/notebook/browser/models/note import { Deferred } from 'sql/base/common/promise'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; -import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, RunAllCellsAction, ClearAllOutputsAction } from 'sql/workbench/parts/notebook/browser/notebookActions'; +import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, RunAllCellsAction, ClearAllOutputsAction, CollapseCellsAction } from 'sql/workbench/parts/notebook/browser/notebookActions'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import * as TaskUtilities from 'sql/workbench/browser/taskUtilities'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -427,6 +427,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted'); this._trustedAction.enabled = false; + let collapseCellsAction = this.instantiationService.createInstance(CollapseCellsAction, 'notebook.collapseCells'); + let taskbar = this.toolbar.nativeElement; this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) }); this._actionBar.context = this; @@ -437,7 +439,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe { element: attachToContainer }, { action: this._trustedAction }, { action: this._runAllCellsAction }, - { action: clearResultsButton } + { action: clearResultsButton }, + { action: collapseCellsAction } ]); } diff --git a/src/sql/workbench/parts/notebook/browser/notebook.css b/src/sql/workbench/parts/notebook/browser/notebook.css index c503f6605a..557120f282 100644 --- a/src/sql/workbench/parts/notebook/browser/notebook.css +++ b/src/sql/workbench/parts/notebook/browser/notebook.css @@ -49,48 +49,66 @@ vertical-align: bottom; } -.notebookEditor .notebook-button.icon-add{ +.notebookEditor .notebook-button.icon-add { background-image: url("./media/light/add.svg"); } .vs-dark .notebookEditor .notebook-button.icon-add, -.hc-black .notebookEditor .notebook-button.icon-add{ +.hc-black .notebookEditor .notebook-button.icon-add { background-image: url("./media/dark/add_inverse.svg"); } -.notebookEditor .notebook-button.icon-run-cells{ +.notebookEditor .notebook-button.icon-run-cells { background-image: url("./media/light/run_cells.svg"); } .vs-dark .notebookEditor .notebook-button.icon-run-cells, -.hc-black .notebookEditor .notebook-button.icon-run-cells{ +.hc-black .notebookEditor .notebook-button.icon-run-cells { background-image: url("./media/dark/run_cells_inverse.svg"); } -.notebookEditor .notebook-button.icon-trusted{ +.notebookEditor .notebook-button.icon-trusted { background-image: url("./media/light/trusted.svg"); } .vs-dark .notebookEditor .notebook-button.icon-trusted, -.hc-black .notebookEditor .notebook-button.icon-trusted{ +.hc-black .notebookEditor .notebook-button.icon-trusted { background-image: url("./media/dark/trusted_inverse.svg"); } -.notebookEditor .notebook-button.icon-notTrusted{ +.notebookEditor .notebook-button.icon-notTrusted { background-image: url("./media/light/nottrusted.svg"); } .vs-dark .notebookEditor .notebook-button.icon-notTrusted, -.hc-black .notebookEditor .notebook-button.icon-notTrusted{ +.hc-black .notebookEditor .notebook-button.icon-notTrusted { background-image: url("./media/dark/nottrusted_inverse.svg"); } -.notebookEditor .notebook-button.icon-clear-results{ +.notebookEditor .notebook-button.icon-show-cells { + background-image: url("./media/light/show_code.svg"); +} + +.vs-dark .notebookEditor .notebook-button.icon-show-cells, +.hc-black .notebookEditor .notebook-button.icon-show-cells { + background-image: url("./media/dark/show_code_inverse.svg"); +} + +.notebookEditor .notebook-button.icon-hide-cells { + background-image: url("./media/light/hide_code.svg"); +} + +.vs-dark .notebookEditor .notebook-button.icon-hide-cells, +.hc-black .notebookEditor .notebook-button.icon-hide-cells { + background-image: url("./media/dark/hide_code_inverse.svg"); +} + +.notebookEditor .notebook-button.icon-clear-results { background-image: url("./media/light/clear_results.svg"); } .vs-dark .notebookEditor .notebook-button.icon-clear-results, -.hc-black .notebookEditor .notebook-button.icon-clear-results{ +.hc-black .notebookEditor .notebook-button.icon-clear-results { background-image: url("./media/dark/clear_results_inverse.svg"); } diff --git a/src/sql/workbench/parts/notebook/browser/notebook.module.ts b/src/sql/workbench/parts/notebook/browser/notebook.module.ts index fcd93bad4f..cc1ffe3790 100644 --- a/src/sql/workbench/parts/notebook/browser/notebook.module.ts +++ b/src/sql/workbench/parts/notebook/browser/notebook.module.ts @@ -29,6 +29,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { LinkHandlerDirective } from 'sql/workbench/parts/notebook/browser/cellViews/linkHandler.directive'; import { IBootstrapParams, ISelector } from 'sql/platform/bootstrap/common/bootstrapParams'; import { ICellComponenetRegistry, Extensions as OutputComponentExtensions } from 'sql/platform/notebooks/common/outputRegistry'; +import { CollapseComponent } from 'sql/workbench/parts/notebook/browser/cellViews/collapse.component'; const outputComponentRegistry = Registry.as(OutputComponentExtensions.CellComponentContributions); @@ -51,6 +52,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I OutputAreaComponent, OutputComponent, StdInComponent, + CollapseComponent, LinkHandlerDirective, ...outputComponents ], diff --git a/src/sql/workbench/parts/notebook/browser/notebookActions.ts b/src/sql/workbench/parts/notebook/browser/notebookActions.ts index 90df2af0cd..962d9711f2 100644 --- a/src/sql/workbench/parts/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/parts/notebook/browser/notebookActions.ts @@ -268,6 +268,47 @@ export class RunAllCellsAction extends Action { } } +export class CollapseCellsAction extends ToggleableAction { + private static readonly collapseCells = localize('collapseAllCells', "Collapse Cells"); + private static readonly expandCells = localize('expandAllCells', "Expand Cells"); + private static readonly baseClass = 'notebook-button'; + private static readonly collapseCssClass = 'icon-hide-cells'; + private static readonly expandCssClass = 'icon-show-cells'; + + constructor(id: string) { + super(id, { + baseClass: CollapseCellsAction.baseClass, + toggleOnLabel: CollapseCellsAction.expandCells, + toggleOnClass: CollapseCellsAction.expandCssClass, + toggleOffLabel: CollapseCellsAction.collapseCells, + toggleOffClass: CollapseCellsAction.collapseCssClass, + isOn: false + }); + } + + public get isCollapsed(): boolean { + return this.state.isOn; + } + public set isCollapsed(value: boolean) { + this.toggle(value); + } + + public run(context: NotebookComponent): Promise { + let self = this; + return new Promise((resolve, reject) => { + try { + self.isCollapsed = !self.isCollapsed; + context.cells.forEach(cell => { + cell.isCollapsed = self.isCollapsed; + }); + resolve(true); + } catch (e) { + reject(e); + } + }); + } +} + export class KernelsDropdown extends SelectBox { private model: NotebookModel; constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise) { diff --git a/src/sql/workbench/parts/notebook/browser/notebookStyles.ts b/src/sql/workbench/parts/notebook/browser/notebookStyles.ts index 8042c2ded9..a9696aa142 100644 --- a/src/sql/workbench/parts/notebook/browser/notebookStyles.ts +++ b/src/sql/workbench/parts/notebook/browser/notebookStyles.ts @@ -144,7 +144,8 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf collector.addRule(` .notebook-cell:not(.active) code-component .monaco-editor, .notebook-cell:not(.active) code-component .monaco-editor-background, - .notebook-cell:not(.active) code-component .monaco-editor .inputarea.ime-input + .notebook-cell:not(.active) code-component .monaco-editor .inputarea.ime-input, + .notebook-cell.active .hide-component-button:hover { background-color: ${codeBackground}; }`); diff --git a/src/sql/workbench/parts/notebook/common/models/contracts.ts b/src/sql/workbench/parts/notebook/common/models/contracts.ts index 3e1c62d319..afab392188 100644 --- a/src/sql/workbench/parts/notebook/common/models/contracts.ts +++ b/src/sql/workbench/parts/notebook/common/models/contracts.ts @@ -45,5 +45,6 @@ export enum NotebookChangeType { TrustChanged, Saved, CellExecuted, + CellInputVisibilityChanged, CellOutputCleared } diff --git a/src/sql/workbench/parts/notebook/test/electron-browser/cell.test.ts b/src/sql/workbench/parts/notebook/test/electron-browser/cell.test.ts index fece3e66bc..8f087e71e3 100644 --- a/src/sql/workbench/parts/notebook/test/electron-browser/cell.test.ts +++ b/src/sql/workbench/parts/notebook/test/electron-browser/cell.test.ts @@ -245,6 +245,94 @@ suite('Cell Model', function (): void { should(JSON.stringify(cell.source)).equal(JSON.stringify([''])); }); + test('Should parse metadata\'s hide_input tag correctly', async function (): Promise { + let notebookModel = new NotebookModelStub({ + name: '', + version: '', + mimetype: '' + }); + let contents: nb.ICellContents = { + cell_type: CellTypes.Code, + source: '' + }; + let model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + + should(model.isCollapsed).be.false(); + model.isCollapsed = true; + should(model.isCollapsed).be.true(); + model.isCollapsed = false; + should(model.isCollapsed).be.false(); + + let modelJson = model.toJSON(); + should(modelJson.metadata.tags).not.be.undefined(); + should(modelJson.metadata.tags).not.containEql('hide_input'); + + contents.metadata = { + tags: ['hide_input'] + }; + model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + + should(model.isCollapsed).be.true(); + model.isCollapsed = false; + should(model.isCollapsed).be.false(); + model.isCollapsed = true; + should(model.isCollapsed).be.true(); + + modelJson = model.toJSON(); + should(modelJson.metadata.tags).not.be.undefined(); + should(modelJson.metadata.tags).containEql('hide_input'); + + contents.metadata = { + tags: ['not_a_real_tag'] + }; + model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + modelJson = model.toJSON(); + should(modelJson.metadata.tags).not.be.undefined(); + should(modelJson.metadata.tags).not.containEql('hide_input'); + + contents.metadata = { + tags: ['not_a_real_tag', 'hide_input'] + }; + model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + modelJson = model.toJSON(); + should(modelJson.metadata.tags).not.be.undefined(); + should(modelJson.metadata.tags).containEql('hide_input'); + }); + + test('Should emit event after collapsing cell', async function (): Promise { + let notebookModel = new NotebookModelStub({ + name: '', + version: '', + mimetype: '' + }); + let contents: nb.ICellContents = { + cell_type: CellTypes.Code, + source: '' + }; + let model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + should(model.isCollapsed).be.false(); + + let createCollapsePromise = () => { + return new Promise((resolve, reject) => { + setTimeout(() => reject(), 2000); + model.onCollapseStateChanged(isCollapsed => { + resolve(isCollapsed); + }); + }); + }; + + should(model.isCollapsed).be.false(); + let collapsePromise = createCollapsePromise(); + model.isCollapsed = true; + let isCollapsed = await collapsePromise; + should(isCollapsed).be.true(); + + collapsePromise = createCollapsePromise(); + model.isCollapsed = false; + isCollapsed = await collapsePromise; + should(isCollapsed).be.false(); + }); + suite('Model Future handling', function (): void { let future: TypeMoq.Mock; let cell: ICellModel;