Add collapse/expand functionality to notebook code cells. (#7481)

This commit is contained in:
Cory Rivera
2019-10-03 16:50:47 -07:00
committed by GitHub
parent 080d9bbaa6
commit 6b29fd05bd
29 changed files with 492 additions and 34 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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<void> {
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();
}
}

View File

@@ -7,7 +7,9 @@
<div style="width: 100%; height: 100%; display: flex; flex-flow: row" (mouseover)="hover=true" (mouseleave)="hover=false">
<div #toolbar class="toolbar">
</div>
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
<div style="flex: 1 1 auto; flex-flow: column; overflow: hidden;">
<div #editor class="editor"></div>
<collapse-component *ngIf="cellModel.cellType === 'code'" [cellModel]="cellModel" [activeCellId]="activeCellId"></collapse-component>
</div>
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div>

View File

@@ -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<boolean>(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);
}
}

View File

@@ -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");
}

View File

@@ -9,8 +9,8 @@
<code-component [cellModel]="cellModel" [model]="model" [activeCellId]="activeCellId"></code-component>
</div>
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel" [activeCellId]="activeCellId">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0 && !cellModel.isCollapsed" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-area-component>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
</div>
</div>
</div>

View File

@@ -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() {
}

View File

@@ -0,0 +1,10 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="width: 100%; height: fit-content; display: flex; flex-flow: column">
<button #collapseCellButton (click)="toggleCollapsed($event)" class="hide-component-button"></button>
<button #expandCellButton (click)="toggleCollapsed($event)" style="display:none" class="hide-component-button icon-show-cell"></button>
</div>

View File

@@ -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 = <HTMLElement>this.collapseCellButtonElement.nativeElement;
let expandButton = <HTMLElement>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 = <HTMLElement>this.collapseCellButtonElement.nativeElement;
let buttonClass = 'icon-hide-cell';
if (isActiveOrHovered) {
collapseButton.classList.add(buttonClass);
} else {
collapseButton.classList.remove(buttonClass);
}
}
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.1484 3.64844L15.8516 4.35156L8 12.2031L0.148438 4.35156L0.851562 3.64844L8 10.7969L15.1484 3.64844Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2734 11.9766L8 4.71094L0.726562 11.9766L0.0234375 11.2734L8 3.28906L15.9766 11.2734L15.2734 11.9766Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.1484 3.64844L15.8516 4.35156L8 12.2031L0.148438 4.35156L0.851562 3.64844L8 10.7969L15.1484 3.64844Z" fill="#4F4F4F"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2734 11.9766L8 4.71094L0.726562 11.9766L0.0234375 11.2734L8 3.28906L15.9766 11.2734L15.2734 11.9766Z" fill="#4F4F4F"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>opac_command_icons_bv</title>
<path d="M0,1H16V2H0ZM.148,6.148,2.5,3.8,4.852,6.148l-.7.7L3,5.711V15H2V5.711L.852,6.852ZM7,5V4h9V5Z" fill="#fff"/>
<rect width="16" height="16" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>opac_command_icons_bv</title>
<path d="M0,1H16V2H0ZM3,13.289l1.148-1.141.7.7L2.5,15.2.148,12.852l.7-.7L2,13.289V4H3ZM7,5V4h9V5ZM7,8V7h9V8Zm0,3V10h9v1Zm0,3V13h9v1Z" fill="#fff"/>
<rect width="16" height="16" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>opac_command_icons_bv</title>
<path d="M0,1H16V2H0ZM.148,6.148,2.5,3.8,4.852,6.148l-.7.7L3,5.711V15H2V5.711L.852,6.852ZM7,5V4h9V5Z"/>
<rect width="16" height="16" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>opac_command_icons_bv</title>
<path d="M0,1H16V2H0ZM3,13.289l1.148-1.141.7.7L2.5,15.2.148,12.852l.7-.7L2,13.289V4H3ZM7,5V4h9V5ZM7,8V7h9V8Zm0,3V10h9v1Zm0,3V13h9v1Z"/>
<rect width="16" height="16" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -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<nb.IStdinMessage>;
private _onCellLoaded = new Emitter<string>();
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<boolean>();
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<boolean> {
return this._onCollapseStateChanged.event;
}
public get onOutputsChanged(): Event<IOutputChangedEvent> {
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<nb.ICellContents> = {
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) {

View File

@@ -488,6 +488,8 @@ export interface ICellModel {
loaded: boolean;
stdInVisible: boolean;
readonly onLoaded: Event<string>;
isCollapsed: boolean;
readonly onCollapseStateChanged: Event<boolean>;
modelContentChangedEvent: IModelContentChangedEvent;
}

View File

@@ -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<INotification>();
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;

View File

@@ -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 = <HTMLElement>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 }
]);
}

View File

@@ -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");
}

View File

@@ -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<ICellComponenetRegistry>(OutputComponentExtensions.CellComponentContributions);
@@ -51,6 +52,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I
OutputAreaComponent,
OutputComponent,
StdInComponent,
CollapseComponent,
LinkHandlerDirective,
...outputComponents
],

View File

@@ -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<boolean> {
let self = this;
return new Promise<boolean>((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<INotebookModel>) {

View File

@@ -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};
}`);

View File

@@ -45,5 +45,6 @@ export enum NotebookChangeType {
TrustChanged,
Saved,
CellExecuted,
CellInputVisibilityChanged,
CellOutputCleared
}

View File

@@ -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<void> {
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<void> {
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<EmptyFuture>;
let cell: ICellModel;