Add collapse/expand functionality to notebook code cells. (#7481)
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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) {
|
||||
|
||||
@@ -488,6 +488,8 @@ export interface ICellModel {
|
||||
loaded: boolean;
|
||||
stdInVisible: boolean;
|
||||
readonly onLoaded: Event<string>;
|
||||
isCollapsed: boolean;
|
||||
readonly onCollapseStateChanged: Event<boolean>;
|
||||
modelContentChangedEvent: IModelContentChangedEvent;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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};
|
||||
}`);
|
||||
|
||||
@@ -45,5 +45,6 @@ export enum NotebookChangeType {
|
||||
TrustChanged,
|
||||
Saved,
|
||||
CellExecuted,
|
||||
CellInputVisibilityChanged,
|
||||
CellOutputCleared
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||