mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-07 17:23:56 -05:00
refactor notebook to be fileservice based (#6459)
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ElementRef } from '@angular/core';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { CellActionBase, CellContext } from 'sql/workbench/parts/notebook/browser/cellViews/codeActions';
|
||||
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { ToggleMoreWidgetAction } from 'sql/workbench/parts/dashboard/browser/core/actions';
|
||||
import { CellModel } from 'sql/workbench/parts/notebook/common/models/cell';
|
||||
|
||||
export const HIDDEN_CLASS = 'actionhidden';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: CellActionBase[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
private _moreActionsElement: HTMLElement;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', "Delete")),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeBefore', localize('codeBefore', "Insert Code Before"), CellTypes.Code, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', "Insert Code After"), CellTypes.Code, true),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', "Insert Text Before"), CellTypes.Markdown, false),
|
||||
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"))
|
||||
);
|
||||
}
|
||||
|
||||
public onInit(elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
let context = new CellContext(model, cellModel);
|
||||
this._moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (this._moreActionsElement.childNodes.length > 0) {
|
||||
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: this._moreActionsElement };
|
||||
let validActions = this._actions.filter(a => a.canRun(context));
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, validActions, context), { icon: true, label: false });
|
||||
}
|
||||
|
||||
public toggleVisible(visible: boolean): void {
|
||||
if (!this._moreActionsElement) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
DOM.addClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
} else {
|
||||
DOM.removeClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellFromContextAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearCellOutputAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
public canRun(context: CellContext): boolean {
|
||||
return context.cell && context.cell.cellType === CellTypes.Code;
|
||||
}
|
||||
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let cell = context.cell || context.model.activeCell;
|
||||
if (cell) {
|
||||
(cell as CellModel).clearOutputs();
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class RunCellsAction extends CellActionBase {
|
||||
constructor(id: string,
|
||||
label: string,
|
||||
private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
) {
|
||||
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) {
|
||||
let editor = this.notebookService.findNotebookEditor(cell.notebookModel.notebookUri);
|
||||
if (editor) {
|
||||
if (this.isAfter) {
|
||||
await editor.runAllCells(cell, undefined);
|
||||
} else {
|
||||
await editor.runAllCells(undefined, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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: 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>
|
||||
<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>
|
||||
</div>
|
||||
@@ -0,0 +1,326 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ElementRef, ViewChild, Output, EventEmitter, OnChanges, SimpleChange, forwardRef, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor';
|
||||
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/browser/cellToggleMoreActions';
|
||||
import { ICellModel, notebookConstants, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, CellContext } from 'sql/workbench/parts/notebook/browser/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SimpleEditorProgressService } from 'vs/editor/standalone/browser/simpleServices';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { CellTypes } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-component';
|
||||
const MARKDOWN_CLASS = 'markdown';
|
||||
|
||||
@Component({
|
||||
selector: CODE_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./code.component.html'))
|
||||
})
|
||||
export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
|
||||
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
|
||||
|
||||
public get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() public set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this.toolbarElement && value && value.cellType === CellTypes.Markdown) {
|
||||
let nativeToolbar = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
DOM.addClass(nativeToolbar, MARKDOWN_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@Output() public onContentChanged = new EventEmitter<void>();
|
||||
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
this._register(value.kernelChanged(() => {
|
||||
// On kernel change, need to reevaluate the language for each cell
|
||||
// Refresh based on the cell magic (since this is kernel-dependent) and then update using notebook language
|
||||
this.checkForLanguageMagics();
|
||||
this.updateLanguageMode();
|
||||
}));
|
||||
this._register(value.onValidConnectionSelected(() => {
|
||||
this.updateConnectionState(this.isActive());
|
||||
}));
|
||||
}
|
||||
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
@Input() set hover(value: boolean) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
protected _actionBar: Taskbar;
|
||||
private readonly _minimumHeight = 30;
|
||||
private readonly _maximumHeight = 4000;
|
||||
private _cellModel: ICellModel;
|
||||
private _editor: QueryTextEditor;
|
||||
private _editorInput: UntitledEditorInput;
|
||||
private _editorModel: ITextModel;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _layoutEmitter = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IModelService) private _modelService: IModelService,
|
||||
@Inject(IModeService) private _modeService: IModeService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(ILogService) private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
this._register(Event.debounce(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false)
|
||||
(() => this.layout()));
|
||||
// Handle disconnect on removal of the cell, if it was the active cell
|
||||
this._register({ dispose: () => this.updateConnectionState(false) });
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.initActionBar();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
this.updateLanguageMode();
|
||||
this.updateModel();
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
let isActive = this.cellModel.id === changedProp.currentValue;
|
||||
this.updateConnectionState(isActive);
|
||||
this.toggleMoreActionsButton(isActive);
|
||||
if (this._editor) {
|
||||
this._editor.toggleEditorSelected(isActive);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateConnectionState(shouldConnect: boolean) {
|
||||
if (this.isSqlCodeCell()) {
|
||||
let cellUri = this.cellModel.cellUri.toString();
|
||||
let connectionService = this.connectionService;
|
||||
if (!shouldConnect && connectionService && connectionService.isConnected(cellUri)) {
|
||||
connectionService.disconnect(cellUri).catch(e => this.logService.error(e));
|
||||
} else if (shouldConnect && this._model.activeConnection && this._model.activeConnection.id !== '-1') {
|
||||
connectionService.connect(this._model.activeConnection, cellUri).catch(e => this.logService.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get connectionService(): IConnectionManagementService {
|
||||
return this._model && this._model.notebookOptions && this._model.notebookOptions.connectionService;
|
||||
}
|
||||
|
||||
private isSqlCodeCell() {
|
||||
return this._model
|
||||
&& this._model.defaultKernel
|
||||
&& this._model.defaultKernel.display_name === notebookConstants.SQL
|
||||
&& this.cellModel.cellType === CellTypes.Code
|
||||
&& this.cellModel.cellUri;
|
||||
}
|
||||
|
||||
private get destroyed(): boolean {
|
||||
return !!(this._changeRef['destroyed']);
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
this.createEditor();
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
|
||||
this._layoutEmitter.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._layoutEmitter.fire();
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private async createEditor(): Promise<void> {
|
||||
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleEditorProgressService()]));
|
||||
this._editor = instantiationService.createInstance(QueryTextEditor);
|
||||
this._editor.create(this.codeElement.nativeElement);
|
||||
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
|
||||
// due to re-disposing things.
|
||||
// There's no negative impact as at this point the component isn't visible (it was removed from the DOM)
|
||||
return;
|
||||
}
|
||||
this._register(this._editor);
|
||||
this._register(this._editorInput);
|
||||
this._register(this._editorModel.onDidChangeContent(e => {
|
||||
this._editor.setHeightToScrollHeight();
|
||||
this.cellModel.source = this._editorModel.getValue();
|
||||
this.onContentChanged.emit();
|
||||
this.checkForLanguageMagics();
|
||||
// TODO see if there's a better way to handle reassessing size.
|
||||
setTimeout(() => this._layoutEmitter.fire(), 250);
|
||||
}));
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.fontSize')) {
|
||||
this._editor.setHeightToScrollHeight(true);
|
||||
}
|
||||
}));
|
||||
this._register(this.model.layoutChanged(() => this._layoutEmitter.fire(), this));
|
||||
this._register(this.cellModel.onExecutionStateChange(event => {
|
||||
if (event === CellExecutionState.Running && !this.cellModel.stdInVisible) {
|
||||
this.setFocusAndScroll();
|
||||
}
|
||||
}));
|
||||
this.layout();
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._editor.layout(new DOM.Dimension(
|
||||
DOM.getContentWidth(this.codeElement.nativeElement),
|
||||
DOM.getContentHeight(this.codeElement.nativeElement)));
|
||||
this._editor.setHeightToScrollHeight();
|
||||
}
|
||||
|
||||
protected initActionBar() {
|
||||
let context = new CellContext(this.model, this.cellModel);
|
||||
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar);
|
||||
this._actionBar.context = context;
|
||||
this._actionBar.setContent([
|
||||
{ action: runCellAction }
|
||||
]);
|
||||
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
|
||||
/// Editor Functions
|
||||
private updateModel() {
|
||||
if (this._editorModel) {
|
||||
let cellModelSource: string;
|
||||
cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
|
||||
this._modelService.updateModel(this._editorModel, cellModelSource);
|
||||
}
|
||||
}
|
||||
|
||||
private checkForLanguageMagics(): void {
|
||||
try {
|
||||
if (!this.cellModel || this.cellModel.cellType !== CellTypes.Code) {
|
||||
return;
|
||||
}
|
||||
if (this._editorModel && this._editor && this._editorModel.getLineCount() > 1) {
|
||||
// Only try to match once we've typed past the first line
|
||||
let magicName = notebookUtils.tryMatchCellMagic(this._editorModel.getLineContent(1));
|
||||
if (magicName) {
|
||||
let kernelName = this._model.clientSession && this._model.clientSession.kernel ? this._model.clientSession.kernel.name : undefined;
|
||||
let magic = this._model.notebookOptions.cellMagicMapper.toLanguageMagic(magicName, kernelName);
|
||||
if (magic && this.cellModel.language !== magic.language) {
|
||||
this.cellModel.setOverrideLanguage(magic.language);
|
||||
this.updateLanguageMode();
|
||||
}
|
||||
} else {
|
||||
this.cellModel.setOverrideLanguage(undefined);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// No-op for now. Should we log?
|
||||
}
|
||||
}
|
||||
|
||||
private updateLanguageMode(): void {
|
||||
if (this._editorModel && this._editor) {
|
||||
let modeValue = this._modeService.create(this.cellModel.language);
|
||||
this._modelService.setMode(this._editorModel, modeValue);
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
|
||||
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
private setFocusAndScroll(): void {
|
||||
if (this.cellModel.id === this._activeCellId) {
|
||||
this._editor.focus();
|
||||
this._editor.getContainer().scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
92
src/sql/workbench/parts/notebook/browser/cellViews/code.css
Normal file
92
src/sql/workbench/parts/notebook/browser/cellViews/code.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
code-component {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
code-component .toolbar {
|
||||
border-right-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-flow:column;
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
orientation: portrait
|
||||
}
|
||||
|
||||
code-component .toolbar.markdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code-component .toolbar .carbon-taskbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconRun {
|
||||
height: 20px;
|
||||
background-image: url('./media/light/execute_cell.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.vs-dark code-component .toolbarIconRun,
|
||||
.hc-black code-component .toolbarIconRun {
|
||||
background-image: url('./media/dark/execute_cell_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .toolbarIconRunError {
|
||||
height: 20px;
|
||||
background-image: url('./media/light/execute_cell_error.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconStop {
|
||||
height: 20px;
|
||||
background-image: url('./media/light/stop_cell_solidanimation.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.vs-dark code-component .toolbarIconStop,
|
||||
.hc-black code-component .toolbarIconStop {
|
||||
background-image: url('./media/dark/stop_cell_solidanimation_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .editor {
|
||||
padding: 5px 0px 5px 0px
|
||||
}
|
||||
/* overview ruler */
|
||||
code-component .monaco-editor .decorationsOverviewRuler {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon {
|
||||
background-size: 20px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon {
|
||||
width: 0px;
|
||||
padding-left: 0px;
|
||||
padding-top: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountTen {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountHundred {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||
{
|
||||
padding-left: 10px
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { ICellModel, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { MultiStateAction, IMultiStateData } from 'sql/workbench/parts/notebook/browser/notebookActions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again");
|
||||
const emptyExecutionCountLabel = '[ ]';
|
||||
|
||||
function hasModelAndCell(context: CellContext, notificationService: INotificationService): boolean {
|
||||
if (!context || !context.model) {
|
||||
return false;
|
||||
}
|
||||
if (context.cell === undefined) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: notebookMoreActionMsg
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class CellContext {
|
||||
constructor(public model: NotebookModel, private _cell?: ICellModel) {
|
||||
}
|
||||
|
||||
public get cell(): ICellModel {
|
||||
return this._cell ? this._cell : this.model.activeCell;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class CellActionBase extends Action {
|
||||
|
||||
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
|
||||
super(id, label, icon);
|
||||
}
|
||||
|
||||
public canRun(context: CellContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public run(context: CellContext): Promise<boolean> {
|
||||
if (hasModelAndCell(context, this.notificationService)) {
|
||||
return this.doRun(context).then(() => true);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
abstract doRun(context: CellContext): Promise<void>;
|
||||
}
|
||||
|
||||
export class RunCellAction extends MultiStateAction<CellExecutionState> {
|
||||
public static ID = 'notebook.runCell';
|
||||
public static LABEL = 'Run cell';
|
||||
private _executionChangedDisposable: IDisposable;
|
||||
private _context: CellContext;
|
||||
constructor(context: CellContext, @INotificationService private notificationService: INotificationService,
|
||||
@IConnectionManagementService private connectionManagementService: IConnectionManagementService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
super(RunCellAction.ID, new IMultiStateData<CellExecutionState>([
|
||||
{ key: CellExecutionState.Hidden, value: { label: emptyExecutionCountLabel, className: '', tooltip: '', hideIcon: true } },
|
||||
{ key: CellExecutionState.Stopped, value: { label: '', className: 'toolbarIconRun', tooltip: localize('runCell', "Run cell"), commandId: 'notebook.command.runactivecell' } },
|
||||
{ key: CellExecutionState.Running, value: { label: '', className: 'toolbarIconStop', tooltip: localize('stopCell', "Cancel execution") } },
|
||||
{ key: CellExecutionState.Error, value: { label: '', className: 'toolbarIconRunError', tooltip: localize('errorRunCell', "Error on last run. Click to run again") } },
|
||||
], CellExecutionState.Hidden), keybindingService, logService);
|
||||
this.ensureContextIsUpdated(context);
|
||||
}
|
||||
|
||||
public run(context?: CellContext): Promise<boolean> {
|
||||
return this.doRun(context).then(() => true);
|
||||
}
|
||||
|
||||
public async doRun(context: CellContext): Promise<void> {
|
||||
this.ensureContextIsUpdated(context);
|
||||
if (!this._context) {
|
||||
// TODO should we error?
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._context.cell.runCell(this.notificationService, this.connectionManagementService);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureContextIsUpdated(context: CellContext) {
|
||||
if (context && context !== this._context) {
|
||||
if (this._executionChangedDisposable) {
|
||||
this._executionChangedDisposable.dispose();
|
||||
}
|
||||
this._context = context;
|
||||
this.updateStateAndExecutionCount(context.cell.executionState);
|
||||
this._executionChangedDisposable = this._context.cell.onExecutionStateChange((state) => {
|
||||
this.updateStateAndExecutionCount(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateStateAndExecutionCount(state: CellExecutionState) {
|
||||
let label = emptyExecutionCountLabel;
|
||||
let className = '';
|
||||
if (!types.isUndefinedOrNull(this._context.cell.executionCount)) {
|
||||
label = `[${this._context.cell.executionCount}]`;
|
||||
// Heuristic to try and align correctly independent of execution count length. Moving left margin
|
||||
// back by a few px seems to make things "work" OK, but isn't a super clean solution
|
||||
if (label.length === 4) {
|
||||
className = 'execCountTen';
|
||||
} else if (label.length > 4) {
|
||||
className = 'execCountHundred';
|
||||
}
|
||||
}
|
||||
this.states.updateStateData(CellExecutionState.Hidden, (data) => {
|
||||
data.label = label;
|
||||
data.className = className;
|
||||
});
|
||||
this.updateState(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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: 100%; display: flex; flex-flow: column">
|
||||
<div style="flex: 0 0 auto;">
|
||||
<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>
|
||||
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, HostListener } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-cell-component';
|
||||
|
||||
@Component({
|
||||
selector: CODE_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
|
||||
})
|
||||
|
||||
export class CodeCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@Input() cellModel: ICellModel;
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
}
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape', ['$event'])
|
||||
handleKeyboardEvent() {
|
||||
this.cellModel.active = false;
|
||||
this._model.activeCell = undefined;
|
||||
}
|
||||
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
|
||||
public inputDeferred: Deferred<string>;
|
||||
public stdIn: nb.IStdinMessage;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.cellModel) {
|
||||
this._register(this.cellModel.onOutputsChanged(() => {
|
||||
this._changeRef.detectChanges();
|
||||
}));
|
||||
// Register request handler, cleanup on dispose of this component
|
||||
this.cellModel.setStdInHandler({ handle: (msg) => this.handleStdIn(msg) });
|
||||
this._register({ dispose: () => this.cellModel.setStdInHandler(undefined) });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
this._activeCellId = changedProp.currentValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
handleStdIn(msg: nb.IStdinMessage): void | Thenable<void> {
|
||||
if (msg) {
|
||||
this.stdIn = msg;
|
||||
this.inputDeferred = new Deferred();
|
||||
this.cellModel.stdInVisible = true;
|
||||
this._changeRef.detectChanges();
|
||||
return this.awaitStdIn();
|
||||
}
|
||||
}
|
||||
|
||||
private async awaitStdIn(): Promise<void> {
|
||||
try {
|
||||
let value = await this.inputDeferred.promise;
|
||||
this.cellModel.future.sendInputReply({ value: value });
|
||||
} catch (err) {
|
||||
// Note: don't have a better way to handle completing input request. For now just canceling by sending empty string?
|
||||
this.cellModel.future.sendInputReply({ value: '' });
|
||||
} finally {
|
||||
// Clean up so no matter what, the stdIn request goes away
|
||||
this.stdIn = undefined;
|
||||
this.inputDeferred = undefined;
|
||||
this.cellModel.stdInVisible = false;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
get isStdInVisible(): boolean {
|
||||
return this.cellModel.stdInVisible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnDestroy } from '@angular/core';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
|
||||
export abstract class CellView extends AngularDisposable implements OnDestroy {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public abstract layout(): void;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Directive, Inject, HostListener, Input } from '@angular/core';
|
||||
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
|
||||
const knownSchemes = new Set(['http', 'https', 'file', 'mailto', 'data', `${product.urlProtocol}`, 'azuredatastudio', 'azuredatastudio-insiders', 'vscode', 'vscode-insiders', 'vscode-resource']);
|
||||
@Directive({
|
||||
selector: '[link-handler]',
|
||||
})
|
||||
export class LinkHandlerDirective {
|
||||
private workbenchFilePath: URI;
|
||||
@Input() isTrusted: boolean;
|
||||
@Input() notebookUri: URI;
|
||||
|
||||
constructor(
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
@Inject(INotebookService) private readonly notebookService: INotebookService
|
||||
) {
|
||||
this.workbenchFilePath = URI.parse(require.toUrl('vs/code/electron-browser/workbench/workbench.html'));
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onclick(event: MouseEvent): void {
|
||||
// Note: this logic is taken from the VSCode handling of links in markdown
|
||||
// Untrusted cells will not support commands or raw HTML tags
|
||||
// Finally, we should consider supporting relative paths - created #5238 to track
|
||||
let target: HTMLElement = event.target as HTMLElement;
|
||||
if (target.tagName !== 'A') {
|
||||
target = target.parentElement;
|
||||
if (!target || target.tagName !== 'A') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const href = target['href'];
|
||||
if (href) {
|
||||
this.handleLink(href);
|
||||
}
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
} finally {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private handleLink(content: string): void {
|
||||
let uri: URI | undefined;
|
||||
try {
|
||||
uri = URI.parse(content);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (uri && this.openerService && this.isSupportedLink(uri)) {
|
||||
if (uri.fragment && uri.fragment.length > 0 && uri.path === this.workbenchFilePath.path) {
|
||||
this.notebookService.navigateTo(this.notebookUri, uri.fragment);
|
||||
} else {
|
||||
this.openerService.open(uri).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isSupportedLink(link: URI): boolean {
|
||||
if (knownSchemes.has(link.scheme)) {
|
||||
return true;
|
||||
}
|
||||
return !!this.isTrusted && link.scheme === 'command';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>execute_cell_inverse </title><circle class="cls-1" cx="8" cy="7.92" r="7.76"/><polygon points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,16 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs><style>.cls-1{fill:#c1d6e6;}.cls-2{fill:#0078d4;}.cls-3{fill:#fff;}</style></defs>
|
||||
<title>stop_cell_solidanimation_inverse</title>
|
||||
<path class="cls-1" d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/>
|
||||
<path class="cls-2" d="M8.51,0v1A7,7,0,0,1,15,8a6.87,6.87,0,0,1-1.07,3.7l.81.64A7.92,7.92,0,0,0,16,8,8,8,0,0,0,8.51,0Z">
|
||||
<animateTransform attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 8 8"
|
||||
to="360 8 8"
|
||||
begin="0s"
|
||||
dur="1.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
<circle class="cls-3" cx="8" cy="8" r="6.32"/>
|
||||
<rect x="4.91" y="4.91" width="6.18" height="6.18"/></svg>
|
||||
|
After Width: | Height: | Size: 774 B |
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs2015.css
|
||||
*/
|
||||
/*
|
||||
* Visual Studio 2015 dark style
|
||||
* Author: Nicolas LLOBERA <nllobera@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
.notebook-preview .hljs-keyword,
|
||||
.notebook-preview .hljs-literal,
|
||||
.notebook-preview .hljs-symbol,
|
||||
.notebook-preview .hljs-name {
|
||||
color: #569CD6;
|
||||
}
|
||||
.notebook-preview .hljs-link {
|
||||
color: #569CD6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-built_in,
|
||||
.notebook-preview .hljs-type {
|
||||
color: #4EC9B0;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-number,
|
||||
.notebook-preview .hljs-class {
|
||||
color: #B8D7A3;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-string,
|
||||
.notebook-preview .hljs-meta-string {
|
||||
color: #D69D85;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-regexp,
|
||||
.notebook-preview .hljs-template-tag {
|
||||
color: #9A5334;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-subst,
|
||||
.notebook-preview .hljs-function,
|
||||
.notebook-preview .hljs-title,
|
||||
.notebook-preview .hljs-params,
|
||||
.notebook-preview .hljs-formula {
|
||||
color: #DCDCDC;
|
||||
}
|
||||
|
||||
.notebook-preview pre code .hljs-subst,
|
||||
.notebook-preview pre code .hljs-function,
|
||||
.notebook-preview pre code .hljs-title,
|
||||
.notebook-preview pre code .hljs-params,
|
||||
.notebook-preview pre code .hljs-formula {
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
|
||||
.notebook-preview .hljs-comment,
|
||||
.notebook-preview .hljs-quote {
|
||||
color: #57A64A;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-doctag {
|
||||
color: #608B4E;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-meta,
|
||||
.notebook-preview .hljs-meta-keyword,
|
||||
.notebook-preview .hljs-tag {
|
||||
color: #9B9B9B;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-variable,
|
||||
.notebook-preview .hljs-template-variable {
|
||||
color: #BD63C5;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-attr,
|
||||
.notebook-preview .hljs-attribute,
|
||||
.notebook-preview .hljs-builtin-name {
|
||||
color: #9CDCFE;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-section {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*.hljs-code {
|
||||
font-family:'Monospace';
|
||||
}*/
|
||||
|
||||
.notebook-preview .hljs-bullet,
|
||||
.notebook-preview .hljs-selector-tag,
|
||||
.notebook-preview .hljs-selector-id,
|
||||
.notebook-preview .hljs-selector-class,
|
||||
.notebook-preview .hljs-selector-attr,
|
||||
.notebook-preview .hljs-selector-pseudo {
|
||||
color: #D7BA7D;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-addition {
|
||||
background-color: var(--vscode-diffEditor-insertedTextBackground, rgba(155, 185, 85, 0.2));
|
||||
color: rgb(155, 185, 85);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-deletion {
|
||||
background: var(--vscode-diffEditor-removedTextBackground, rgba(255, 0, 0, 0.2));
|
||||
color: rgb(255, 0, 0);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
From https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs.css
|
||||
*/
|
||||
/*
|
||||
|
||||
Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
|
||||
|
||||
*/
|
||||
|
||||
.notebook-preview .hljs-function,
|
||||
.notebook-preview .hljs-params {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-comment,
|
||||
.notebook-preview .hljs-quote,
|
||||
.notebook-preview .hljs-variable {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-keyword,
|
||||
.notebook-preview .hljs-selector-tag,
|
||||
.notebook-preview .hljs-built_in,
|
||||
.notebook-preview .hljs-name,
|
||||
.notebook-preview .hljs-tag {
|
||||
color: #00f;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-string,
|
||||
.notebook-preview .hljs-title,
|
||||
.notebook-preview .hljs-section,
|
||||
.notebook-preview .hljs-attribute,
|
||||
.notebook-preview .hljs-literal,
|
||||
.notebook-preview .hljs-template-tag,
|
||||
.notebook-preview .hljs-template-variable,
|
||||
.notebook-preview .hljs-type {
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-selector-attr,
|
||||
.notebook-preview .hljs-selector-pseudo,
|
||||
.notebook-preview .hljs-meta {
|
||||
color: #2b91af;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-doctag {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-attr {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-symbol,
|
||||
.notebook-preview .hljs-bullet,
|
||||
.notebook-preview .hljs-link {
|
||||
color: #00b0e8;
|
||||
}
|
||||
|
||||
|
||||
.notebook-preview .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notebook-preview .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>execute_cell</title><circle cx="8" cy="7.92" r="7.76"/><polygon class="cls-1" points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#d02e00;}.cls-2{fill:#fff;}</style></defs><title>execute_cell_error</title><circle class="cls-1" cx="8" cy="7.92" r="7.76"/><polygon class="cls-2" points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,16 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs><style>.cls-1{fill:#c1d6e6;}.cls-2{fill:#0078d4;}.cls-3{fill:#fff;}</style></defs>
|
||||
<title>stop_cell_solidanimation</title>
|
||||
<path class="cls-1" d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/>
|
||||
<path class="cls-2" d="M8.51,0v1A7,7,0,0,1,15,8a6.87,6.87,0,0,1-1.07,3.7l.81.64A7.92,7.92,0,0,0,16,8,8,8,0,0,0,8.51,0Z">
|
||||
<animateTransform attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 8 8"
|
||||
to="360 8 8"
|
||||
begin="0s"
|
||||
dur="1.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
<circle cx="8" cy="8" r="6.32"/>
|
||||
<rect class="cls-3" x="4.91" y="4.91" width="6.18" height="6.18"/></svg>
|
||||
|
After Width: | Height: | Size: 766 B |
@@ -0,0 +1,231 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.notebook-preview {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.notebook-preview #code-csp-warning {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
margin: 16px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
background-color:#444444;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
.notebook-preview #code-csp-warning:hover {
|
||||
text-decoration: none;
|
||||
background-color:#007acc;
|
||||
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
|
||||
.notebook-preview .scrollBeyondLastLine {
|
||||
margin-bottom: calc(100vh - 22px);
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection .code-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection .code-active-line:before,
|
||||
.notebook-preview .showEditorSelection .code-line:hover:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection li.code-active-line:before,
|
||||
.notebook-preview .showEditorSelection li.code-line:hover:before {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.40);
|
||||
}
|
||||
|
||||
.notebook-preview .showEditorSelection .code-line .code-line:hover:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview .showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview .showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.60);
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.hc-black .notebook-preview .showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(255, 160, 0, 0.7);
|
||||
}
|
||||
|
||||
.hc-black .notebook-preview .showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(255, 160, 0, 1);
|
||||
}
|
||||
|
||||
.hc-black .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.notebook-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.notebookEditor a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notebookEditor a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notebook-preview a:focus,
|
||||
.notebook-preview input:focus,
|
||||
.notebook-preview select:focus,
|
||||
.notebook-preview textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.notebook-preview hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.notebook-preview h1 {
|
||||
padding-bottom: 0.3em;
|
||||
line-height: 1.2;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.notebook-preview h1, .notebook-preview h2, .notebook-preview h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.notebook-preview h1 code,
|
||||
.notebook-preview h2 code,
|
||||
.notebook-preview h3 code,
|
||||
.notebook-preview h4 code,
|
||||
.notebook-preview h5 code,
|
||||
.notebook-preview h6 code {
|
||||
font-size: inherit;
|
||||
line-height: auto;
|
||||
}
|
||||
|
||||
.notebook-preview table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.notebook-preview table > thead > tr > th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.notebook-preview table > thead > tr > th,
|
||||
.notebook-preview table > thead > tr > td,
|
||||
.notebook-preview table > tbody > tr > th,
|
||||
.notebook-preview .notebook-preview table > tbody > tr > td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.notebook-preview table > tbody > tr + tr > td {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.notebook-preview blockquote {
|
||||
margin: 0 7px 0 5px;
|
||||
padding: 0 16px 0 10px;
|
||||
border-left-width: 5px;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
.notebook-preview code {
|
||||
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.notebook-preview pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.notebook-preview .mac code {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.notebook-preview pre:not(.hljs),
|
||||
.notebook-preview pre.hljs code > div {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/** Theming */
|
||||
|
||||
.notebook-preview pre code {
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
|
||||
.notebook-preview pre {
|
||||
background-color: rgba(220, 220, 220, 0.4);
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview pre {
|
||||
background-color: rgba(10, 10, 10, 0.4);
|
||||
}
|
||||
|
||||
.hc-black .notebook-preview pre {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.hc-black .notebook-preview h1 {
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.notebook-preview table > thead > tr > th {
|
||||
border-color: rgba(0, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview table > thead > tr > th {
|
||||
border-color: rgba(255, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.notebook-preview h1,
|
||||
.notebook-preview hr,
|
||||
.notebook-preview table > tbody > tr + tr > td {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.vs-dark .notebook-preview h1,
|
||||
.vs-dark .notebook-preview hr,
|
||||
.vs-dark .notebook-preview table > tbody > tr + tr > td {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| RenderedText
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
output-component .jp-RenderedText {
|
||||
text-align: left;
|
||||
padding-left: var(--jp-code-padding);
|
||||
font-size: var(--jp-code-font-size);
|
||||
line-height: var(--jp-code-line-height);
|
||||
font-family: var(--jp-code-font-family);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedText pre,
|
||||
.jp-RenderedJavaScript pre,
|
||||
output-component .jp-RenderedHTMLCommon pre {
|
||||
color: var(--jp-content-font-color1);
|
||||
border: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
/* ansi_up creates classed spans for console foregrounds and backgrounds. */
|
||||
output-component .jp-RenderedText pre .ansi-black-fg {
|
||||
color: #3e424d;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-red-fg {
|
||||
color: #e75c58;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-green-fg {
|
||||
color: #00a250;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-yellow-fg {
|
||||
color: #ddb62b;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-blue-fg {
|
||||
color: #208ffb;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-magenta-fg {
|
||||
color: #d160c4;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-cyan-fg {
|
||||
color: #60c6c8;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-white-fg {
|
||||
color: #c5c1b4;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedText pre .ansi-black-bg {
|
||||
background-color: #3e424d;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-red-bg {
|
||||
background-color: #e75c58;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-green-bg {
|
||||
background-color: #00a250;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-yellow-bg {
|
||||
background-color: #ddb62b;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-blue-bg {
|
||||
background-color: #208ffb;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-magenta-bg {
|
||||
background-color: #d160c4;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-cyan-bg {
|
||||
background-color: #60c6c8;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-white-bg {
|
||||
background-color: #c5c1b4;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedText pre .ansi-bright-black-fg {
|
||||
color: #282c36;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-red-fg {
|
||||
color: #b22b31;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-green-fg {
|
||||
color: #007427;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-yellow-fg {
|
||||
color: #b27d12;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-blue-fg {
|
||||
color: #0065ca;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-magenta-fg {
|
||||
color: #a03196;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-cyan-fg {
|
||||
color: #258f8f;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-white-fg {
|
||||
color: #a1a6b2;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedText pre .ansi-bright-black-bg {
|
||||
background-color: #282c36;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-red-bg {
|
||||
background-color: #b22b31;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-green-bg {
|
||||
background-color: #007427;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-yellow-bg {
|
||||
background-color: #b27d12;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-blue-bg {
|
||||
background-color: #0065ca;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-magenta-bg {
|
||||
background-color: #a03196;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-cyan-bg {
|
||||
background-color: #258f8f;
|
||||
}
|
||||
output-component .jp-RenderedText pre .ansi-bright-white-bg {
|
||||
background-color: #a1a6b2;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedText[data-mime-type='application/vnd.jupyter.stderr'] {
|
||||
background: var(--jp-rendermime-error-background);
|
||||
padding-top: var(--jp-code-padding);
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| RenderedLatex
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
.jp-RenderedLatex {
|
||||
color: var(--jp-content-font-color1);
|
||||
font-size: var(--jp-content-font-size1);
|
||||
line-height: var(--jp-content-line-height);
|
||||
}
|
||||
|
||||
/* Left-justify outputs.*/
|
||||
.jp-OutputArea-output.jp-RenderedLatex {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| RenderedHTML
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
output-component .jp-RenderedHTMLCommon {
|
||||
color: var(--jp-content-font-color1);
|
||||
font-family: var(--jp-content-font-family);
|
||||
font-size: var(--jp-content-font-size1);
|
||||
line-height: var(--jp-content-line-height);
|
||||
/* Give a bit more R padding on Markdown text to keep line lengths reasonable */
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h1,
|
||||
output-component .jp-RenderedHTMLCommon h2,
|
||||
output-component .jp-RenderedHTMLCommon h3,
|
||||
output-component .jp-RenderedHTMLCommon h4,
|
||||
output-component .jp-RenderedHTMLCommon h5,
|
||||
output-component .jp-RenderedHTMLCommon h6 {
|
||||
line-height: var(--jp-content-heading-line-height);
|
||||
font-weight: var(--jp-content-heading-font-weight);
|
||||
font-style: normal;
|
||||
margin: var(--jp-content-heading-margin-top) 0
|
||||
var(--jp-content-heading-margin-bottom) 0;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h1:first-child,
|
||||
output-component .jp-RenderedHTMLCommon h2:first-child,
|
||||
output-component .jp-RenderedHTMLCommon h3:first-child,
|
||||
output-component .jp-RenderedHTMLCommon h4:first-child,
|
||||
output-component .jp-RenderedHTMLCommon h5:first-child,
|
||||
output-component .jp-RenderedHTMLCommon h6:first-child {
|
||||
margin-top: calc(0.5 * var(--jp-content-heading-margin-top));
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h1:last-child,
|
||||
output-component .jp-RenderedHTMLCommon h2:last-child,
|
||||
output-component .jp-RenderedHTMLCommon h3:last-child,
|
||||
output-component .jp-RenderedHTMLCommon h4:last-child,
|
||||
output-component .jp-RenderedHTMLCommon h5:last-child,
|
||||
output-component .jp-RenderedHTMLCommon h6:last-child {
|
||||
margin-bottom: calc(0.5 * var(--jp-content-heading-margin-bottom));
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h1 {
|
||||
font-size: var(--jp-content-font-size5);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h2 {
|
||||
font-size: var(--jp-content-font-size4);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h3 {
|
||||
font-size: var(--jp-content-font-size3);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h4 {
|
||||
font-size: var(--jp-content-font-size2);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h5 {
|
||||
font-size: var(--jp-content-font-size1);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon h6 {
|
||||
font-size: var(--jp-content-font-size0);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ul:not(.list-inline),
|
||||
output-component .jp-RenderedHTMLCommon ol:not(.list-inline) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ul ul {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ul ul ul {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol ol {
|
||||
list-style: upper-alpha;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol ol ol {
|
||||
list-style: lower-alpha;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol ol ol ol {
|
||||
list-style: lower-roman;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol ol ol ol ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ol,
|
||||
output-component .jp-RenderedHTMLCommon ul {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon ul ul,
|
||||
output-component .jp-RenderedHTMLCommon ul ol,
|
||||
output-component .jp-RenderedHTMLCommon ol ul,
|
||||
output-component .jp-RenderedHTMLCommon ol ol {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon hr {
|
||||
color: var(--jp-border-color2);
|
||||
background-color: var(--jp-border-color1);
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon > pre {
|
||||
margin: 1.5em 2em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon pre,
|
||||
output-component .jp-RenderedHTMLCommon code {
|
||||
border: 0;
|
||||
background-color: var(--jp-layout-color0);
|
||||
color: var(--jp-content-font-color1);
|
||||
font-family: var(--jp-code-font-family);
|
||||
font-size: inherit;
|
||||
line-height: var(--jp-code-line-height);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon p > code {
|
||||
background-color: var(--jp-layout-color2);
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
output-component .jp-RenderedHTMLCommon table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: none;
|
||||
color: var(--jp-ui-font-color1);
|
||||
font-size: 12px;
|
||||
table-layout: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon thead {
|
||||
border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon td,
|
||||
output-component .jp-RenderedHTMLCommon th,
|
||||
output-component .jp-RenderedHTMLCommon tr {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
padding: 0.5em 0.5em;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
max-width: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.jp-RenderedMarkdown.jp-RenderedHTMLCommon td,
|
||||
.jp-RenderedMarkdown.jp-RenderedHTMLCommon th {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
output-component th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon tbody tr:nth-child(odd) {
|
||||
background: var(--jp-layout-color0);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon tbody tr:nth-child(even) {
|
||||
background: var(--jp-rendermime-table-row-background);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon tbody tr:hover {
|
||||
background: var(--jp-rendermime-table-row-hover-background);
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon table {
|
||||
margin-bottom: 1em;
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon p {
|
||||
text-align: left;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon img {
|
||||
-moz-force-broken-image-icon: 1;
|
||||
}
|
||||
|
||||
/* Restrict to direct children as other images could be nested in other content. */
|
||||
output-component .jp-RenderedHTMLCommon > img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* Change color behind transparent images if they need it... */
|
||||
[data-theme-light='false'] .jp-RenderedImage img.jp-needs-light-background {
|
||||
background-color: var(--jp-inverse-layout-color1);
|
||||
}
|
||||
[data-theme-light='true'] .jp-RenderedImage img.jp-needs-dark-background {
|
||||
background-color: var(--jp-inverse-layout-color1);
|
||||
}
|
||||
/* ...or leave it untouched if they don't */
|
||||
[data-theme-light='false'] .jp-RenderedImage img.jp-needs-dark-background {
|
||||
}
|
||||
[data-theme-light='true'] .jp-RenderedImage img.jp-needs-light-background {
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon img,
|
||||
.jp-RenderedImage img,
|
||||
output-component .jp-RenderedHTMLCommon svg,
|
||||
.jp-RenderedSVG svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon img.jp-mod-unconfined,
|
||||
.jp-RenderedImage img.jp-mod-unconfined,
|
||||
output-component .jp-RenderedHTMLCommon svg.jp-mod-unconfined,
|
||||
.jp-RenderedSVG svg.jp-mod-unconfined {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon .alert {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
output-component .jp-RenderedHTMLCommon blockquote {
|
||||
margin: 1em 2em;
|
||||
padding: 0 1em;
|
||||
border-left: 5px solid var(--jp-border-color2);
|
||||
}
|
||||
|
||||
a.jp-InternalAnchorLink {
|
||||
visibility: hidden;
|
||||
margin-left: 8px;
|
||||
color: var(--md-blue-800);
|
||||
}
|
||||
|
||||
h1:hover .jp-InternalAnchorLink,
|
||||
h2:hover .jp-InternalAnchorLink,
|
||||
h3:hover .jp-InternalAnchorLink,
|
||||
h4:hover .jp-InternalAnchorLink,
|
||||
h5:hover .jp-InternalAnchorLink,
|
||||
h6:hover .jp-InternalAnchorLink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Most direct children of .jp-RenderedHTMLCommon have a margin-bottom of 1.0.
|
||||
* At the bottom of cells this is a bit too much as there is also spacing
|
||||
* between cells. Going all the way to 0 gets too tight between markdown and
|
||||
* code cells.
|
||||
*/
|
||||
output-component .jp-RenderedHTMLCommon > *:last-child {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| RenderedPDF
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
.jp-RenderedPDF {
|
||||
font-size: var(--jp-ui-font-size1);
|
||||
}
|
||||
|
||||
plotly-output .plotly-wrapper {
|
||||
display: block;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div style="flex: 0 0 auto; user-select: none;">
|
||||
<div #output class="output-userselect">
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,187 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'vs/css!./media/output';
|
||||
|
||||
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, SimpleChange, AfterViewInit, forwardRef, ChangeDetectorRef, ComponentRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { nb } from 'azdata';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import * as outputProcessor from 'sql/workbench/parts/notebook/common/models/outputProcessor';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/browser/core/componentHost.directive';
|
||||
import { Extensions, IMimeComponent, IMimeComponentRegistry } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
export const OUTPUT_SELECTOR: string = 'output-component';
|
||||
const USER_SELECT_CLASS = 'actionselect';
|
||||
|
||||
const componentRegistry = <IMimeComponentRegistry>Registry.as(Extensions.MimeComponentContribution);
|
||||
|
||||
@Component({
|
||||
selector: OUTPUT_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./output.component.html'))
|
||||
})
|
||||
export class OutputComponent extends AngularDisposable implements OnInit, AfterViewInit {
|
||||
@ViewChild('output', { read: ElementRef }) private outputElement: ElementRef;
|
||||
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
|
||||
@Input() cellOutput: nb.ICellOutput;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
private _trusted: boolean;
|
||||
private _initialized: boolean = false;
|
||||
private _activeCellId: string;
|
||||
private _componentInstance: IMimeComponent;
|
||||
public errorText: string;
|
||||
|
||||
constructor(
|
||||
@Inject(IThemeService) private _themeService: IThemeService,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
|
||||
this.loadComponent();
|
||||
this.layout();
|
||||
this._initialized = true;
|
||||
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
|
||||
(() => this.layout()));
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.updateTheme(this._themeService.getTheme());
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
this.toggleUserSelect(this.isActive());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleUserSelect(userSelect: boolean): void {
|
||||
if (!this.nativeOutputElement) {
|
||||
return;
|
||||
}
|
||||
if (userSelect) {
|
||||
DOM.addClass(this.nativeOutputElement, USER_SELECT_CLASS);
|
||||
} else {
|
||||
DOM.removeClass(this.nativeOutputElement, USER_SELECT_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
private get nativeOutputElement() {
|
||||
return this.outputElement ? this.outputElement.nativeElement : undefined;
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (this.componentInstance && this.componentInstance.layout) {
|
||||
this.componentInstance.layout();
|
||||
}
|
||||
}
|
||||
|
||||
private get componentInstance(): IMimeComponent {
|
||||
if (!this._componentInstance) {
|
||||
this.loadComponent();
|
||||
}
|
||||
return this._componentInstance;
|
||||
}
|
||||
|
||||
get trustedMode(): boolean {
|
||||
return this._trusted;
|
||||
}
|
||||
|
||||
@Input() set trustedMode(value: boolean) {
|
||||
this._trusted = value;
|
||||
if (this._initialized) {
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
public hasError(): boolean {
|
||||
return !types.isUndefinedOrNull(this.errorText);
|
||||
}
|
||||
private updateTheme(theme: ITheme): void {
|
||||
let el = <HTMLElement>this._ref.nativeElement;
|
||||
let backgroundColor = theme.getColor(colors.editorBackground, true);
|
||||
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
|
||||
|
||||
if (backgroundColor) {
|
||||
el.style.backgroundColor = backgroundColor.toString();
|
||||
}
|
||||
if (foregroundColor) {
|
||||
el.style.color = foregroundColor.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private loadComponent(): void {
|
||||
let options = outputProcessor.getBundleOptions({ value: this.cellOutput, trusted: this.trustedMode });
|
||||
options.themeService = this._themeService;
|
||||
let mimeType = componentRegistry.getPreferredMimeType(
|
||||
options.data,
|
||||
options.trusted ? 'any' : 'ensure'
|
||||
);
|
||||
this.errorText = undefined;
|
||||
if (!mimeType) {
|
||||
this.errorText = localize('noMimeTypeFound', "No {0}renderer could be found for output. It has the following MIME types: {1}",
|
||||
options.trusted ? '' : localize('safe', "safe "),
|
||||
Object.keys(options.data).join(', '));
|
||||
return;
|
||||
}
|
||||
let selector = componentRegistry.getCtorFromMimeType(mimeType);
|
||||
if (!selector) {
|
||||
this.errorText = localize('noSelectorFound', "No component could be found for selector {0}", mimeType);
|
||||
return;
|
||||
}
|
||||
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
|
||||
|
||||
let viewContainerRef = this.componentHost.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
let componentRef: ComponentRef<IMimeComponent>;
|
||||
try {
|
||||
componentRef = viewContainerRef.createComponent(componentFactory, 0);
|
||||
this._componentInstance = componentRef.instance;
|
||||
this._componentInstance.mimeType = mimeType;
|
||||
this._componentInstance.cellModel = this.cellModel;
|
||||
this._componentInstance.bundleOptions = options;
|
||||
this._changeref.detectChanges();
|
||||
let el = <HTMLElement>componentRef.location.nativeElement;
|
||||
|
||||
// set widget styles to conform to its box
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.position = 'relative';
|
||||
} catch (e) {
|
||||
this.errorText = localize('componentRenderError', "Error rendering component: {0}", getErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div #outputarea link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-output" style="flex: 0 0 auto;">
|
||||
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" [cellModel]="cellModel" [activeCellId]="activeCellId">
|
||||
</output-component>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'vs/css!./outputArea';
|
||||
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef } from '@angular/core';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
|
||||
|
||||
@Component({
|
||||
selector: OUTPUT_AREA_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./outputArea.component.html'))
|
||||
})
|
||||
export class OutputAreaComponent extends AngularDisposable implements OnInit {
|
||||
@ViewChild('outputarea', { read: ElementRef }) private outputArea: ElementRef;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
private _activeCellId: string;
|
||||
|
||||
constructor(
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
if (this.cellModel) {
|
||||
this._register(this.cellModel.onOutputsChanged(e => {
|
||||
if (!(this._changeRef['destroyed'])) {
|
||||
this._changeRef.detectChanges();
|
||||
if (e && e.shouldScroll) {
|
||||
this.setFocusAndScroll(this.outputArea.nativeElement);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public get isTrusted(): boolean {
|
||||
return this.cellModel.trustedMode;
|
||||
}
|
||||
|
||||
public get notebookUri(): URI {
|
||||
return this.cellModel.notebookModel.notebookUri;
|
||||
}
|
||||
|
||||
private setFocusAndScroll(node: HTMLElement): void {
|
||||
if (node) {
|
||||
node.focus();
|
||||
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let outputElement = <HTMLElement>this.outputArea.nativeElement;
|
||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
output-area-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
output-area-component .notebook-output {
|
||||
border-top-width: 0px;
|
||||
user-select: text;
|
||||
padding: 5px 20px 0px;
|
||||
}
|
||||
|
||||
.output-userselect.actionselect {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.output-userselect pre{
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
placeholder-cell-component {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.14);
|
||||
}
|
||||
|
||||
placeholder-cell-component .text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
-webkit-margin-before: 0em;
|
||||
-webkit-margin-after: 0em;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div class="placeholder-cell-component" style="flex: 0 0 auto;">
|
||||
<div class="placeholder-cell-component text">
|
||||
<p>{{clickOn}} <a href="#" (click)="addCell('code', $event)">{{plusCode}}</a> {{or}} <a href="#" (click)="addCell('markdown', $event)">{{plusText}}</a> {{toAddCell}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./placeholder';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
|
||||
|
||||
export const PLACEHOLDER_SELECTOR: string = 'placeholder-cell-component';
|
||||
|
||||
@Component({
|
||||
selector: PLACEHOLDER_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./placeholderCell.component.html'))
|
||||
})
|
||||
|
||||
export class PlaceholderCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@Input() cellModel: ICellModel;
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
}
|
||||
|
||||
private _model: NotebookModel;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.cellModel) {
|
||||
this._register(this.cellModel.onOutputsChanged(() => {
|
||||
this._changeRef.detectChanges();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get clickOn(): string {
|
||||
return localize('clickOn', "Click on");
|
||||
}
|
||||
|
||||
get plusCode(): string {
|
||||
return localize('plusCode', "+ Code");
|
||||
}
|
||||
|
||||
get or(): string {
|
||||
return localize('or', "or");
|
||||
}
|
||||
|
||||
get plusText(): string {
|
||||
return localize('plusText', "+ Text");
|
||||
}
|
||||
|
||||
get toAddCell(): string {
|
||||
return localize('toAddCell', "to add a code or text cell");
|
||||
}
|
||||
|
||||
public addCell(cellType: string, event?: Event): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
let type: CellType = <CellType>cellType;
|
||||
if (!type) {
|
||||
type = 'code';
|
||||
}
|
||||
this._model.addCell(<CellType>cellType);
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./stdin';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, ChangeDetectorRef, forwardRef,
|
||||
ViewChild, ElementRef, AfterViewInit, HostListener
|
||||
} from '@angular/core';
|
||||
import { nb } from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { inputBackground, inputBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { ICellModel, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
|
||||
export const STDIN_SELECTOR: string = 'stdin-component';
|
||||
@Component({
|
||||
selector: STDIN_SELECTOR,
|
||||
template: `
|
||||
<div class="prompt">{{prompt}}</div>
|
||||
<div #input class="input"></div>
|
||||
`
|
||||
})
|
||||
export class StdInComponent extends AngularDisposable implements AfterViewInit {
|
||||
private _input: InputBox;
|
||||
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
|
||||
|
||||
@Input() stdIn: nb.IStdinMessage;
|
||||
@Input() onSendInput: Deferred<string>;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private changeRef: ChangeDetectorRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||
@Inject(forwardRef(() => ElementRef)) private el: ElementRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
let inputOptions: IInputOptions = {
|
||||
placeholder: '',
|
||||
ariaLabel: this.prompt
|
||||
};
|
||||
this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions);
|
||||
if (this.password) {
|
||||
this._input.inputElement.type = 'password';
|
||||
}
|
||||
this._register(this._input);
|
||||
this._register(attachInputBoxStyler(this._input, this.themeService, {
|
||||
inputValidationInfoBackground: inputBackground,
|
||||
inputValidationInfoBorder: inputBorder,
|
||||
}));
|
||||
if (this.cellModel) {
|
||||
this._register(this.cellModel.onExecutionStateChange((status) => this.handleExecutionChange(status)));
|
||||
}
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
public handleKeyboardInput(event: KeyboardEvent): void {
|
||||
let e = new StandardKeyboardEvent(event);
|
||||
switch (e.keyCode) {
|
||||
case KeyCode.Enter:
|
||||
// Indi
|
||||
if (this.onSendInput) {
|
||||
this.onSendInput.resolve(this._input.value);
|
||||
}
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case KeyCode.Escape:
|
||||
if (this.onSendInput) {
|
||||
this.onSendInput.reject('');
|
||||
}
|
||||
e.stopPropagation();
|
||||
break;
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleExecutionChange(status: CellExecutionState): void {
|
||||
if (status !== CellExecutionState.Running && this.onSendInput) {
|
||||
this.onSendInput.reject('');
|
||||
}
|
||||
}
|
||||
|
||||
private get prompt(): string {
|
||||
if (this.stdIn && this.stdIn.content && this.stdIn.content.prompt) {
|
||||
return this.stdIn.content.prompt;
|
||||
}
|
||||
return localize('stdInLabel', "StdIn:");
|
||||
}
|
||||
|
||||
private get password(): boolean {
|
||||
return this.stdIn && this.stdIn.content && this.stdIn.content.password;
|
||||
}
|
||||
}
|
||||
18
src/sql/workbench/parts/notebook/browser/cellViews/stdin.css
Normal file
18
src/sql/workbench/parts/notebook/browser/cellViews/stdin.css
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
stdin-component {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
stdin-component .prompt {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
stdin-component .input {
|
||||
flex: 1 1 auto;
|
||||
padding-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column" (mouseover)="hover=true" (mouseleave)="hover=false">
|
||||
<div class="notebook-text" style="flex: 0 0 auto;">
|
||||
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId">
|
||||
</code-component>
|
||||
</div>
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div #preview link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto" (dblclick)="toggleEditMode()">
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,265 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./textCell';
|
||||
import 'vs/css!./media/markdown';
|
||||
import 'vs/css!./media/highlight';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener } from '@angular/core';
|
||||
import * as path from 'path';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/browser/outputs/notebookMarkdown';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/browser/outputs/sanitizer';
|
||||
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/browser/cellToggleMoreActions';
|
||||
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
|
||||
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
|
||||
|
||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
const USER_SELECT_CLASS = 'actionselect';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: TEXT_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
|
||||
})
|
||||
export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
}
|
||||
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
@Input() set hover(value: boolean) {
|
||||
this._hover = value;
|
||||
if (!this.isActive()) {
|
||||
// Only make a change if we're not active, since this has priority
|
||||
this.updateMoreActions();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape', ['$event'])
|
||||
handleKeyboardEvent() {
|
||||
if (this.isEditMode) {
|
||||
this.toggleEditMode(false);
|
||||
}
|
||||
this.cellModel.active = false;
|
||||
this._model.activeCell = undefined;
|
||||
}
|
||||
|
||||
private _content: string | string[];
|
||||
private _lastTrustedMode: boolean;
|
||||
private isEditMode: boolean;
|
||||
private _sanitizer: ISanitizer;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private readonly _onDidClickLink = this._register(new Emitter<URI>());
|
||||
public readonly onDidClickLink = this._onDidClickLink.event;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _hover: boolean;
|
||||
private markdownRenderer: NotebookMarkdownRenderer;
|
||||
private markdownResult: IMarkdownRenderResult;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
@Inject(IConfigurationService) private configurationService: IConfigurationService,
|
||||
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = true;
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
|
||||
this._register(toDisposable(() => {
|
||||
if (this.markdownResult) {
|
||||
this.markdownResult.dispose();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
//Gets sanitizer from ISanitizer interface
|
||||
private get sanitizer(): ISanitizer {
|
||||
if (this._sanitizer) {
|
||||
return this._sanitizer;
|
||||
}
|
||||
return this._sanitizer = defaultSanitizer;
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.cellModel.loaded = !isLoading;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
|
||||
this.setFocusAndScroll();
|
||||
this._register(this.cellModel.onOutputsChanged(e => {
|
||||
this.updatePreview();
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
this._activeCellId = changedProp.currentValue;
|
||||
this.toggleUserSelect(this.isActive());
|
||||
// If the activeCellId is undefined (i.e. in an active cell update), don't unnecessarily set editMode to false;
|
||||
// it will be set to true in a subsequent call to toggleEditMode()
|
||||
if (changedProp.previousValue !== undefined) {
|
||||
this.toggleEditMode(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get isTrusted(): boolean {
|
||||
return this.model.trustedMode;
|
||||
}
|
||||
|
||||
public get notebookUri(): URI {
|
||||
return this.model.notebookUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the preview of markdown component with latest changes
|
||||
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
|
||||
* Sanitizes the data to be shown in markdown cell
|
||||
*/
|
||||
private updatePreview(): void {
|
||||
let trustedChanged = this.cellModel && this._lastTrustedMode !== this.cellModel.trustedMode;
|
||||
let contentChanged = this._content !== this.cellModel.source || this.cellModel.source.length === 0;
|
||||
if (trustedChanged || contentChanged) {
|
||||
this._lastTrustedMode = this.cellModel.trustedMode;
|
||||
if (!this.cellModel.source && !this.isEditMode) {
|
||||
this._content = localize('doubleClickEdit', "Double-click to edit");
|
||||
} else {
|
||||
this._content = this.cellModel.source;
|
||||
}
|
||||
|
||||
if (useInProcMarkdown(this.configurationService)) {
|
||||
this.markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
|
||||
this.markdownResult = this.markdownRenderer.render({
|
||||
isTrusted: true,
|
||||
value: Array.isArray(this._content) ? this._content.join('') : this._content
|
||||
});
|
||||
this.markdownResult.element.innerHTML = this.sanitizeContent(this.markdownResult.element.innerHTML);
|
||||
this.setLoading(false);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = this.markdownResult.element.innerHTML;
|
||||
} else {
|
||||
this._commandService.executeCommand<string>('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
|
||||
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this.cellModel);
|
||||
htmlcontent = this.sanitizeContent(htmlcontent);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = htmlcontent;
|
||||
this.setLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sanitizes the content based on trusted mode of Cell Model
|
||||
private sanitizeContent(content: string): string {
|
||||
if (this.cellModel && !this.cellModel.trustedMode) {
|
||||
content = this.sanitizer.sanitize(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Todo: implement layout
|
||||
public layout() {
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
|
||||
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
public handleContentChanged(): void {
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
public toggleEditMode(editMode?: boolean): void {
|
||||
this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode;
|
||||
this.updateMoreActions();
|
||||
this.updatePreview();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private updateMoreActions(): void {
|
||||
if (!this.isEditMode && (this.isActive() || this._hover)) {
|
||||
this.toggleMoreActionsButton(true);
|
||||
}
|
||||
else {
|
||||
this.toggleMoreActionsButton(false);
|
||||
}
|
||||
}
|
||||
|
||||
private toggleUserSelect(userSelect: boolean): void {
|
||||
if (!this.output) {
|
||||
return;
|
||||
}
|
||||
if (userSelect) {
|
||||
DOM.addClass(this.output.nativeElement, USER_SELECT_CLASS);
|
||||
} else {
|
||||
DOM.removeClass(this.output.nativeElement, USER_SELECT_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
private setFocusAndScroll(): void {
|
||||
this.toggleEditMode(this.isActive());
|
||||
|
||||
if (this.output && this.output.nativeElement) {
|
||||
(<HTMLElement>this.output.nativeElement).scrollTo({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
text-cell-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
text-cell-component .notebook-preview {
|
||||
user-select: none;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.notebook-preview.actionselect {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
text-cell-component table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
table-layout: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1em;
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
text-cell-component thead {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
text-cell-component td,
|
||||
text-cell-component th,
|
||||
text-cell-component tr {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
padding: 0.5em 0.5em;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
max-width: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
text-cell-component th {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;">
|
||||
</div>
|
||||
<div #container class="scrollable" style="flex: 1 1 auto; position: relative; outline: none" (click)="unselectActiveCell()" (scroll)="scrollHandler($event)">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell, $event)" [class.active]="cell.active">
|
||||
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||
</code-cell-component>
|
||||
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||
</text-cell-component>
|
||||
</div>
|
||||
<div class="notebook-cell" *ngIf="(!cells || !cells.length) && !isLoading">
|
||||
<placeholder-cell-component [cellModel]="cell" [model]="model">
|
||||
</placeholder-cell-component>
|
||||
</div>
|
||||
<div class="book-nav" #bookNav [style.visibility]="navigationVisibility">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
695
src/sql/workbench/parts/notebook/browser/notebook.component.ts
Normal file
695
src/sql/workbench/parts/notebook/browser/notebook.component.ts
Normal file
@@ -0,0 +1,695 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { INotificationService, INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IAction, Action, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { ICellModel, IModelFactory, INotebookModel, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER, INotebookSection, INavigationProvider } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/workbench/parts/notebook/common/models/modelFactory';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils';
|
||||
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 { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { CellMagicMapper } from 'sql/workbench/parts/notebook/common/models/cellMagicMapper';
|
||||
import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { CellModel } from 'sql/workbench/parts/notebook/common/models/cell';
|
||||
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { isValidBasename } from 'vs/base/common/extpath';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { LabeledMenuItemActionItem, fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IBootstrapParams } from 'sql/platform/bootstrap/common/bootstrapParams';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
|
||||
|
||||
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: NOTEBOOK_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./notebook.component.html'))
|
||||
})
|
||||
export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor {
|
||||
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
|
||||
@ViewChild('container', { read: ElementRef }) private container: ElementRef;
|
||||
@ViewChild('bookNav', { read: ElementRef }) private bookNav: ElementRef;
|
||||
|
||||
private _model: NotebookModel;
|
||||
private _isInErrorState: boolean = false;
|
||||
private _errorMessage: string;
|
||||
protected _actionBar: Taskbar;
|
||||
protected isLoading: boolean;
|
||||
private notebookManagers: INotebookManager[] = [];
|
||||
private _modelReadyDeferred = new Deferred<NotebookModel>();
|
||||
private profile: IConnectionProfile;
|
||||
private _trustedAction: TrustedAction;
|
||||
private _runAllCellsAction: RunAllCellsAction;
|
||||
private _providerRelatedActions: IAction[] = [];
|
||||
private _scrollTop: number;
|
||||
private _navProvider: INavigationProvider;
|
||||
private navigationResult: nb.NavigationResult;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
|
||||
@Inject(IObjectExplorerService) private objectExplorerService: IObjectExplorerService,
|
||||
@Inject(IEditorService) private editorService: IEditorService,
|
||||
@Inject(INotificationService) private notificationService: INotificationService,
|
||||
@Inject(INotebookService) private notebookService: INotebookService,
|
||||
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
|
||||
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
|
||||
@Inject(IMenuService) private menuService: IMenuService,
|
||||
@Inject(IKeybindingService) private keybindingService: IKeybindingService,
|
||||
@Inject(IViewletService) private viewletService: IViewletService,
|
||||
@Inject(ICapabilitiesService) private capabilitiesService: ICapabilitiesService,
|
||||
@Inject(ITextFileService) private textFileService: ITextFileService,
|
||||
@Inject(ILogService) private readonly logService: ILogService,
|
||||
@Inject(ITelemetryService) private telemetryService: ITelemetryService
|
||||
) {
|
||||
super();
|
||||
this.updateProfile();
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
private updateProfile(): void {
|
||||
this.profile = this.notebookParams ? this.notebookParams.profile : undefined;
|
||||
if (!this.profile) {
|
||||
// Second use global connection if possible
|
||||
let profile: IConnectionProfile = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
|
||||
|
||||
// TODO use generic method to match kernel with valid connection that's compatible. For now, we only have 1
|
||||
if (profile && profile.providerName) {
|
||||
this.profile = profile;
|
||||
} else {
|
||||
// if not, try 1st active connection that matches our filter
|
||||
let activeProfiles = this.connectionManagementService.getActiveConnections();
|
||||
if (activeProfiles && activeProfiles.length > 0) {
|
||||
this.profile = activeProfiles[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.initActionBar();
|
||||
this.setScrollPosition();
|
||||
this.doLoad();
|
||||
this.initNavSection();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
if (this.notebookService) {
|
||||
this.notebookService.removeNotebookEditor(this);
|
||||
}
|
||||
}
|
||||
|
||||
public get model(): NotebookModel | null {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
public get activeCellId(): string {
|
||||
return this._model && this._model.activeCell ? this._model.activeCell.id : '';
|
||||
}
|
||||
|
||||
public get cells(): ICellModel[] {
|
||||
return this._model ? this._model.cells : [];
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let toolbarEl = <HTMLElement>this.toolbar.nativeElement;
|
||||
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
public selectCell(cell: ICellModel, event?: Event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (cell !== this.model.activeCell) {
|
||||
if (this.model.activeCell) {
|
||||
this.model.activeCell.active = false;
|
||||
}
|
||||
this._model.activeCell = cell;
|
||||
this._model.activeCell.active = true;
|
||||
this.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
//Saves scrollTop value on scroll change
|
||||
public scrollHandler(event: Event) {
|
||||
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
|
||||
}
|
||||
|
||||
public unselectActiveCell() {
|
||||
if (this.model && this.model.activeCell) {
|
||||
this.model.activeCell.active = false;
|
||||
this.model.activeCell = undefined;
|
||||
}
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
// Add cell based on cell type
|
||||
public addCell(cellType: CellType) {
|
||||
this._model.addCell(cellType);
|
||||
}
|
||||
|
||||
public onKeyDown(event) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
let nextIndex = (this.findCellIndex(this.model.activeCell) + 1) % this.cells.length;
|
||||
this.selectCell(this.cells[nextIndex]);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
let index = this.findCellIndex(this.model.activeCell);
|
||||
if (index === 0) {
|
||||
index = this.cells.length;
|
||||
}
|
||||
this.selectCell(this.cells[--index]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private setScrollPosition(): void {
|
||||
if (this._notebookParams && this._notebookParams.input) {
|
||||
this._notebookParams.input.layoutChanged(() => {
|
||||
let containerElement = <HTMLElement>this.container.nativeElement;
|
||||
containerElement.scrollTop = this._scrollTop;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async doLoad(): Promise<void> {
|
||||
try {
|
||||
await this.createModelAndLoadContents();
|
||||
await this.setNotebookManager();
|
||||
await this.loadModel();
|
||||
this._modelReadyDeferred.resolve(this._model);
|
||||
this.notebookService.addNotebookEditor(this);
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
// Offer to create a file from the error if we have a file not found and the name is valid
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(this.notebookParams.notebookUri))) {
|
||||
let errorWithAction = createErrorWithActions(toErrorMessage(error), {
|
||||
actions: [
|
||||
new Action('workbench.files.action.createMissingFile', localize('createFile', "Create File"), undefined, true, () => {
|
||||
return this.textFileService.create(this.notebookParams.notebookUri).then(() => this.editorService.openEditor({
|
||||
resource: this.notebookParams.notebookUri,
|
||||
options: {
|
||||
pinned: true // new file gets pinned by default
|
||||
}
|
||||
}));
|
||||
})
|
||||
]
|
||||
});
|
||||
this.notificationService.error(errorWithAction);
|
||||
|
||||
let editors = this.editorService.visibleControls;
|
||||
for (let editor of editors) {
|
||||
if (editor && editor.input.getResource() === this._notebookParams.input.notebookUri) {
|
||||
await editor.group.closeEditor(this._notebookParams.input, { preserveFocus: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setViewInErrorState(localize('displayFailed', "Could not display contents: {0}", getErrorMessage(error)));
|
||||
this.setLoading(false);
|
||||
this._modelReadyDeferred.reject(error);
|
||||
|
||||
this.notebookService.addNotebookEditor(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.isLoading = isLoading;
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
// Wait on provider information to be available before loading kernel and other information
|
||||
await this.awaitNonDefaultProvider();
|
||||
await this._model.requestModelLoad();
|
||||
this.detectChanges();
|
||||
await this._model.startSession(this._model.notebookManager, undefined, true);
|
||||
this.setContextKeyServiceWithProviderId(this._model.providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private async createModelAndLoadContents(): Promise<void> {
|
||||
let model = new NotebookModel({
|
||||
factory: this.modelFactory,
|
||||
notebookUri: this._notebookParams.notebookUri,
|
||||
connectionService: this.connectionManagementService,
|
||||
notificationService: this.notificationService,
|
||||
notebookManagers: this.notebookManagers,
|
||||
contentManager: this._notebookParams.input.contentManager,
|
||||
cellMagicMapper: new CellMagicMapper(this.notebookService.languageMagics),
|
||||
providerId: 'sql',
|
||||
defaultKernel: this._notebookParams.input.defaultKernel,
|
||||
layoutChanged: this._notebookParams.input.layoutChanged,
|
||||
capabilitiesService: this.capabilitiesService,
|
||||
editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp
|
||||
}, this.profile, this.logService, this.notificationService, this.telemetryService);
|
||||
let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty());
|
||||
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
|
||||
model.contentChanged((change) => this.handleContentChanged(change));
|
||||
model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider));
|
||||
model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs));
|
||||
this._model = this._register(model);
|
||||
await this._model.loadContents(trusted);
|
||||
this.setLoading(false);
|
||||
this.updateToolbarComponents(this._model.trustedMode);
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private async setNotebookManager(): Promise<void> {
|
||||
let providerInfo = await this._notebookParams.providerInfo;
|
||||
for (let providerId of providerInfo.providers) {
|
||||
let notebookManager = await this.notebookService.getOrCreateNotebookManager(providerId, this._notebookParams.notebookUri);
|
||||
this.notebookManagers.push(notebookManager);
|
||||
}
|
||||
}
|
||||
|
||||
private async awaitNonDefaultProvider(): Promise<void> {
|
||||
// Wait on registration for now. Long-term would be good to cache and refresh
|
||||
await this.notebookService.registrationComplete;
|
||||
this.model.standardKernels = this._notebookParams.input.standardKernels;
|
||||
// Refresh the provider if we had been using default
|
||||
let providerInfo = await this._notebookParams.providerInfo;
|
||||
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
|
||||
let providers = notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
|
||||
let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER);
|
||||
providerInfo.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0];
|
||||
}
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
|
||||
// If it's still the default, warn them they should install an extension
|
||||
this.notificationService.prompt(Severity.Warning,
|
||||
localize('noKernelInstalled', "Please install the SQL Server 2019 extension to run cells."),
|
||||
[{
|
||||
label: localize('installSql2019Extension', "Install Extension"),
|
||||
run: () => this.openExtensionGallery()
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private async openExtensionGallery(): Promise<void> {
|
||||
try {
|
||||
let viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true) as IExtensionsViewlet;
|
||||
viewlet.search('sql-vnext');
|
||||
viewlet.focus();
|
||||
} catch (error) {
|
||||
this.notificationService.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates toolbar components
|
||||
private updateToolbarComponents(isTrusted: boolean) {
|
||||
if (this._trustedAction) {
|
||||
this._trustedAction.enabled = true;
|
||||
this._trustedAction.trusted = isTrusted;
|
||||
}
|
||||
}
|
||||
|
||||
private get modelFactory(): IModelFactory {
|
||||
if (!this._notebookParams.modelFactory) {
|
||||
this._notebookParams.modelFactory = new ModelFactory(this.instantiationService);
|
||||
}
|
||||
return this._notebookParams.modelFactory;
|
||||
}
|
||||
|
||||
private handleModelError(notification: INotification): void {
|
||||
this.notificationService.notify(notification);
|
||||
}
|
||||
|
||||
private handleContentChanged(change: NotebookContentChange) {
|
||||
// Note: for now we just need to set dirty state and refresh the UI.
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private handleProviderIdChanged(providerId: string) {
|
||||
// If there are any actions that were related to the previous provider,
|
||||
// disable them in the actionBar
|
||||
this._providerRelatedActions.forEach(action => {
|
||||
action.enabled = false;
|
||||
});
|
||||
this.setContextKeyServiceWithProviderId(providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
}
|
||||
|
||||
private handleKernelChanged(kernelArgs: nb.IKernelChangedArgs) {
|
||||
this.fillInActionsForCurrentContext();
|
||||
}
|
||||
|
||||
findCellIndex(cellModel: ICellModel): number {
|
||||
return this._model.cells.findIndex((cell) => cell.id === cellModel.id);
|
||||
}
|
||||
|
||||
private setViewInErrorState(error: any): any {
|
||||
this._isInErrorState = true;
|
||||
this._errorMessage = getErrorMessage(error);
|
||||
// For now, send message as error notification #870 covers having dedicated area for this
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
|
||||
protected initActionBar(): void {
|
||||
let kernelContainer = document.createElement('div');
|
||||
let kernelDropdown = new KernelsDropdown(kernelContainer, this.contextViewService, this.modelReady);
|
||||
kernelDropdown.render(kernelContainer);
|
||||
attachSelectBoxStyler(kernelDropdown, this.themeService);
|
||||
|
||||
let attachToContainer = document.createElement('div');
|
||||
let attachToDropdown = new AttachToDropdown(attachToContainer, this.contextViewService, this.modelReady,
|
||||
this.connectionManagementService, this.connectionDialogService, this.notificationService, this.capabilitiesService, this.logService);
|
||||
attachToDropdown.render(attachToContainer);
|
||||
attachSelectBoxStyler(attachToDropdown, this.themeService);
|
||||
|
||||
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', "Code"), 'notebook-button icon-add');
|
||||
addCodeCellButton.cellType = CellTypes.Code;
|
||||
|
||||
let addTextCellButton = new AddCellAction('notebook.AddTextCell', localize('text', "Text"), 'notebook-button icon-add');
|
||||
addTextCellButton.cellType = CellTypes.Markdown;
|
||||
|
||||
this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAll', "Run Cells"), 'notebook-button icon-run-cells');
|
||||
let clearResultsButton = new ClearAllOutputsAction('notebook.ClearAllOutputs', localize('clearResults', "Clear Results"), 'notebook-button icon-clear-results');
|
||||
|
||||
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
|
||||
this._trustedAction.enabled = false;
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbar.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
|
||||
this._actionBar.context = this;
|
||||
this._actionBar.setContent([
|
||||
{ action: addCodeCellButton },
|
||||
{ action: addTextCellButton },
|
||||
{ element: kernelContainer },
|
||||
{ element: attachToContainer },
|
||||
{ action: this._trustedAction },
|
||||
{ action: this._runAllCellsAction },
|
||||
{ action: clearResultsButton }
|
||||
]);
|
||||
}
|
||||
|
||||
protected initNavSection(): void {
|
||||
this._navProvider = this.notebookService.getNavigationProvider(this._notebookParams.notebookUri);
|
||||
|
||||
if (product.quality !== 'stable' &&
|
||||
this.contextKeyService.getContextKeyValue('bookOpened') &&
|
||||
this._navProvider) {
|
||||
this._navProvider.getNavigation(this._notebookParams.notebookUri).then(result => {
|
||||
this.navigationResult = result;
|
||||
this.addButton(localize('previousButtonLabel', "< Previous"),
|
||||
() => this.previousPage(), this.navigationResult.previous ? true : false);
|
||||
this.addButton(localize('nextButtonLabel', "Next >"),
|
||||
() => this.nextPage(), this.navigationResult.next ? true : false);
|
||||
this.detectChanges();
|
||||
}, err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get navigationVisibility(): 'hidden' | 'visible' {
|
||||
if (this.navigationResult) {
|
||||
return this.navigationResult.hasNavigation ? 'visible' : 'hidden';
|
||||
}
|
||||
return 'hidden';
|
||||
}
|
||||
|
||||
private addButton(label: string, onDidClick?: () => void, enabled?: boolean): void {
|
||||
const container = DOM.append(this.bookNav.nativeElement, DOM.$('.dialog-message-button'));
|
||||
let button = new Button(container);
|
||||
button.icon = '';
|
||||
button.label = label;
|
||||
if (onDidClick) {
|
||||
this._register(button.onDidClick(onDidClick));
|
||||
}
|
||||
if (!enabled) {
|
||||
button.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private actionItemProvider(action: Action): IActionViewItem {
|
||||
// Check extensions to create ActionItem; otherwise, return undefined
|
||||
// This is similar behavior that exists in MenuItemActionItem
|
||||
if (action instanceof MenuItemAction) {
|
||||
return new LabeledMenuItemActionItem(action, this.keybindingService, this.contextMenuService, this.notificationService, 'notebook-button');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the menu contributions that use the ID 'notebook/toolbar'.
|
||||
* Then, find all groups (currently we don't leverage the contributed
|
||||
* groups functionality for the notebook toolbar), and fill in the 'primary'
|
||||
* array with items that don't list a group. Finally, add any actions from
|
||||
* the primary array to the end of the toolbar.
|
||||
*/
|
||||
private fillInActionsForCurrentContext(): void {
|
||||
let primary: IAction[] = [];
|
||||
let secondary: IAction[] = [];
|
||||
let notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService);
|
||||
let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true });
|
||||
fillInActions(groups, { primary, secondary }, false, (group: string) => group === undefined || group === '');
|
||||
this.addPrimaryContributedActions(primary);
|
||||
}
|
||||
|
||||
private detectChanges(): void {
|
||||
if (!(this._changeRef['destroyed'])) {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private addPrimaryContributedActions(primary: IAction[]) {
|
||||
for (let action of primary) {
|
||||
// Need to ensure that we don't add the same action multiple times
|
||||
let foundIndex = this._providerRelatedActions.findIndex(act => act.id === action.id);
|
||||
if (foundIndex < 0) {
|
||||
this._actionBar.addAction(action);
|
||||
this._providerRelatedActions.push(action);
|
||||
} else {
|
||||
this._providerRelatedActions[foundIndex].enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setContextKeyServiceWithProviderId(providerId: string) {
|
||||
let provider = new RawContextKey<string>('providerId', providerId);
|
||||
provider.bindTo(this.contextKeyService);
|
||||
}
|
||||
|
||||
public get notebookParams(): INotebookParams {
|
||||
return this._notebookParams;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._notebookParams.notebookUri.toString();
|
||||
}
|
||||
|
||||
public get modelReady(): Promise<INotebookModel> {
|
||||
return this._modelReadyDeferred.promise;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.editorService.activeEditor === this.notebookParams.input;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
let notebookEditor = this.notebookParams.input;
|
||||
return this.editorService.visibleEditors.some(e => e === notebookEditor);
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.notebookParams.input.isDirty();
|
||||
}
|
||||
|
||||
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
|
||||
if (!edits || edits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
this._model.pushEditOperations(edits);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async runCell(cell: ICellModel): Promise<boolean> {
|
||||
await this.modelReady;
|
||||
let uriString = cell.cellUri.toString();
|
||||
if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
|
||||
this.selectCell(cell);
|
||||
return cell.runCell(this.notificationService, this.connectionManagementService);
|
||||
} else {
|
||||
return Promise.reject(new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString)));
|
||||
}
|
||||
}
|
||||
|
||||
public async runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise<boolean> {
|
||||
await this.modelReady;
|
||||
let codeCells = this._model.cells.filter(cell => cell.cellType === CellTypes.Code);
|
||||
if (codeCells && codeCells.length) {
|
||||
// For the run all cells scenario where neither startId not endId are provided, set defaults
|
||||
let startIndex = 0;
|
||||
let endIndex = codeCells.length;
|
||||
if (!isUndefinedOrNull(startCell)) {
|
||||
startIndex = codeCells.findIndex(c => c.id === startCell.id);
|
||||
}
|
||||
if (!isUndefinedOrNull(endCell)) {
|
||||
endIndex = codeCells.findIndex(c => c.id === endCell.id);
|
||||
}
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
let cellStatus = await this.runCell(codeCells[i]);
|
||||
if (!cellStatus) {
|
||||
return Promise.reject(new Error(localize('cellRunFailed', "Run Cells failed - See error in output of the currently selected cell for more information.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async clearOutput(cell: ICellModel): Promise<boolean> {
|
||||
try {
|
||||
await this.modelReady;
|
||||
let uriString = cell.cellUri.toString();
|
||||
if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
|
||||
this.selectCell(cell);
|
||||
// Clear outputs of the requested cell if cell type is code cell.
|
||||
// If cell is markdown cell, clearOutputs() is a no-op
|
||||
if (cell.cellType === CellTypes.Code) {
|
||||
(cell as CellModel).clearOutputs();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return Promise.reject(new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString)));
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async clearAllOutputs(): Promise<boolean> {
|
||||
try {
|
||||
await this.modelReady;
|
||||
this._model.cells.forEach(cell => {
|
||||
if (cell.cellType === CellTypes.Code) {
|
||||
(cell as CellModel).clearOutputs();
|
||||
}
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async nextPage(): Promise<void> {
|
||||
try {
|
||||
if (this._navProvider) {
|
||||
this._navProvider.onNext(this.model.notebookUri);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notificationService.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
public previousPage() {
|
||||
try {
|
||||
if (this._navProvider) {
|
||||
this._navProvider.onPrevious(this.model.notebookUri);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notificationService.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
getSections(): INotebookSection[] {
|
||||
return this.getSectionElements();
|
||||
}
|
||||
|
||||
private getSectionElements(): NotebookSection[] {
|
||||
let headers: NotebookSection[] = [];
|
||||
let el: HTMLElement = this.container.nativeElement;
|
||||
let headerElements = el.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
for (let i = 0; i < headerElements.length; i++) {
|
||||
let headerEl = headerElements[i] as HTMLElement;
|
||||
if (headerEl['id']) {
|
||||
headers.push(new NotebookSection(headerEl));
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
navigateToSection(id: string): void {
|
||||
id = id.toLowerCase();
|
||||
let section = this.getSectionElements().find(s => s.relativeUri && s.relativeUri.toLowerCase() === id);
|
||||
if (section) {
|
||||
// Scroll this section to the top of the header instead of just bringing header into view.
|
||||
let scrollTop = jQuery(section.headerEl).offset().top;
|
||||
(<HTMLElement>this.container.nativeElement).scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
section.headerEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotebookSection implements INotebookSection {
|
||||
|
||||
constructor(public headerEl: HTMLElement) {
|
||||
}
|
||||
|
||||
get relativeUri(): string {
|
||||
return this.headerEl['id'];
|
||||
}
|
||||
|
||||
get header(): string {
|
||||
return this.headerEl.textContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
|
||||
import { SyncActionDescriptor, registerAction } from 'vs/platform/actions/common/actions';
|
||||
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/common/models/notebookInput';
|
||||
import { NotebookEditor } from 'sql/workbench/parts/notebook/browser/notebookEditor';
|
||||
import { NewNotebookAction } from 'sql/workbench/parts/notebook/browser/notebookActions';
|
||||
import { KeyMod } from 'vs/editor/common/standalone/standaloneBase';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { GridOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/gridOutput.component';
|
||||
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/plotlyOutput.component';
|
||||
import { registerComponentType } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import { MimeRendererComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRenderer.component';
|
||||
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/markdownOutput.component';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
|
||||
// Model View editor registration
|
||||
const viewModelEditorDescriptor = new EditorDescriptor(
|
||||
NotebookEditor,
|
||||
NotebookEditor.ID,
|
||||
'Notebook'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]);
|
||||
|
||||
// Global Actions
|
||||
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
NewNotebookAction,
|
||||
NewNotebookAction.ID,
|
||||
NewNotebookAction.LABEL,
|
||||
{ primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_N },
|
||||
|
||||
),
|
||||
NewNotebookAction.LABEL
|
||||
);
|
||||
|
||||
registerAction({
|
||||
id: 'workbench.action.setWorkspaceAndOpen',
|
||||
handler: async (accessor, options: { forceNewWindow: boolean, folderPath: URI }) => {
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
const workspaceEditingService = accessor.get(IWorkspaceEditingService);
|
||||
const windowService = accessor.get(IWindowService);
|
||||
let folders = [];
|
||||
if (!options.folderPath) {
|
||||
return;
|
||||
}
|
||||
folders.push(options.folderPath);
|
||||
await workspaceEditingService.addFolders(folders.map(folder => ({ uri: folder })));
|
||||
await viewletService.openViewlet(viewletService.getDefaultViewletId(), true);
|
||||
if (options.forceNewWindow) {
|
||||
return windowService.openWindow([{ folderUri: folders[0] }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
else {
|
||||
return windowService.reloadWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'notebook',
|
||||
'title': 'Notebook',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'notebook.useInProcMarkdown': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('notebook.inProcMarkdown', "Use in-process markdown viewer to render text cells more quickly (Experimental).")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* *************** Output components *************** */
|
||||
// Note: most existing types use the same component to render. In order to
|
||||
// preserve correct rank order, we register it once for each different rank of
|
||||
// MIME types.
|
||||
|
||||
/**
|
||||
* A mime renderer component for raw html.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/html'],
|
||||
rank: 50,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for images.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
|
||||
rank: 90,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for svg.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['image/svg+xml'],
|
||||
rank: 80,
|
||||
safe: false,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for plain and jupyter console text data.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'text/plain',
|
||||
'application/vnd.jupyter.stdout',
|
||||
'application/vnd.jupyter.stderr'
|
||||
],
|
||||
rank: 120,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A placeholder component for deprecated rendered JavaScript.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/javascript', 'application/javascript'],
|
||||
rank: 110,
|
||||
safe: false,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for grid data.
|
||||
* This will be replaced by a dedicated component in the future
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
rank: 40,
|
||||
safe: true,
|
||||
ctor: GridOutputComponent,
|
||||
selector: GridOutputComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for LaTeX.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/latex'],
|
||||
rank: 70,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for Markdown.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/markdown'],
|
||||
rank: 60,
|
||||
safe: true,
|
||||
ctor: MarkdownOutputComponent,
|
||||
selector: MarkdownOutputComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for Plotly graphs.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['application/vnd.plotly.v1+json'],
|
||||
rank: 45,
|
||||
safe: true,
|
||||
ctor: PlotlyOutputComponent,
|
||||
selector: PlotlyOutputComponent.SELECTOR
|
||||
});
|
||||
/**
|
||||
* A mime renderer component for Plotly HTML output
|
||||
* that will ensure this gets ignored if possible since it's only output
|
||||
* on offline init and adds a <script> tag which does what we've done (add Plotly support into the app)
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/vnd.plotly.v1+html'],
|
||||
rank: 46,
|
||||
safe: true,
|
||||
ctor: PlotlyOutputComponent,
|
||||
selector: PlotlyOutputComponent.SELECTOR
|
||||
});
|
||||
88
src/sql/workbench/parts/notebook/browser/notebook.module.ts
Normal file
88
src/sql/workbench/parts/notebook/browser/notebook.module.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/browser/core/componentHost.directive';
|
||||
import { providerIterator } from 'sql/platform/bootstrap/browser/bootstrapService';
|
||||
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
|
||||
import { EditableDropDown } from 'sql/platform/browser/editableDropdown/editableDropdown.component';
|
||||
import { NotebookComponent } from 'sql/workbench/parts/notebook/browser/notebook.component';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CodeComponent } from 'sql/workbench/parts/notebook/browser/cellViews/code.component';
|
||||
import { CodeCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/codeCell.component';
|
||||
import { TextCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/textCell.component';
|
||||
import { OutputAreaComponent } from 'sql/workbench/parts/notebook/browser/cellViews/outputArea.component';
|
||||
import { OutputComponent } from 'sql/workbench/parts/notebook/browser/cellViews/output.component';
|
||||
import { StdInComponent } from 'sql/workbench/parts/notebook/browser/cellViews/stdin.component';
|
||||
import { PlaceholderCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/placeholderCell.component';
|
||||
import LoadingSpinner from 'sql/workbench/browser/modelComponents/loadingSpinner.component';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
|
||||
import { SelectBox } from 'sql/platform/browser/selectBox/selectBox.component';
|
||||
import { InputBox } from 'sql/platform/browser/inputbox/inputBox.component';
|
||||
import { IMimeComponentRegistry, Extensions } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
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';
|
||||
|
||||
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
|
||||
let outputComponents = Registry.as<IMimeComponentRegistry>(Extensions.MimeComponentContribution).getAllCtors();
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
Checkbox,
|
||||
SelectBox,
|
||||
EditableDropDown,
|
||||
InputBox,
|
||||
LoadingSpinner,
|
||||
CodeComponent,
|
||||
CodeCellComponent,
|
||||
TextCellComponent,
|
||||
PlaceholderCellComponent,
|
||||
NotebookComponent,
|
||||
ComponentHostDirective,
|
||||
OutputAreaComponent,
|
||||
OutputComponent,
|
||||
StdInComponent,
|
||||
LinkHandlerDirective,
|
||||
...outputComponents
|
||||
],
|
||||
entryComponents: [
|
||||
NotebookComponent,
|
||||
...outputComponents
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
BrowserModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
CommonServiceInterface,
|
||||
{ provide: IBootstrapParams, useValue: params },
|
||||
{ provide: ISelector, useValue: selector },
|
||||
...providerIterator(instantiationService)
|
||||
]
|
||||
})
|
||||
class ModuleClass {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(ISelector) private selector: string
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factoryWrapper: any = this._resolver.resolveComponentFactory(NotebookComponent);
|
||||
factoryWrapper.factory.selector = this.selector;
|
||||
appRef.bootstrap(factoryWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
return ModuleClass;
|
||||
};
|
||||
563
src/sql/workbench/parts/notebook/browser/notebookActions.ts
Normal file
563
src/sql/workbench/parts/notebook/browser/notebookActions.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { INotificationService, Severity, INotificationActions } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { SelectBox, ISelectBoxOptionsWithLabel } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { IConnectionManagementService, ConnectionType } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { noKernel } from 'sql/workbench/services/notebook/common/sessionManager';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
|
||||
import { generateUri } from 'sql/platform/connection/common/utils';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { NotebookComponent } from 'sql/workbench/parts/notebook/browser/notebook.component';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { INotebookModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
|
||||
const msgLoading = localize('loading', "Loading kernels...");
|
||||
const msgChanging = localize('changing', "Changing kernel...");
|
||||
const kernelLabel: string = localize('Kernel', "Kernel: ");
|
||||
const attachToLabel: string = localize('AttachTo', "Attach To: ");
|
||||
const msgLoadingContexts = localize('loadingContexts', "Loading contexts...");
|
||||
const msgAddNewConnection = localize('addNewConnection', "Add New Connection");
|
||||
const msgSelectConnection = localize('selectConnection', "Select Connection");
|
||||
const msgLocalHost = localize('localhost', "localhost");
|
||||
const HIDE_ICON_CLASS = ' hideIcon';
|
||||
|
||||
// Action to add a cell to notebook based on cell type(code/markdown).
|
||||
export class AddCellAction extends Action {
|
||||
public cellType: CellType;
|
||||
|
||||
constructor(
|
||||
id: string, label: string, cssClass: string
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
public run(context: NotebookComponent): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
context.addCell(this.cellType);
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Action to clear outputs of all code cells.
|
||||
export class ClearAllOutputsAction extends Action {
|
||||
constructor(
|
||||
id: string, label: string, cssClass: string
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
public run(context: NotebookComponent): Promise<boolean> {
|
||||
return context.clearAllOutputs();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IToggleableState {
|
||||
baseClass?: string;
|
||||
shouldToggleTooltip?: boolean;
|
||||
toggleOnClass: string;
|
||||
toggleOnLabel: string;
|
||||
toggleOffLabel: string;
|
||||
toggleOffClass: string;
|
||||
isOn: boolean;
|
||||
}
|
||||
|
||||
export abstract class ToggleableAction extends Action {
|
||||
|
||||
constructor(id: string, protected state: IToggleableState) {
|
||||
super(id, '');
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
|
||||
private updateLabelAndIcon() {
|
||||
if (this.state.shouldToggleTooltip) {
|
||||
this.tooltip = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel;
|
||||
} else {
|
||||
this.label = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel;
|
||||
}
|
||||
let classes = this.state.baseClass ? `${this.state.baseClass} ` : '';
|
||||
classes += this.state.isOn ? this.state.toggleOnClass : this.state.toggleOffClass;
|
||||
this.class = classes;
|
||||
}
|
||||
|
||||
protected toggle(isOn: boolean): void {
|
||||
this.state.isOn = isOn;
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface IActionStateData {
|
||||
className?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
hideIcon?: boolean;
|
||||
commandId?: string;
|
||||
}
|
||||
|
||||
export class IMultiStateData<T> {
|
||||
private _stateMap = new Map<T, IActionStateData>();
|
||||
constructor(mappings: { key: T, value: IActionStateData }[], private _state: T, private _baseClass?: string) {
|
||||
if (mappings) {
|
||||
mappings.forEach(s => this._stateMap.set(s.key, s.value));
|
||||
}
|
||||
}
|
||||
|
||||
public set state(value: T) {
|
||||
if (!this._stateMap.has(value)) {
|
||||
throw new Error('State value must be in stateMap');
|
||||
}
|
||||
this._state = value;
|
||||
}
|
||||
|
||||
public updateStateData(state: T, updater: (data: IActionStateData) => void): void {
|
||||
let data = this._stateMap.get(state);
|
||||
if (data) {
|
||||
updater(data);
|
||||
}
|
||||
}
|
||||
|
||||
public get classes(): string {
|
||||
let classVal = this.getStateValueOrDefault<string>((data) => data.className, '');
|
||||
let classes = this._baseClass ? `${this._baseClass} ` : '';
|
||||
classes += classVal;
|
||||
if (this.getStateValueOrDefault<boolean>((data) => data.hideIcon, false)) {
|
||||
classes += HIDE_ICON_CLASS;
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return this.getStateValueOrDefault<string>((data) => data.label, '');
|
||||
}
|
||||
|
||||
public get tooltip(): string {
|
||||
return this.getStateValueOrDefault<string>((data) => data.tooltip, '');
|
||||
}
|
||||
|
||||
public get commandId(): string {
|
||||
return this.getStateValueOrDefault<string>((data) => data.commandId, '');
|
||||
}
|
||||
|
||||
private getStateValueOrDefault<U>(getter: (data: IActionStateData) => U, defaultVal?: U): U {
|
||||
let data = this._stateMap.get(this._state);
|
||||
return data ? getter(data) : defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export abstract class MultiStateAction<T> extends Action {
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
protected states: IMultiStateData<T>,
|
||||
private _keybindingService: IKeybindingService,
|
||||
private readonly logService: ILogService) {
|
||||
super(id, '');
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
|
||||
private updateLabelAndIcon() {
|
||||
let keyboardShortcut: string;
|
||||
try {
|
||||
// If a keyboard shortcut exists for the command id passed in, append that to the label
|
||||
if (this.states.commandId !== '') {
|
||||
let binding = this._keybindingService.lookupKeybinding(this.states.commandId);
|
||||
keyboardShortcut = binding ? binding.getLabel() : undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
this.label = this.states.label;
|
||||
this.tooltip = keyboardShortcut ? this.states.tooltip + ` (${keyboardShortcut})` : this.states.tooltip;
|
||||
this.class = this.states.classes;
|
||||
}
|
||||
|
||||
protected updateState(state: T): void {
|
||||
this.states.state = state;
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
}
|
||||
|
||||
export class TrustedAction extends ToggleableAction {
|
||||
// Constants
|
||||
private static readonly trustedLabel = localize('trustLabel', "Trusted");
|
||||
private static readonly notTrustedLabel = localize('untrustLabel', "Not Trusted");
|
||||
private static readonly alreadyTrustedMsg = localize('alreadyTrustedMsg', "Notebook is already trusted.");
|
||||
private static readonly baseClass = 'notebook-button';
|
||||
private static readonly trustedCssClass = 'icon-trusted';
|
||||
private static readonly notTrustedCssClass = 'icon-notTrusted';
|
||||
|
||||
// Properties
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@INotificationService private _notificationService: INotificationService
|
||||
) {
|
||||
super(id, {
|
||||
baseClass: TrustedAction.baseClass,
|
||||
toggleOnLabel: TrustedAction.trustedLabel,
|
||||
toggleOnClass: TrustedAction.trustedCssClass,
|
||||
toggleOffLabel: TrustedAction.notTrustedLabel,
|
||||
toggleOffClass: TrustedAction.notTrustedCssClass,
|
||||
isOn: false
|
||||
});
|
||||
}
|
||||
|
||||
public get trusted(): boolean {
|
||||
return this.state.isOn;
|
||||
}
|
||||
public set trusted(value: boolean) {
|
||||
this.toggle(value);
|
||||
}
|
||||
|
||||
public run(context: NotebookComponent): Promise<boolean> {
|
||||
let self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
if (self.trusted) {
|
||||
const actions: INotificationActions = { primary: [] };
|
||||
self._notificationService.notify({ severity: Severity.Info, message: TrustedAction.alreadyTrustedMsg, actions });
|
||||
}
|
||||
else {
|
||||
self.trusted = !self.trusted;
|
||||
context.model.trustedMode = self.trusted;
|
||||
}
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Action to run all code cells in a notebook.
|
||||
export class RunAllCellsAction extends Action {
|
||||
constructor(
|
||||
id: string, label: string, cssClass: string,
|
||||
@INotificationService private notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
public async run(context: NotebookComponent): Promise<boolean> {
|
||||
try {
|
||||
await context.runAllCells();
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.notificationService.error(getErrorMessage(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class KernelsDropdown extends SelectBox {
|
||||
private model: NotebookModel;
|
||||
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>) {
|
||||
super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel } as ISelectBoxOptionsWithLabel);
|
||||
|
||||
if (modelReady) {
|
||||
modelReady
|
||||
.then((model) => this.updateModel(model))
|
||||
.catch((err) => {
|
||||
// No-op for now
|
||||
});
|
||||
}
|
||||
|
||||
this.onDidSelect(e => this.doChangeKernel(e.selected));
|
||||
}
|
||||
|
||||
updateModel(model: INotebookModel): void {
|
||||
this.model = model as NotebookModel;
|
||||
this._register(this.model.kernelChanged((changedArgs: azdata.nb.IKernelChangedArgs) => {
|
||||
this.updateKernel(changedArgs.newValue);
|
||||
}));
|
||||
let kernel = this.model.clientSession && this.model.clientSession.kernel;
|
||||
this.updateKernel(kernel);
|
||||
}
|
||||
|
||||
// Update SelectBox values
|
||||
public updateKernel(kernel: azdata.nb.IKernel) {
|
||||
let kernels: string[] = this.model.standardKernelsDisplayName();
|
||||
if (kernel && kernel.isReady) {
|
||||
let standardKernel = this.model.getStandardKernelFromName(kernel.name);
|
||||
|
||||
if (kernels && standardKernel) {
|
||||
let index = kernels.findIndex((kernel => kernel === standardKernel.displayName));
|
||||
this.setOptions(kernels, index);
|
||||
}
|
||||
} else if (this.model.clientSession.isInErrorState) {
|
||||
let noKernelName = localize('noKernel', "No Kernel");
|
||||
kernels.unshift(noKernelName);
|
||||
this.setOptions(kernels, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public doChangeKernel(displayName: string): void {
|
||||
this.setOptions([msgChanging], 0);
|
||||
this.model.changeKernel(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
export class AttachToDropdown extends SelectBox {
|
||||
private model: NotebookModel;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IConnectionDialogService private _connectionDialogService: IConnectionDialogService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false, ariaLabel: attachToLabel } as ISelectBoxOptionsWithLabel);
|
||||
if (modelReady) {
|
||||
modelReady
|
||||
.then(model => {
|
||||
this.updateModel(model);
|
||||
this.updateAttachToDropdown(model);
|
||||
})
|
||||
.catch(err => {
|
||||
// No-op for now
|
||||
});
|
||||
}
|
||||
this.onDidSelect(e => {
|
||||
this.doChangeContext(this.getSelectedConnection(e.selected));
|
||||
});
|
||||
}
|
||||
|
||||
public updateModel(model: INotebookModel): void {
|
||||
this.model = model as NotebookModel;
|
||||
this._register(model.contextsChanged(() => {
|
||||
this.handleContextsChanged();
|
||||
}));
|
||||
this._register(this.model.contextsLoading(() => {
|
||||
this.setOptions([msgLoadingContexts], 0);
|
||||
}));
|
||||
this.model.requestConnectionHandler = () => this.openConnectionDialog(true);
|
||||
this.handleContextsChanged();
|
||||
}
|
||||
|
||||
private handleContextsChanged(showSelectConnection?: boolean) {
|
||||
let kernelDisplayName: string = this.getKernelDisplayName();
|
||||
if (kernelDisplayName) {
|
||||
this.loadAttachToDropdown(this.model, kernelDisplayName, showSelectConnection);
|
||||
} else if (this.model.clientSession.isInErrorState) {
|
||||
this.setOptions([localize('noContextAvailable', "None")], 0);
|
||||
}
|
||||
}
|
||||
|
||||
private updateAttachToDropdown(model: INotebookModel): void {
|
||||
if (this.model.connectionProfile && this.model.connectionProfile.serverName) {
|
||||
let connectionUri = generateUri(this.model.connectionProfile, 'notebook');
|
||||
this.model.notebookOptions.connectionService.connect(this.model.connectionProfile, connectionUri).then(result => {
|
||||
if (result.connected) {
|
||||
let connectionProfile = new ConnectionProfile(this._capabilitiesService, result.connectionProfile);
|
||||
this.model.addAttachToConnectionsToBeDisposed(connectionUri);
|
||||
this.doChangeContext(connectionProfile);
|
||||
} else {
|
||||
this.openConnectionDialog(true);
|
||||
}
|
||||
}).catch(err =>
|
||||
this.logService.error(err));
|
||||
}
|
||||
this._register(model.onValidConnectionSelected(validConnection => {
|
||||
this.handleContextsChanged(!validConnection);
|
||||
}));
|
||||
}
|
||||
|
||||
private getKernelDisplayName(): string {
|
||||
let kernelDisplayName: string;
|
||||
if (this.model.clientSession && this.model.clientSession.kernel && this.model.clientSession.kernel.name) {
|
||||
let currentKernelName = this.model.clientSession.kernel.name.toLowerCase();
|
||||
let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name && kernel.name.toLowerCase() === currentKernelName);
|
||||
if (currentKernelSpec) {
|
||||
kernelDisplayName = currentKernelSpec.display_name;
|
||||
}
|
||||
}
|
||||
return kernelDisplayName;
|
||||
}
|
||||
|
||||
// Load "Attach To" dropdown with the values corresponding to Kernel dropdown
|
||||
public async loadAttachToDropdown(model: INotebookModel, currentKernel: string, showSelectConnection?: boolean): Promise<void> {
|
||||
let connProviderIds = this.model.getApplicableConnectionProviderIds(currentKernel);
|
||||
if ((connProviderIds && connProviderIds.length === 0) || currentKernel === noKernel) {
|
||||
this.setOptions([msgLocalHost]);
|
||||
}
|
||||
else {
|
||||
let connections = this.getConnections(model);
|
||||
this.enable();
|
||||
if (showSelectConnection) {
|
||||
connections = this.loadWithSelectConnection(connections);
|
||||
}
|
||||
else {
|
||||
if (connections.length === 1 && connections[0] === msgAddNewConnection) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
}
|
||||
else {
|
||||
if (!connections.includes(msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
}
|
||||
this.setOptions(connections, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadWithSelectConnection(connections: string[]): string[] {
|
||||
if (connections && connections.length > 0) {
|
||||
if (!connections.includes(msgSelectConnection)) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
}
|
||||
|
||||
if (!connections.includes(msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
this.setOptions(connections, 0);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
//Get connections from context
|
||||
public getConnections(model: INotebookModel): string[] {
|
||||
let otherConnections: ConnectionProfile[] = [];
|
||||
model.contexts.otherConnections.forEach((conn) => { otherConnections.push(conn); });
|
||||
// If current connection connects to master, select the option in the dropdown that doesn't specify a database
|
||||
if (!model.contexts.defaultConnection.databaseName) {
|
||||
this.selectWithOptionName(model.contexts.defaultConnection.serverName);
|
||||
} else {
|
||||
if (model.contexts.defaultConnection) {
|
||||
this.selectWithOptionName(model.contexts.defaultConnection.title ? model.contexts.defaultConnection.title : model.contexts.defaultConnection.serverName);
|
||||
} else {
|
||||
this.select(0);
|
||||
}
|
||||
}
|
||||
otherConnections = this.setConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections);
|
||||
let connections = otherConnections.map((context) => context.title ? context.title : context.serverName);
|
||||
return connections;
|
||||
}
|
||||
|
||||
private setConnectionsList(defaultConnection: ConnectionProfile, otherConnections: ConnectionProfile[]) {
|
||||
if (defaultConnection.serverName !== msgSelectConnection) {
|
||||
otherConnections = otherConnections.filter(conn => conn.id !== defaultConnection.id);
|
||||
otherConnections.unshift(defaultConnection);
|
||||
if (otherConnections.length > 1) {
|
||||
otherConnections = otherConnections.filter(val => val.serverName !== msgSelectConnection);
|
||||
}
|
||||
}
|
||||
return otherConnections;
|
||||
}
|
||||
|
||||
public getSelectedConnection(selection: string): ConnectionProfile {
|
||||
// Find all connections with the the same server as the selected option
|
||||
let connections = this.model.contexts.otherConnections.filter((c) => selection === c.title);
|
||||
// If only one connection exists with the same server name, use that one
|
||||
if (connections.length === 1) {
|
||||
return connections[0];
|
||||
} else {
|
||||
return this.model.contexts.otherConnections.find((c) => selection === c.title);
|
||||
}
|
||||
}
|
||||
|
||||
public doChangeContext(connection?: ConnectionProfile, hideErrorMessage?: boolean): void {
|
||||
if (this.value === msgAddNewConnection) {
|
||||
this.openConnectionDialog();
|
||||
} else {
|
||||
this.model.changeContext(this.value, connection, hideErrorMessage).then(ok => undefined, err => this._notificationService.error(getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open connection dialog
|
||||
* Enter server details and connect to a server from the dialog
|
||||
* Bind the server value to 'Attach To' drop down
|
||||
* Connected server is displayed at the top of drop down
|
||||
**/
|
||||
public async openConnectionDialog(useProfile: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
let connection = await this._connectionDialogService.openDialogAndWait(this._connectionManagementService,
|
||||
{
|
||||
connectionType: ConnectionType.temporary,
|
||||
providers: this.model.getApplicableConnectionProviderIds(this.model.clientSession.kernel.name)
|
||||
},
|
||||
useProfile ? this.model.connectionProfile : undefined);
|
||||
|
||||
let attachToConnections = this.values;
|
||||
if (!connection) {
|
||||
this.loadAttachToDropdown(this.model, this.getKernelDisplayName());
|
||||
this.doChangeContext(undefined, true);
|
||||
return false;
|
||||
}
|
||||
let connectionUri = this._connectionManagementService.getConnectionUri(connection);
|
||||
let connectionProfile = new ConnectionProfile(this._capabilitiesService, connection);
|
||||
let connectedServer = connectionProfile.title ? connectionProfile.title : connectionProfile.serverName;
|
||||
//Check to see if the same server is already there in dropdown. We only have server names in dropdown
|
||||
if (attachToConnections.some(val => val === connectedServer)) {
|
||||
this.loadAttachToDropdown(this.model, this.getKernelDisplayName());
|
||||
this.doChangeContext();
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
attachToConnections.unshift(connectedServer);
|
||||
}
|
||||
//To ignore n/a after we have at least one valid connection
|
||||
attachToConnections = attachToConnections.filter(val => val !== msgSelectConnection);
|
||||
|
||||
let index = attachToConnections.findIndex((connection => connection === connectedServer));
|
||||
this.setOptions([]);
|
||||
this.setOptions(attachToConnections);
|
||||
if (!index || index < 0 || index >= attachToConnections.length) {
|
||||
index = 0;
|
||||
}
|
||||
this.select(index);
|
||||
|
||||
this.model.addAttachToConnectionsToBeDisposed(connectionUri);
|
||||
// Call doChangeContext to set the newly chosen connection in the model
|
||||
this.doChangeContext(connectionProfile);
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
const actions: INotificationActions = { primary: [] };
|
||||
this._notificationService.notify({ severity: Severity.Error, message: getErrorMessage(error), actions });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NewNotebookAction extends Action {
|
||||
|
||||
public static readonly ID = 'notebook.command.new';
|
||||
public static readonly LABEL = localize('newNotebookAction', "New Notebook");
|
||||
|
||||
private static readonly INTERNAL_NEW_NOTEBOOK_CMD_ID = '_notebook.command.new';
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private commandService: ICommandService
|
||||
) {
|
||||
super(id, label);
|
||||
this.class = 'notebook-action new-notebook';
|
||||
}
|
||||
|
||||
run(context?: azdata.ConnectedContext): Promise<void> {
|
||||
return this.commandService.executeCommand(NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID, context);
|
||||
}
|
||||
|
||||
}
|
||||
104
src/sql/workbench/parts/notebook/browser/notebookEditor.ts
Normal file
104
src/sql/workbench/parts/notebook/browser/notebookEditor.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { bootstrapAngular } from 'sql/platform/bootstrap/browser/bootstrapService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/common/models/notebookInput';
|
||||
import { NotebookModule } from 'sql/workbench/parts/notebook/browser/notebook.module';
|
||||
import { NOTEBOOK_SELECTOR } from 'sql/workbench/parts/notebook/browser/notebook.component';
|
||||
import { INotebookParams } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class NotebookEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.notebookEditor';
|
||||
private _notebookContainer: HTMLElement;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(NotebookEditor.ID, telemetryService, themeService, storageService);
|
||||
}
|
||||
|
||||
public get notebookInput(): NotebookInput {
|
||||
return this.input as NotebookInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent element.
|
||||
*/
|
||||
public createEditor(parent: HTMLElement): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
||||
* To be called when the container of this editor changes size.
|
||||
*/
|
||||
public layout(dimension: DOM.Dimension): void {
|
||||
if (this.notebookInput) {
|
||||
this.notebookInput.doChangeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public setInput(input: NotebookInput, options: EditorOptions): Promise<void> {
|
||||
if (this.input && this.input.matches(input)) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const parentElement = this.getContainer();
|
||||
|
||||
super.setInput(input, options, CancellationToken.None);
|
||||
|
||||
DOM.clearNode(parentElement);
|
||||
|
||||
if (!input.hasBootstrapped) {
|
||||
let container = DOM.$<HTMLElement>('.notebookEditor');
|
||||
container.style.height = '100%';
|
||||
this._notebookContainer = DOM.append(parentElement, container);
|
||||
input.container = this._notebookContainer;
|
||||
return Promise.resolve(this.bootstrapAngular(input));
|
||||
} else {
|
||||
this._notebookContainer = DOM.append(parentElement, input.container);
|
||||
input.doChangeLayout();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(input: NotebookInput): void {
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
input.hasBootstrapped = true;
|
||||
let params: INotebookParams = {
|
||||
notebookUri: input.notebookUri,
|
||||
input: input,
|
||||
providerInfo: input.getProviderInfo(),
|
||||
profile: input.connectionProfile
|
||||
};
|
||||
bootstrapAngular(this.instantiationService,
|
||||
NotebookModule,
|
||||
this._notebookContainer,
|
||||
NOTEBOOK_SELECTOR,
|
||||
params,
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import * as widgets from './widgets';
|
||||
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
|
||||
|
||||
/**
|
||||
* A mime renderer factory for raw html.
|
||||
*/
|
||||
export const htmlRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: ['text/html'],
|
||||
defaultRank: 50,
|
||||
createRenderer: options => new widgets.RenderedHTML(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A mime renderer factory for images.
|
||||
*/
|
||||
export const imageRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
|
||||
defaultRank: 90,
|
||||
createRenderer: options => new widgets.RenderedImage(options)
|
||||
};
|
||||
|
||||
// /**
|
||||
// * A mime renderer factory for LaTeX.
|
||||
// */
|
||||
// export const latexRendererFactory: IRenderMime.IRendererFactory = {
|
||||
// safe: true,
|
||||
// mimeTypes: ['text/latex'],
|
||||
// defaultRank: 70,
|
||||
// createRenderer: options => new widgets.RenderedLatex(options)
|
||||
// };
|
||||
|
||||
/**
|
||||
* A mime renderer factory for svg.
|
||||
*/
|
||||
export const svgRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['image/svg+xml'],
|
||||
defaultRank: 80,
|
||||
createRenderer: options => new widgets.RenderedSVG(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A mime renderer factory for plain and jupyter console text data.
|
||||
*/
|
||||
export const textRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'text/plain',
|
||||
'application/vnd.jupyter.stdout',
|
||||
'application/vnd.jupyter.stderr'
|
||||
],
|
||||
defaultRank: 120,
|
||||
createRenderer: options => new widgets.RenderedText(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A placeholder factory for deprecated rendered JavaScript.
|
||||
*/
|
||||
export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['text/javascript', 'application/javascript'],
|
||||
defaultRank: 110,
|
||||
createRenderer: options => new widgets.RenderedJavaScript(options)
|
||||
};
|
||||
|
||||
export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
defaultRank: 40,
|
||||
createRenderer: options => new widgets.RenderedDataResource(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* The standard factories provided by the rendermime package.
|
||||
*/
|
||||
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
|
||||
htmlRendererFactory,
|
||||
// latexRendererFactory,
|
||||
svgRendererFactory,
|
||||
imageRendererFactory,
|
||||
javaScriptRendererFactory,
|
||||
textRendererFactory,
|
||||
dataResourceRendererFactory
|
||||
];
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Component, Input, Inject, ViewChild, ElementRef } from '@angular/core';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||
import { IDataResource } from 'sql/workbench/services/notebook/sql/sqlSessionManager';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { getEolString, shouldIncludeHeaders, shouldRemoveNewLines } from 'sql/platform/query/common/queryRunner';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
|
||||
import { GridTableState } from 'sql/workbench/parts/query/common/gridPanelState';
|
||||
import { GridTableBase } from 'sql/workbench/parts/query/browser/gridPanel';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
@Component({
|
||||
selector: GridOutputComponent.SELECTOR,
|
||||
template: `<div #output class="notebook-cellTable"></div>`
|
||||
})
|
||||
export class GridOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||
public static readonly SELECTOR: string = 'grid-output';
|
||||
|
||||
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
|
||||
|
||||
private _initialized: boolean = false;
|
||||
private _cellModel: ICellModel;
|
||||
private _bundleOptions: MimeModel.IOptions;
|
||||
private _table: DataResourceTable;
|
||||
constructor(
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IThemeService) private readonly themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
this._bundleOptions = value;
|
||||
if (this._initialized) {
|
||||
this.renderGrid();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() mimeType: string;
|
||||
|
||||
get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this._initialized) {
|
||||
this.renderGrid();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.renderGrid();
|
||||
}
|
||||
|
||||
renderGrid(): void {
|
||||
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
|
||||
return;
|
||||
}
|
||||
if (!this._table) {
|
||||
let source = <IDataResource><any>this._bundleOptions.data[this.mimeType];
|
||||
let state = new GridTableState(0, 0);
|
||||
this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel.notebookModel.notebookUri.toString(), state);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.appendChild(this._table.element);
|
||||
this._register(attachTableStyler(this._table, this.themeService));
|
||||
this.layout();
|
||||
this._table.onAdd();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this._table) {
|
||||
let maxSize = Math.min(this._table.maximumSize, 500);
|
||||
this._table.layout(maxSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataResourceTable extends GridTableBase<any> {
|
||||
|
||||
private _gridDataProvider: IGridDataProvider;
|
||||
|
||||
constructor(source: IDataResource,
|
||||
documentUri: string,
|
||||
state: GridTableState,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||
this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri);
|
||||
}
|
||||
|
||||
get gridDataProvider(): IGridDataProvider {
|
||||
return this._gridDataProvider;
|
||||
}
|
||||
|
||||
protected getCurrentActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected getContextActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public get maximumSize(): number {
|
||||
// Overriding action bar size calculation for now.
|
||||
// When we add this back in, we should update this calculation
|
||||
return Math.max(this.maxSize, /* ACTIONBAR_HEIGHT + BOTTOM_PADDING */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
class DataResourceDataProvider implements IGridDataProvider {
|
||||
private rows: azdata.DbCellValue[][];
|
||||
constructor(source: IDataResource,
|
||||
private resultSet: azdata.ResultSetSummary,
|
||||
private documentUri: string,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
this.transformSource(source);
|
||||
}
|
||||
|
||||
private transformSource(source: IDataResource): void {
|
||||
this.rows = source.data.map(row => {
|
||||
let rowData: azdata.DbCellValue[] = [];
|
||||
Object.keys(row).forEach((val, index) => {
|
||||
let displayValue = String(Object.values(row)[index]);
|
||||
// Since the columns[0] represents the row number, start at 1
|
||||
rowData.push({
|
||||
displayValue: displayValue,
|
||||
isNull: false,
|
||||
invariantCultureDisplayValue: displayValue
|
||||
});
|
||||
});
|
||||
return rowData;
|
||||
});
|
||||
}
|
||||
|
||||
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
|
||||
let rowEnd = rowStart + numberOfRows;
|
||||
if (rowEnd > this.rows.length) {
|
||||
rowEnd = this.rows.length;
|
||||
}
|
||||
let resultSubset: azdata.QueryExecuteSubsetResult = {
|
||||
message: undefined,
|
||||
resultSubset: {
|
||||
rowCount: rowEnd - rowStart,
|
||||
rows: this.rows.slice(rowStart, rowEnd)
|
||||
}
|
||||
};
|
||||
return Promise.resolve(resultSubset);
|
||||
}
|
||||
|
||||
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
|
||||
this.copyResultsAsync(selection, includeHeaders);
|
||||
}
|
||||
|
||||
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||
try {
|
||||
let results = await getResultsString(this, selection, includeHeaders);
|
||||
this._clipboardService.writeText(results);
|
||||
} catch (error) {
|
||||
this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
|
||||
}
|
||||
}
|
||||
|
||||
getEolString(): string {
|
||||
return getEolString(this._textResourcePropertiesService, this.documentUri);
|
||||
}
|
||||
shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||
return shouldIncludeHeaders(includeHeaders, this._configurationService);
|
||||
}
|
||||
shouldRemoveNewLines(): boolean {
|
||||
return shouldRemoveNewLines(this._configurationService);
|
||||
}
|
||||
|
||||
getColumnHeaders(range: Slick.Range): string[] {
|
||||
let headers: string[] = this.resultSet.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
|
||||
return info.columnName;
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
get canSerialize(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
function createResultSet(source: IDataResource): azdata.ResultSetSummary {
|
||||
let columnInfo: azdata.IDbColumn[] = source.schema.fields.map(field => {
|
||||
let column = new SimpleDbColumn(field.name);
|
||||
if (field.type) {
|
||||
switch (field.type) {
|
||||
case 'xml':
|
||||
column.isXml = true;
|
||||
break;
|
||||
case 'json':
|
||||
column.isJson = true;
|
||||
break;
|
||||
default:
|
||||
// Only handling a few cases for now
|
||||
break;
|
||||
}
|
||||
}
|
||||
return column;
|
||||
});
|
||||
let summary: azdata.ResultSetSummary = {
|
||||
batchId: 0,
|
||||
id: 0,
|
||||
complete: true,
|
||||
rowCount: source.data.length,
|
||||
columnInfo: columnInfo
|
||||
};
|
||||
return summary;
|
||||
}
|
||||
|
||||
class SimpleDbColumn implements azdata.IDbColumn {
|
||||
|
||||
constructor(columnName: string) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
allowDBNull?: boolean;
|
||||
baseCatalogName: string;
|
||||
baseColumnName: string;
|
||||
baseSchemaName: string;
|
||||
baseServerName: string;
|
||||
baseTableName: string;
|
||||
columnName: string;
|
||||
columnOrdinal?: number;
|
||||
columnSize?: number;
|
||||
isAliased?: boolean;
|
||||
isAutoIncrement?: boolean;
|
||||
isExpression?: boolean;
|
||||
isHidden?: boolean;
|
||||
isIdentity?: boolean;
|
||||
isKey?: boolean;
|
||||
isBytes?: boolean;
|
||||
isChars?: boolean;
|
||||
isSqlVariant?: boolean;
|
||||
isUdt?: boolean;
|
||||
dataType: string;
|
||||
isXml?: boolean;
|
||||
isJson?: boolean;
|
||||
isLong?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isUnique?: boolean;
|
||||
numericPrecision?: number;
|
||||
numericScale?: number;
|
||||
udtAssemblyQualifiedName: string;
|
||||
dataTypeName: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div class="icon in-progress" *ngIf="loading === true"></div>
|
||||
<div #output link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto">
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,154 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!../cellViews/textCell';
|
||||
import 'vs/css!../cellViews/media/markdown';
|
||||
import 'vs/css!../cellViews/media/highlight';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/browser/outputs/sanitizer';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/browser/outputs/notebookMarkdown';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
@Component({
|
||||
selector: MarkdownOutputComponent.SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./markdownOutput.component.html'))
|
||||
})
|
||||
export class MarkdownOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||
public static readonly SELECTOR: string = 'markdown-output';
|
||||
|
||||
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
|
||||
|
||||
private _sanitizer: ISanitizer;
|
||||
private _lastTrustedMode: boolean;
|
||||
|
||||
private _bundleOptions: MimeModel.IOptions;
|
||||
private _initialized: boolean = false;
|
||||
public loading: boolean = false;
|
||||
private _cellModel: ICellModel;
|
||||
private _markdownRenderer: NotebookMarkdownRenderer;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(INotebookService) private _notebookService: INotebookService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
|
||||
|
||||
) {
|
||||
super();
|
||||
this._sanitizer = this._notebookService.getMimeRegistry().sanitizer;
|
||||
this._markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
this._bundleOptions = value;
|
||||
if (this._initialized) {
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() mimeType: string;
|
||||
|
||||
get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this._initialized) {
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
public get isTrusted(): boolean {
|
||||
return this._bundleOptions && this._bundleOptions.trusted;
|
||||
}
|
||||
|
||||
public get notebookUri(): URI {
|
||||
return this.cellModel.notebookModel.notebookUri;
|
||||
}
|
||||
|
||||
//Gets sanitizer from ISanitizer interface
|
||||
private get sanitizer(): ISanitizer {
|
||||
if (this._sanitizer) {
|
||||
return this._sanitizer;
|
||||
}
|
||||
return this._sanitizer = defaultSanitizer;
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.loading = isLoading;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the preview of markdown component with latest changes
|
||||
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
|
||||
* Sanitizes the data to be shown in markdown cell
|
||||
*/
|
||||
private updatePreview() {
|
||||
if (!this._bundleOptions || !this._cellModel) {
|
||||
return;
|
||||
}
|
||||
let trustedChanged = this._bundleOptions && this._lastTrustedMode !== this.isTrusted;
|
||||
if (trustedChanged || !this._initialized) {
|
||||
this._lastTrustedMode = this.isTrusted;
|
||||
let content = this._bundleOptions.data['text/markdown'];
|
||||
if (useInProcMarkdown(this._configurationService)) {
|
||||
this._markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
|
||||
let markdownResult = this._markdownRenderer.render({
|
||||
isTrusted: this.cellModel.trustedMode,
|
||||
value: content.toString()
|
||||
});
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = markdownResult.element.innerHTML;
|
||||
} else {
|
||||
if (!content) {
|
||||
|
||||
} else {
|
||||
this._commandService.executeCommand<string>('notebook.showPreview', this._cellModel.notebookModel.notebookUri, content).then((htmlcontent) => {
|
||||
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this._cellModel);
|
||||
htmlcontent = this.sanitizeContent(htmlcontent);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = htmlcontent;
|
||||
this.setLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Sanitizes the content based on trusted mode of Cell Model
|
||||
private sanitizeContent(content: string): string {
|
||||
if (this.isTrusted) {
|
||||
content = this.sanitizer.sanitize(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
public layout() {
|
||||
// Do we need to update on layout changed?
|
||||
}
|
||||
|
||||
public handleContentChanged(): void {
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
194
src/sql/workbench/parts/notebook/browser/outputs/mimeRegistry.ts
Normal file
194
src/sql/workbench/parts/notebook/browser/outputs/mimeRegistry.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { ReadonlyJSONObject } from 'sql/workbench/parts/notebook/common/models/jsonext';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
|
||||
export type FactoryIdentifier = string;
|
||||
|
||||
export const Extensions = {
|
||||
MimeComponentContribution: 'notebook.contributions.mimecomponents'
|
||||
};
|
||||
|
||||
export interface IMimeComponent {
|
||||
bundleOptions: MimeModel.IOptions;
|
||||
mimeType: string;
|
||||
cellModel?: ICellModel;
|
||||
layout(): void;
|
||||
}
|
||||
|
||||
export interface IMimeComponentDefinition {
|
||||
/**
|
||||
* Whether the component is a "safe" component.
|
||||
*
|
||||
* #### Notes
|
||||
* A "safe" component produces renderer widgets which can render
|
||||
* untrusted model data in a usable way. *All* renderers must
|
||||
* handle untrusted data safely, but some may simply failover
|
||||
* with a "Run cell to view output" message. A "safe" renderer
|
||||
* is an indication that its sanitized output will be useful.
|
||||
*/
|
||||
readonly safe: boolean;
|
||||
|
||||
/**
|
||||
* The mime types handled by this component.
|
||||
*/
|
||||
readonly mimeTypes: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The angular selector for this component
|
||||
*/
|
||||
readonly selector: string;
|
||||
/**
|
||||
* The default rank of the factory. If not given, defaults to 100.
|
||||
*/
|
||||
readonly rank?: number;
|
||||
|
||||
readonly ctor: Type<IMimeComponent>;
|
||||
}
|
||||
|
||||
export type SafetyLevel = 'ensure' | 'prefer' | 'any';
|
||||
type RankPair = { readonly id: number; readonly rank: number };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> rank pair.
|
||||
*/
|
||||
type RankMap = { [key: string]: RankPair };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> ordered factories.
|
||||
*/
|
||||
export type ComponentMap = { [key: string]: IMimeComponentDefinition };
|
||||
|
||||
export interface IMimeComponentRegistry {
|
||||
|
||||
/**
|
||||
* Add a MIME component to the registry.
|
||||
*
|
||||
* @param componentDefinition - The definition of this component including
|
||||
* the constructor to initialize it, supported `mimeTypes`, and `rank` order
|
||||
* of preference vs. other mime types.
|
||||
* If no `rank` is given, it will default to 100.
|
||||
*
|
||||
* #### Notes
|
||||
* The renderer will replace an existing renderer for the given
|
||||
* mimeType.
|
||||
*/
|
||||
registerComponentType(componentDefinition: IMimeComponentDefinition): void;
|
||||
|
||||
/**
|
||||
* Find the preferred mime type for a mime bundle.
|
||||
*
|
||||
* @param bundle - The bundle of mime data.
|
||||
*
|
||||
* @param safe - How to consider safe/unsafe factories. If 'ensure',
|
||||
* it will only consider safe factories. If 'any', any factory will be
|
||||
* considered. If 'prefer', unsafe factories will be considered, but
|
||||
* only after the safe options have been exhausted.
|
||||
*
|
||||
* @returns The preferred mime type from the available factories,
|
||||
* or `undefined` if the mime type cannot be rendered.
|
||||
*/
|
||||
getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel): string;
|
||||
getCtorFromMimeType(mimeType: string): Type<IMimeComponent>;
|
||||
getAllCtors(): Array<Type<IMimeComponent>>;
|
||||
getAllMimeTypes(): Array<string>;
|
||||
}
|
||||
|
||||
class MimeComponentRegistry implements IMimeComponentRegistry {
|
||||
private _id = 0;
|
||||
private _ranks: RankMap = {};
|
||||
private _types: string[] | null = null;
|
||||
private _componentDefinitions: ComponentMap = {};
|
||||
|
||||
registerComponentType(componentDefinition: IMimeComponentDefinition): void {
|
||||
let rank = !types.isUndefinedOrNull(componentDefinition.rank) ? componentDefinition.rank : 100;
|
||||
for (let mt of componentDefinition.mimeTypes) {
|
||||
this._componentDefinitions[mt] = componentDefinition;
|
||||
this._ranks[mt] = { rank, id: this._id++ };
|
||||
}
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
public getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel = 'ensure'): string | undefined {
|
||||
// Try to find a safe factory first, if preferred.
|
||||
if (safe === 'ensure' || safe === 'prefer') {
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle && this._componentDefinitions[mt].safe) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (safe !== 'ensure') {
|
||||
// Otherwise, search for the best factory among all factories.
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, no matching mime type exists.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getCtorFromMimeType(mimeType: string): Type<IMimeComponent> {
|
||||
let componentDescriptor = this._componentDefinitions[mimeType];
|
||||
return componentDescriptor ? componentDescriptor.ctor : undefined;
|
||||
}
|
||||
|
||||
public getAllCtors(): Array<Type<IMimeComponent>> {
|
||||
let addedCtors = [];
|
||||
let ctors = Object.values(this._componentDefinitions)
|
||||
.map((c: IMimeComponentDefinition) => c.ctor)
|
||||
.filter(ctor => {
|
||||
let shouldAdd = !addedCtors.find((ctor2) => ctor === ctor2);
|
||||
if (shouldAdd) {
|
||||
addedCtors.push(ctor);
|
||||
}
|
||||
return shouldAdd;
|
||||
});
|
||||
return ctors;
|
||||
}
|
||||
|
||||
public getAllMimeTypes(): Array<string> {
|
||||
return Object.keys(this._componentDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* The ordered list of mimeTypes.
|
||||
*/
|
||||
get mimeTypes(): ReadonlyArray<string> {
|
||||
return this._types || (this._types = sortedTypes(this._ranks));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const componentRegistry = new MimeComponentRegistry();
|
||||
platform.Registry.add(Extensions.MimeComponentContribution, componentRegistry);
|
||||
|
||||
export function registerComponentType(componentDefinition: IMimeComponentDefinition): void {
|
||||
componentRegistry.registerComponentType(componentDefinition);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the mime types in the map, ordered by rank.
|
||||
*/
|
||||
function sortedTypes(map: RankMap): string[] {
|
||||
return Object.keys(map).sort((a, b) => {
|
||||
let p1 = map[a];
|
||||
let p2 = map[b];
|
||||
if (p1.rank !== p2.rank) {
|
||||
return p1.rank - p2.rank;
|
||||
}
|
||||
return p1.id - p2.id;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { ElementRef, forwardRef, Inject, Component, OnInit, Input } from '@angular/core';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/browser/outputs/registry';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
@Component({
|
||||
selector: MimeRendererComponent.SELECTOR,
|
||||
template: ``
|
||||
})
|
||||
export class MimeRendererComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||
public static readonly SELECTOR = 'mime-output';
|
||||
private _bundleOptions: MimeModel.IOptions;
|
||||
private registry: RenderMimeRegistry;
|
||||
private _initialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) private el: ElementRef,
|
||||
@Inject(INotebookService) private _notebookService: INotebookService,
|
||||
) {
|
||||
super();
|
||||
this.registry = this._notebookService.getMimeRegistry();
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
this._bundleOptions = value;
|
||||
if (this._initialized) {
|
||||
this.renderOutput();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() mimeType: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.renderOutput();
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
// Re-layout the output when layout is requested
|
||||
this.renderOutput();
|
||||
}
|
||||
|
||||
private renderOutput(): void {
|
||||
// TODO handle safe/unsafe mapping
|
||||
this.createRenderedMimetype(this._bundleOptions, this.el.nativeElement);
|
||||
}
|
||||
|
||||
protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void {
|
||||
if (this.mimeType) {
|
||||
let renderer = this.registry.createRenderer(this.mimeType);
|
||||
renderer.node = node;
|
||||
let model = new MimeModel(options);
|
||||
renderer.renderModel(model).catch(error => {
|
||||
// Manually append error message to output
|
||||
renderer.node.innerHTML = `<pre>Javascript Error: ${error.message}</pre>`;
|
||||
// Remove mime-type-specific CSS classes
|
||||
renderer.node.className = 'p-Widget jp-RenderedText';
|
||||
renderer.node.setAttribute(
|
||||
'data-mime-type',
|
||||
'application/vnd.jupyter.stderr'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
node.innerHTML = localize('noRendererFound',
|
||||
"No {0} renderer could be found for output. It has the following MIME types: {1}",
|
||||
options.trusted ? '' : localize('safe', "(safe) "),
|
||||
Object.keys(options.data).join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
import { RenderOptions } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { IMarkdownString, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import marked = require('vs/base/common/marked/marked');
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Based off of HtmlContentRenderer
|
||||
export class NotebookMarkdownRenderer {
|
||||
private _notebookURI: URI;
|
||||
private _baseUrls: string[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
render(markdown: IMarkdownString): IMarkdownRenderResult {
|
||||
const element: HTMLElement = markdown ? this.renderMarkdown(markdown, undefined) : document.createElement('span');
|
||||
return {
|
||||
element,
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
|
||||
createElement(options: RenderOptions): HTMLElement {
|
||||
const tagName = options.inline ? 'span' : 'div';
|
||||
const element = document.createElement(tagName);
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
parse(text: string): any {
|
||||
let data = JSON.parse(text);
|
||||
data = revive(data, 0);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create html nodes for the given content element.
|
||||
* Adapted from htmlContentRenderer. Ensures that the markdown renderer
|
||||
* gets passed in the correct baseUrl for the notebook's saved location,
|
||||
* respects the trusted state of a notebook, and allows command links to
|
||||
* be clickable.
|
||||
*/
|
||||
renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement {
|
||||
const element = this.createElement(options);
|
||||
|
||||
// signal to code-block render that the element has been created
|
||||
let signalInnerHTML: () => void;
|
||||
const withInnerHTML = new Promise(c => signalInnerHTML = c);
|
||||
|
||||
let notebookFolder = path.dirname(this._notebookURI.path) + '/';
|
||||
if (!this._baseUrls.includes(notebookFolder)) {
|
||||
this._baseUrls.push(notebookFolder);
|
||||
}
|
||||
const renderer = new marked.Renderer({ baseUrl: notebookFolder });
|
||||
renderer.image = (href: string, title: string, text: string) => {
|
||||
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
|
||||
let dimensions: string[] = [];
|
||||
if (href) {
|
||||
const splitted = href.split('|').map(s => s.trim());
|
||||
href = splitted[0];
|
||||
const parameters = splitted[1];
|
||||
if (parameters) {
|
||||
const heightFromParams = /height=(\d+)/.exec(parameters);
|
||||
const widthFromParams = /width=(\d+)/.exec(parameters);
|
||||
const height = heightFromParams ? heightFromParams[1] : '';
|
||||
const width = widthFromParams ? widthFromParams[1] : '';
|
||||
const widthIsFinite = isFinite(parseInt(width));
|
||||
const heightIsFinite = isFinite(parseInt(height));
|
||||
if (widthIsFinite) {
|
||||
dimensions.push(`width="${width}"`);
|
||||
}
|
||||
if (heightIsFinite) {
|
||||
dimensions.push(`height="${height}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let attributes: string[] = [];
|
||||
if (href) {
|
||||
attributes.push(`src="${href}"`);
|
||||
}
|
||||
if (text) {
|
||||
attributes.push(`alt="${text}"`);
|
||||
}
|
||||
if (title) {
|
||||
attributes.push(`title="${title}"`);
|
||||
}
|
||||
if (dimensions.length) {
|
||||
attributes = attributes.concat(dimensions);
|
||||
}
|
||||
return '<img ' + attributes.join(' ') + '>';
|
||||
};
|
||||
renderer.link = (href: string, title: string, text: string): string => {
|
||||
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
|
||||
if (href === null) {
|
||||
return text;
|
||||
}
|
||||
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
|
||||
if (href === text) { // raw link case
|
||||
text = removeMarkdownEscapes(text);
|
||||
}
|
||||
title = removeMarkdownEscapes(title);
|
||||
href = removeMarkdownEscapes(href);
|
||||
if (
|
||||
!href
|
||||
|| !markdown.isTrusted
|
||||
|| href.match(/^data:|javascript:/i)
|
||||
|| href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)
|
||||
) {
|
||||
// drop the link
|
||||
return text;
|
||||
|
||||
} else {
|
||||
// HTML Encode href
|
||||
href = href.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return `<a href=${href} data-href="${href}" title="${title || href}">${text}</a>`;
|
||||
}
|
||||
};
|
||||
renderer.paragraph = (text): string => {
|
||||
return `<p>${text}</p>`;
|
||||
};
|
||||
|
||||
if (options.codeBlockRenderer) {
|
||||
renderer.code = (code, lang) => {
|
||||
const value = options.codeBlockRenderer!(lang, code);
|
||||
// when code-block rendering is async we return sync
|
||||
// but update the node with the real result later.
|
||||
const id = defaultGenerator.nextId();
|
||||
|
||||
const promise = value.then(strValue => {
|
||||
withInnerHTML.then(e => {
|
||||
const span = element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
span.innerHTML = strValue;
|
||||
}
|
||||
}).catch(err => {
|
||||
// ignore
|
||||
});
|
||||
});
|
||||
|
||||
if (options.codeBlockRenderCallback) {
|
||||
promise.then(options.codeBlockRenderCallback);
|
||||
}
|
||||
|
||||
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
|
||||
};
|
||||
}
|
||||
|
||||
const markedOptions: marked.MarkedOptions = {
|
||||
sanitize: !markdown.isTrusted,
|
||||
renderer,
|
||||
baseUrl: notebookFolder
|
||||
};
|
||||
|
||||
element.innerHTML = marked.parse(markdown.value, markedOptions);
|
||||
signalInnerHTML!();
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
// This following methods have been adapted from marked.js
|
||||
// Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/)
|
||||
cleanUrl(sanitize: boolean, base: string, href: string) {
|
||||
if (sanitize) {
|
||||
let prot: string;
|
||||
try {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (URI.parse(href)) {
|
||||
return href;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
let originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
||||
if (base && !originIndependentUrl.test(href) && !fs.existsSync(href)) {
|
||||
href = this.resolveUrl(base, href);
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
resolveUrl(base: string, href: string) {
|
||||
if (!this._baseUrls[' ' + base]) {
|
||||
// we can ignore everything in base after the last slash of its path component,
|
||||
// but we might need to add _that_
|
||||
// https://tools.ietf.org/html/rfc3986#section-3
|
||||
if (/^[^:]+:\/*[^/]*$/.test(base)) {
|
||||
this._baseUrls[' ' + base] = base + '/';
|
||||
} else {
|
||||
// Remove trailing 'c's. /c*$/ is vulnerable to REDOS.
|
||||
this._baseUrls[' ' + base] = base.replace(/c*$/, '');
|
||||
}
|
||||
}
|
||||
base = this._baseUrls[' ' + base];
|
||||
|
||||
if (href.slice(0, 2) === '//') {
|
||||
return base.replace(/:[\s\S]*/, ':') + href;
|
||||
} else if (href.charAt(0) === '/') {
|
||||
return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href;
|
||||
} else {
|
||||
return base + href;
|
||||
}
|
||||
}
|
||||
|
||||
// end marked.js adaptation
|
||||
|
||||
setNotebookURI(val: URI) {
|
||||
this._notebookURI = val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { OnInit, Component, Input, Inject, ElementRef, ViewChild } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
type ObjectType = object;
|
||||
|
||||
interface FigureLayout extends ObjectType {
|
||||
width?: string | number;
|
||||
height?: string;
|
||||
autosize?: boolean;
|
||||
}
|
||||
|
||||
interface Figure extends ObjectType {
|
||||
data: object[];
|
||||
layout: Partial<FigureLayout>;
|
||||
}
|
||||
|
||||
declare class PlotlyHTMLElement extends HTMLDivElement {
|
||||
data: object;
|
||||
layout: object;
|
||||
newPlot: () => void;
|
||||
redraw: () => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: PlotlyOutputComponent.SELECTOR,
|
||||
template: `<div #output class="plotly-wrapper"></div>
|
||||
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
|
||||
`
|
||||
})
|
||||
export class PlotlyOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||
public static readonly SELECTOR: string = 'plotly-output';
|
||||
|
||||
Plotly!: {
|
||||
newPlot: (
|
||||
div: PlotlyHTMLElement | null | undefined,
|
||||
data: object,
|
||||
layout: FigureLayout
|
||||
) => void;
|
||||
redraw: (div?: PlotlyHTMLElement) => void;
|
||||
};
|
||||
|
||||
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
|
||||
|
||||
private _initialized: boolean = false;
|
||||
private _rendered: boolean = false;
|
||||
private _cellModel: ICellModel;
|
||||
private _bundleOptions: MimeModel.IOptions;
|
||||
private _plotDiv: PlotlyHTMLElement;
|
||||
public errorText: string;
|
||||
|
||||
constructor(
|
||||
@Inject(IThemeService) private readonly themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
this._bundleOptions = value;
|
||||
if (this._initialized) {
|
||||
this.renderPlotly();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() mimeType: string;
|
||||
|
||||
get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this._initialized) {
|
||||
this.renderPlotly();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.Plotly = require.__$__nodeRequire('plotly.js-dist');
|
||||
this._plotDiv = this.output.nativeElement;
|
||||
this.renderPlotly();
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
renderPlotly(): void {
|
||||
if (this._rendered) {
|
||||
// just re-layout
|
||||
this.layout();
|
||||
return;
|
||||
}
|
||||
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
|
||||
return;
|
||||
}
|
||||
if (this.mimeType === 'text/vnd.plotly.v1+html') {
|
||||
// Do nothing - this is our way to ignore the offline init Plotly attempts to do via a <script> tag.
|
||||
// We have "handled" it by pulling in the plotly library into this component instead
|
||||
return;
|
||||
}
|
||||
this.errorText = undefined;
|
||||
const figure = this.getFigure(true);
|
||||
if (figure) {
|
||||
figure.layout = figure.layout || {};
|
||||
if (!figure.layout.width && !figure.layout.autosize) {
|
||||
// Workaround: to avoid filling up the entire cell, use plotly's default
|
||||
figure.layout.width = Math.min(700, this._plotDiv.clientWidth);
|
||||
}
|
||||
try {
|
||||
this.Plotly.newPlot(this._plotDiv, figure.data, figure.layout);
|
||||
} catch (error) {
|
||||
this.displayError(error);
|
||||
}
|
||||
}
|
||||
this._rendered = true;
|
||||
}
|
||||
|
||||
getFigure(showError: boolean): Figure {
|
||||
const figure = <Figure><any>this._bundleOptions.data[this.mimeType];
|
||||
if (typeof figure === 'string') {
|
||||
try {
|
||||
JSON.parse(figure);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
this.displayError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data = [], layout = {} } = figure;
|
||||
|
||||
return { data, layout };
|
||||
}
|
||||
|
||||
private displayError(error: Error | string): void {
|
||||
this.errorText = localize('plotlyError', 'Error displaying Plotly graph: {0}', getErrorMessage(error));
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
// No need to re-layout for now as Plotly is doing its own resize handling.
|
||||
}
|
||||
|
||||
public hasError(): boolean {
|
||||
return !types.isUndefinedOrNull(this.errorText);
|
||||
}
|
||||
|
||||
}
|
||||
352
src/sql/workbench/parts/notebook/browser/outputs/registry.ts
Normal file
352
src/sql/workbench/parts/notebook/browser/outputs/registry.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
|
||||
import { MimeModel } from '../../common/models/mimemodel';
|
||||
import { ReadonlyJSONObject } from '../../common/models/jsonext';
|
||||
import { defaultSanitizer } from './sanitizer';
|
||||
|
||||
/**
|
||||
* An object which manages mime renderer factories.
|
||||
*
|
||||
* This object is used to render mime models using registered mime
|
||||
* renderers, selecting the preferred mime renderer to render the
|
||||
* model into a widget.
|
||||
*
|
||||
* #### Notes
|
||||
* This class is not intended to be subclassed.
|
||||
*/
|
||||
export class RenderMimeRegistry {
|
||||
/**
|
||||
* Construct a new rendermime.
|
||||
*
|
||||
* @param options - The options for initializing the instance.
|
||||
*/
|
||||
constructor(options: RenderMimeRegistry.IOptions = {}) {
|
||||
// Parse the options.
|
||||
this.resolver = options.resolver || null;
|
||||
this.linkHandler = options.linkHandler || null;
|
||||
this.latexTypesetter = options.latexTypesetter || null;
|
||||
this.sanitizer = options.sanitizer || defaultSanitizer;
|
||||
|
||||
// Add the initial factories.
|
||||
if (options.initialFactories) {
|
||||
for (let factory of options.initialFactories) {
|
||||
this.addFactory(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizer used by the rendermime instance.
|
||||
*/
|
||||
readonly sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The object used to resolve relative urls for the rendermime instance.
|
||||
*/
|
||||
readonly resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* The object used to handle path opening links.
|
||||
*/
|
||||
readonly linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the rendermime.
|
||||
*/
|
||||
readonly latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
|
||||
/**
|
||||
* The ordered list of mimeTypes.
|
||||
*/
|
||||
get mimeTypes(): ReadonlyArray<string> {
|
||||
return this._types || (this._types = Private.sortedTypes(this._ranks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the preferred mime type for a mime bundle.
|
||||
*
|
||||
* @param bundle - The bundle of mime data.
|
||||
*
|
||||
* @param safe - How to consider safe/unsafe factories. If 'ensure',
|
||||
* it will only consider safe factories. If 'any', any factory will be
|
||||
* considered. If 'prefer', unsafe factories will be considered, but
|
||||
* only after the safe options have been exhausted.
|
||||
*
|
||||
* @returns The preferred mime type from the available factories,
|
||||
* or `undefined` if the mime type cannot be rendered.
|
||||
*/
|
||||
preferredMimeType(
|
||||
bundle: ReadonlyJSONObject,
|
||||
safe: 'ensure' | 'prefer' | 'any' = 'ensure'
|
||||
): string | undefined {
|
||||
// Try to find a safe factory first, if preferred.
|
||||
if (safe === 'ensure' || safe === 'prefer') {
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle && this._factories[mt].safe) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (safe !== 'ensure') {
|
||||
// Otherwise, search for the best factory among all factories.
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, no matching mime type exists.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a renderer for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns A new renderer for the given mime type.
|
||||
*
|
||||
* @throws An error if no factory exists for the mime type.
|
||||
*/
|
||||
createRenderer(mimeType: string): IRenderMime.IRenderer {
|
||||
// Throw an error if no factory exists for the mime type.
|
||||
if (!(mimeType in this._factories)) {
|
||||
throw new Error(`No factory for mime type: '${mimeType}'`);
|
||||
}
|
||||
|
||||
// Invoke the best factory for the given mime type.
|
||||
return this._factories[mimeType].createRenderer({
|
||||
mimeType,
|
||||
resolver: this.resolver,
|
||||
sanitizer: this.sanitizer,
|
||||
linkHandler: this.linkHandler,
|
||||
latexTypesetter: this.latexTypesetter
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mime model. This is a convenience method.
|
||||
*
|
||||
* @options - The options used to create the model.
|
||||
*
|
||||
* @returns A new mime model.
|
||||
*/
|
||||
createModel(options: MimeModel.IOptions = {}): MimeModel {
|
||||
return new MimeModel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of this rendermime instance.
|
||||
*
|
||||
* @param options - The options for configuring the clone.
|
||||
*
|
||||
* @returns A new independent clone of the rendermime.
|
||||
*/
|
||||
clone(options: RenderMimeRegistry.ICloneOptions = {}): RenderMimeRegistry {
|
||||
// Create the clone.
|
||||
let clone = new RenderMimeRegistry({
|
||||
resolver: options.resolver || this.resolver || undefined,
|
||||
sanitizer: options.sanitizer || this.sanitizer || undefined,
|
||||
linkHandler: options.linkHandler || this.linkHandler || undefined,
|
||||
latexTypesetter: options.latexTypesetter || this.latexTypesetter
|
||||
});
|
||||
|
||||
// Clone the internal state.
|
||||
clone._factories = { ...this._factories };
|
||||
clone._ranks = { ...this._ranks };
|
||||
clone._id = this._id;
|
||||
|
||||
// Return the cloned object.
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the renderer factory registered for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The factory for the mime type, or `undefined`.
|
||||
*/
|
||||
getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
|
||||
return this._factories[mimeType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a renderer factory to the rendermime.
|
||||
*
|
||||
* @param factory - The renderer factory of interest.
|
||||
*
|
||||
* @param rank - The rank of the renderer. A lower rank indicates
|
||||
* a higher priority for rendering. If not given, the rank will
|
||||
* defer to the `defaultRank` of the factory. If no `defaultRank`
|
||||
* is given, it will default to 100.
|
||||
*
|
||||
* #### Notes
|
||||
* The renderer will replace an existing renderer for the given
|
||||
* mimeType.
|
||||
*/
|
||||
addFactory(factory: IRenderMime.IRendererFactory, rank?: number): void {
|
||||
if (rank === undefined) {
|
||||
rank = factory.defaultRank;
|
||||
if (rank === undefined) {
|
||||
rank = 100;
|
||||
}
|
||||
}
|
||||
for (let mt of factory.mimeTypes) {
|
||||
this._factories[mt] = factory;
|
||||
this._ranks[mt] = { rank, id: this._id++ };
|
||||
}
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*/
|
||||
removeMimeType(mimeType: string): void {
|
||||
delete this._factories[mimeType];
|
||||
delete this._ranks[mimeType];
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rank for a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The rank of the mime type or undefined.
|
||||
*/
|
||||
getRank(mimeType: string): number | undefined {
|
||||
let rank = this._ranks[mimeType];
|
||||
return rank && rank.rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rank of a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @param rank - The new rank to assign.
|
||||
*
|
||||
* #### Notes
|
||||
* This is a no-op if the mime type is not registered.
|
||||
*/
|
||||
setRank(mimeType: string, rank: number): void {
|
||||
if (!this._ranks[mimeType]) {
|
||||
return;
|
||||
}
|
||||
let id = this._id++;
|
||||
this._ranks[mimeType] = { rank, id };
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
private _id = 0;
|
||||
private _ranks: Private.RankMap = {};
|
||||
private _types: string[] | null = null;
|
||||
private _factories: Private.FactoryMap = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for `RenderMimeRegistry` class statics.
|
||||
*/
|
||||
export namespace RenderMimeRegistry {
|
||||
/**
|
||||
* The options used to initialize a rendermime instance.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Initial factories to add to the rendermime instance.
|
||||
*/
|
||||
initialFactories?: ReadonlyArray<IRenderMime.IRendererFactory>;
|
||||
|
||||
/**
|
||||
* The sanitizer used to sanitize untrusted html inputs.
|
||||
*
|
||||
* If not given, a default sanitizer will be used.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The initial resolver object.
|
||||
*
|
||||
* The default is `null`.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* An optional path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* An optional LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to clone a rendermime instance.
|
||||
*/
|
||||
export interface ICloneOptions {
|
||||
/**
|
||||
* The new sanitizer used to sanitize untrusted html inputs.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The new resolver object.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* The new path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* The new LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the module implementation details.
|
||||
*/
|
||||
namespace Private {
|
||||
/**
|
||||
* A type alias for a mime rank and tie-breaking id.
|
||||
*/
|
||||
export type RankPair = { readonly id: number; readonly rank: number };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> rank pair.
|
||||
*/
|
||||
export type RankMap = { [key: string]: RankPair };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> ordered factories.
|
||||
*/
|
||||
export type FactoryMap = { [key: string]: IRenderMime.IRendererFactory };
|
||||
|
||||
/**
|
||||
* Get the mime types in the map, ordered by rank.
|
||||
*/
|
||||
export function sortedTypes(map: RankMap): string[] {
|
||||
return Object.keys(map).sort((a, b) => {
|
||||
let p1 = map[a];
|
||||
let p2 = map[b];
|
||||
if (p1.rank !== p2.rank) {
|
||||
return p1.rank - p2.rank;
|
||||
}
|
||||
return p1.id - p2.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
649
src/sql/workbench/parts/notebook/browser/outputs/renderers.ts
Normal file
649
src/sql/workbench/parts/notebook/browser/outputs/renderers.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { default as AnsiUp } from 'ansi_up';
|
||||
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
|
||||
import { URLExt } from '../../common/models/url';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
|
||||
/**
|
||||
* Render HTML into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderHTML(options: renderHTML.IOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let {
|
||||
host,
|
||||
source,
|
||||
trusted,
|
||||
sanitizer,
|
||||
resolver,
|
||||
linkHandler,
|
||||
shouldTypeset,
|
||||
latexTypesetter
|
||||
} = options;
|
||||
|
||||
let originalSource = source;
|
||||
|
||||
// Bail early if the source is empty.
|
||||
if (!source) {
|
||||
host.textContent = '';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Sanitize the source if it is not trusted. This removes all
|
||||
// `<script>` tags as well as other potentially harmful HTML.
|
||||
if (!trusted) {
|
||||
originalSource = `${source}`;
|
||||
source = sanitizer.sanitize(source);
|
||||
}
|
||||
|
||||
// Set the inner HTML of the host.
|
||||
host.innerHTML = source;
|
||||
|
||||
if (host.getElementsByTagName('script').length > 0) {
|
||||
// If output it trusted, eval any script tags contained in the HTML.
|
||||
// This is not done automatically by the browser when script tags are
|
||||
// created by setting `innerHTML`.
|
||||
if (trusted) {
|
||||
Private.evalInnerHTMLScriptTags(host);
|
||||
} else {
|
||||
const container = document.createElement('div');
|
||||
const warning = document.createElement('pre');
|
||||
warning.textContent =
|
||||
'This HTML output contains inline scripts. Are you sure that you want to run arbitrary Javascript within your Notebook session?';
|
||||
const runButton = document.createElement('button');
|
||||
runButton.textContent = 'Run';
|
||||
runButton.onclick = event => {
|
||||
host.innerHTML = originalSource;
|
||||
Private.evalInnerHTMLScriptTags(host);
|
||||
host.removeChild(host.firstChild);
|
||||
};
|
||||
container.appendChild(warning);
|
||||
container.appendChild(runButton);
|
||||
host.insertBefore(container, host.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default behavior of nodes.
|
||||
Private.handleDefaults(host, resolver);
|
||||
|
||||
// Patch the urls if a resolver is available.
|
||||
let promise: Promise<void>;
|
||||
if (resolver) {
|
||||
promise = Private.handleUrls(host, resolver, linkHandler);
|
||||
} else {
|
||||
promise = Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Return the final rendered promise.
|
||||
return promise.then(() => {
|
||||
if (shouldTypeset && latexTypesetter) {
|
||||
latexTypesetter.typeset(host);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderHTML` function statics.
|
||||
*/
|
||||
export namespace renderHTML {
|
||||
/**
|
||||
* The options for the `renderHTML` function.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* The host node for the rendered HTML.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The HTML source to render.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the source is trusted.
|
||||
*/
|
||||
trusted: boolean;
|
||||
|
||||
/**
|
||||
* The html sanitizer for untrusted source.
|
||||
*/
|
||||
sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* An optional url resolver.
|
||||
*/
|
||||
resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* An optional link handler.
|
||||
*/
|
||||
linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* Whether the node should be typeset.
|
||||
*/
|
||||
shouldTypeset: boolean;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the application.
|
||||
*/
|
||||
latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an image into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderImage(
|
||||
options: renderImage.IRenderOptions
|
||||
): Promise<void> {
|
||||
// Unpack the options.
|
||||
let {
|
||||
host,
|
||||
mimeType,
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
needsBackground,
|
||||
unconfined
|
||||
} = options;
|
||||
|
||||
// Clear the content in the host.
|
||||
host.textContent = '';
|
||||
|
||||
// Create the image element.
|
||||
let img = document.createElement('img');
|
||||
|
||||
// Set the source of the image.
|
||||
img.src = `data:${mimeType};base64,${source}`;
|
||||
|
||||
// Set the size of the image if provided.
|
||||
if (typeof height === 'number') {
|
||||
img.height = height;
|
||||
}
|
||||
if (typeof width === 'number') {
|
||||
img.width = width;
|
||||
}
|
||||
|
||||
if (needsBackground === 'light') {
|
||||
img.classList.add('jp-needs-light-background');
|
||||
} else if (needsBackground === 'dark') {
|
||||
img.classList.add('jp-needs-dark-background');
|
||||
}
|
||||
|
||||
if (unconfined === true) {
|
||||
img.classList.add('jp-mod-unconfined');
|
||||
}
|
||||
|
||||
// Add the image to the host.
|
||||
host.appendChild(img);
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderImage` function statics.
|
||||
*/
|
||||
export namespace renderImage {
|
||||
/**
|
||||
* The options for the `renderImage` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The image node to update with the content.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The mime type for the image.
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* The base64 encoded source for the image.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* The optional width for the image.
|
||||
*/
|
||||
width?: number;
|
||||
|
||||
/**
|
||||
* The optional height for the image.
|
||||
*/
|
||||
height?: number;
|
||||
|
||||
/**
|
||||
* Whether an image requires a background for legibility.
|
||||
*/
|
||||
needsBackground?: string;
|
||||
|
||||
/**
|
||||
* Whether the image should be unconfined.
|
||||
*/
|
||||
unconfined?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render LaTeX into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderLatex(
|
||||
options: renderLatex.IRenderOptions
|
||||
): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source, shouldTypeset, latexTypesetter } = options;
|
||||
|
||||
// Set the source on the node.
|
||||
host.textContent = source;
|
||||
|
||||
// Typeset the node if needed.
|
||||
if (shouldTypeset && latexTypesetter) {
|
||||
latexTypesetter.typeset(host);
|
||||
}
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderLatex` function statics.
|
||||
*/
|
||||
export namespace renderLatex {
|
||||
/**
|
||||
* The options for the `renderLatex` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered LaTeX.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The LaTeX source to render.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the node should be typeset.
|
||||
*/
|
||||
shouldTypeset: boolean;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the application.
|
||||
*/
|
||||
latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderDataResource` function statics.
|
||||
*/
|
||||
export namespace renderDataResource {
|
||||
/**
|
||||
* The options for the `renderDataResource` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered LaTeX.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The DataResource source to render.
|
||||
*/
|
||||
source: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render SVG into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source, trusted, unconfined } = options;
|
||||
|
||||
// Clear the content if there is no source.
|
||||
if (!source) {
|
||||
host.textContent = '';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Display a message if the source is not trusted.
|
||||
if (!trusted) {
|
||||
host.textContent =
|
||||
'Cannot display an untrusted SVG. Maybe you need to run the cell?';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Render in img so that user can save it easily
|
||||
const img = new Image();
|
||||
img.src = `data:image/svg+xml,${encodeURIComponent(source)}`;
|
||||
host.appendChild(img);
|
||||
|
||||
if (unconfined === true) {
|
||||
host.classList.add('jp-mod-unconfined');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderSVG` function statics.
|
||||
*/
|
||||
export namespace renderSVG {
|
||||
/**
|
||||
* The options for the `renderSVG` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered SVG.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The SVG source.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the source is trusted.
|
||||
*/
|
||||
trusted: boolean;
|
||||
|
||||
/**
|
||||
* Whether the svg should be unconfined.
|
||||
*/
|
||||
unconfined?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderText(options: renderText.IRenderOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source } = options;
|
||||
|
||||
const ansiUp = new AnsiUp();
|
||||
ansiUp.escape_for_html = true;
|
||||
ansiUp.use_classes = true;
|
||||
|
||||
// Create the HTML content.
|
||||
let content = ansiUp.ansi_to_html(source);
|
||||
|
||||
// Set the inner HTML for the host node.
|
||||
host.innerHTML = `<pre>${content}</pre>`;
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderText` function statics.
|
||||
*/
|
||||
export namespace renderText {
|
||||
/**
|
||||
* The options for the `renderText` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the text content.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The source text to render.
|
||||
*/
|
||||
source: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for module implementation details.
|
||||
*/
|
||||
namespace Private {
|
||||
/**
|
||||
* Eval the script tags contained in a host populated by `innerHTML`.
|
||||
*
|
||||
* When script tags are created via `innerHTML`, the browser does not
|
||||
* evaluate them when they are added to the page. This function works
|
||||
* around that by creating new equivalent script nodes manually, and
|
||||
* replacing the originals.
|
||||
*/
|
||||
export function evalInnerHTMLScriptTags(host: HTMLElement): void {
|
||||
// Create a snapshot of the current script nodes.
|
||||
let scripts = host.getElementsByTagName('script');
|
||||
|
||||
// Loop over each script node.
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
let script = scripts.item(i);
|
||||
// Skip any scripts which no longer have a parent.
|
||||
if (!script.parentNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new script node which will be clone.
|
||||
let clone = document.createElement('script');
|
||||
|
||||
// Copy the attributes into the clone.
|
||||
let attrs = script.attributes;
|
||||
for (let i = 0, n = attrs.length; i < n; ++i) {
|
||||
let { name, value } = attrs[i];
|
||||
clone.setAttribute(name, value);
|
||||
}
|
||||
|
||||
// Copy the text content into the clone.
|
||||
clone.textContent = script.textContent;
|
||||
|
||||
// Replace the old script in the parent.
|
||||
script.parentNode.replaceChild(clone, script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the default behavior of nodes.
|
||||
*/
|
||||
export function handleDefaults(
|
||||
node: HTMLElement,
|
||||
resolver?: IRenderMime.IResolver
|
||||
): void {
|
||||
// Handle anchor elements.
|
||||
let anchors = node.getElementsByTagName('a');
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
let path = anchors[i].href || '';
|
||||
const isLocal =
|
||||
resolver && resolver.isLocal
|
||||
? resolver.isLocal(path)
|
||||
: URLExt.isLocal(path);
|
||||
if (isLocal) {
|
||||
anchors[i].target = '_self';
|
||||
} else {
|
||||
anchors[i].target = '_blank';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle image elements.
|
||||
let imgs = node.getElementsByTagName('img');
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].alt) {
|
||||
imgs[i].alt = 'Image';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the relative urls in element `src` and `href` attributes.
|
||||
*
|
||||
* @param node - The head html element.
|
||||
*
|
||||
* @param resolver - A url resolver.
|
||||
*
|
||||
* @param linkHandler - An optional link handler for nodes.
|
||||
*
|
||||
* @returns a promise fulfilled when the relative urls have been resolved.
|
||||
*/
|
||||
export function handleUrls(
|
||||
node: HTMLElement,
|
||||
resolver: IRenderMime.IResolver,
|
||||
linkHandler: IRenderMime.ILinkHandler | null
|
||||
): Promise<void> {
|
||||
// Set up an array to collect promises.
|
||||
let promises: Promise<void>[] = [];
|
||||
|
||||
// Handle HTML Elements with src attributes.
|
||||
let nodes = node.querySelectorAll('*[src]');
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
|
||||
}
|
||||
|
||||
// Handle anchor elements.
|
||||
let anchors = node.getElementsByTagName('a');
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
promises.push(handleAnchor(anchors[i], resolver, linkHandler));
|
||||
}
|
||||
|
||||
// Handle link elements.
|
||||
let links = node.getElementsByTagName('link');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
promises.push(handleAttr(links[i], 'href', resolver));
|
||||
}
|
||||
|
||||
// Wait on all promises.
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply ids to headers.
|
||||
*/
|
||||
export function headerAnchors(node: HTMLElement): void {
|
||||
let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
for (let headerType of headerNames) {
|
||||
let headers = node.getElementsByTagName(headerType);
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
let header = headers[i];
|
||||
header.id = encodeURIComponent(header.innerHTML.replace(/ /g, '-'));
|
||||
let anchor = document.createElement('a');
|
||||
anchor.target = '_self';
|
||||
anchor.textContent = '¶';
|
||||
anchor.href = '#' + header.id;
|
||||
anchor.classList.add('jp-InternalAnchorLink');
|
||||
header.appendChild(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a node with a `src` or `href` attribute.
|
||||
*/
|
||||
function handleAttr(
|
||||
node: HTMLElement,
|
||||
name: 'src' | 'href',
|
||||
resolver: IRenderMime.IResolver
|
||||
): Promise<void> {
|
||||
let source = node.getAttribute(name) || '';
|
||||
const isLocal = resolver.isLocal
|
||||
? resolver.isLocal(source)
|
||||
: URLExt.isLocal(source);
|
||||
if (!source || !isLocal) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
node.setAttribute(name, '');
|
||||
return resolver
|
||||
.resolveUrl(source)
|
||||
.then(path => {
|
||||
return resolver.getDownloadUrl(path);
|
||||
})
|
||||
.then(url => {
|
||||
// Check protocol again in case it changed:
|
||||
if (URI.parse(url).scheme !== 'data:') {
|
||||
// Bust caching for local src attrs.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
||||
url += (/\?/.test(url) ? '&' : '?') + new Date().getTime();
|
||||
}
|
||||
node.setAttribute(name, url);
|
||||
})
|
||||
.catch(err => {
|
||||
// If there was an error getting the url,
|
||||
// just make it an empty link.
|
||||
node.setAttribute(name, '');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an anchor node.
|
||||
*/
|
||||
function handleAnchor(
|
||||
anchor: HTMLAnchorElement,
|
||||
resolver: IRenderMime.IResolver,
|
||||
linkHandler: IRenderMime.ILinkHandler | null
|
||||
): Promise<void> {
|
||||
// Get the link path without the location prepended.
|
||||
// (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
|
||||
let href = anchor.getAttribute('href') || '';
|
||||
const isLocal = resolver.isLocal
|
||||
? resolver.isLocal(href)
|
||||
: URLExt.isLocal(href);
|
||||
// Bail if it is not a file-like url.
|
||||
if (!href || !isLocal) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// Remove the hash until we can handle it.
|
||||
let hash = anchor.hash;
|
||||
if (hash) {
|
||||
// Handle internal link in the file.
|
||||
if (hash === href) {
|
||||
anchor.target = '_self';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// For external links, remove the hash until we have hash handling.
|
||||
href = href.replace(hash, '');
|
||||
}
|
||||
// Get the appropriate file path.
|
||||
return resolver
|
||||
.resolveUrl(href)
|
||||
.then(path => {
|
||||
// Handle the click override.
|
||||
if (linkHandler) {
|
||||
linkHandler.handleLink(anchor, path, hash);
|
||||
}
|
||||
// Get the appropriate file download path.
|
||||
return resolver.getDownloadUrl(path);
|
||||
})
|
||||
.then(url => {
|
||||
// Set the visible anchor.
|
||||
anchor.href = url + hash;
|
||||
})
|
||||
.catch(err => {
|
||||
// If there was an error getting the url,
|
||||
// just make it an empty link.
|
||||
anchor.href = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
1052
src/sql/workbench/parts/notebook/browser/outputs/sanitizer.ts
Normal file
1052
src/sql/workbench/parts/notebook/browser/outputs/sanitizer.ts
Normal file
File diff suppressed because it is too large
Load Diff
378
src/sql/workbench/parts/notebook/browser/outputs/widgets.ts
Normal file
378
src/sql/workbench/parts/notebook/browser/outputs/widgets.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as renderers from './renderers';
|
||||
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
|
||||
import { ReadonlyJSONObject } from '../../common/models/jsonext';
|
||||
import * as tableRenderers from 'sql/workbench/parts/notebook/browser/outputs/tableRenderers';
|
||||
|
||||
/**
|
||||
* A common base class for mime renderers.
|
||||
*/
|
||||
export abstract class RenderedCommon implements IRenderMime.IRenderer {
|
||||
private _node: HTMLElement;
|
||||
private cachedClasses: string[] = [];
|
||||
/**
|
||||
* Construct a new rendered common widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
this.mimeType = options.mimeType;
|
||||
this.sanitizer = options.sanitizer;
|
||||
this.resolver = options.resolver;
|
||||
this.linkHandler = options.linkHandler;
|
||||
this.latexTypesetter = options.latexTypesetter;
|
||||
}
|
||||
|
||||
public get node(): HTMLElement {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
public set node(value: HTMLElement) {
|
||||
this._node = value;
|
||||
value.dataset['mimeType'] = this.mimeType;
|
||||
this._node.classList.add(...this.cachedClasses);
|
||||
this.cachedClasses = [];
|
||||
}
|
||||
|
||||
toggleClass(className: string, enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.addClass(className);
|
||||
} else {
|
||||
this.removeClass(className);
|
||||
}
|
||||
}
|
||||
|
||||
addClass(className: string): void {
|
||||
if (!this._node) {
|
||||
this.cachedClasses.push(className);
|
||||
} else if (!this._node.classList.contains(className)) {
|
||||
this._node.classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
removeClass(className: string): void {
|
||||
if (!this._node) {
|
||||
this.cachedClasses = this.cachedClasses.filter(c => c !== className);
|
||||
} else if (this._node.classList.contains(className)) {
|
||||
this._node.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The mimetype being rendered.
|
||||
*/
|
||||
readonly mimeType: string;
|
||||
|
||||
/**
|
||||
* The sanitizer used to sanitize untrusted html inputs.
|
||||
*/
|
||||
readonly sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The resolver object.
|
||||
*/
|
||||
readonly resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* The link handler.
|
||||
*/
|
||||
readonly linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The latexTypesetter.
|
||||
*/
|
||||
readonly latexTypesetter: IRenderMime.ILatexTypesetter;
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
// TODO compare model against old model for early bail?
|
||||
|
||||
// Toggle the trusted class on the widget.
|
||||
this.toggleClass('jp-mod-trusted', model.trusted);
|
||||
|
||||
// Render the actual content.
|
||||
return this.render(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
abstract render(model: IRenderMime.IMimeModel): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A common base class for HTML mime renderers.
|
||||
*/
|
||||
export abstract class RenderedHTMLCommon extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered HTML common widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedHTMLCommon');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mime renderer for displaying HTML and math.
|
||||
*/
|
||||
export class RenderedHTML extends RenderedHTMLCommon {
|
||||
/**
|
||||
* Construct a new rendered HTML widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderHTML({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType]),
|
||||
trusted: model.trusted,
|
||||
resolver: this.resolver,
|
||||
sanitizer: this.sanitizer,
|
||||
linkHandler: this.linkHandler,
|
||||
shouldTypeset: true, //this.isAttached,
|
||||
latexTypesetter: this.latexTypesetter
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A mime renderer for displaying LaTeX output.
|
||||
// */
|
||||
// export class RenderedLatex extends RenderedCommon {
|
||||
// /**
|
||||
// * Construct a new rendered LaTeX widget.
|
||||
// *
|
||||
// * @param options - The options for initializing the widget.
|
||||
// */
|
||||
// constructor(options: IRenderMime.IRendererOptions) {
|
||||
// super(options);
|
||||
// this.addClass('jp-RenderedLatex');
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Render a mime model.
|
||||
// *
|
||||
// * @param model - The mime model to render.
|
||||
// *
|
||||
// * @returns A promise which resolves when rendering is complete.
|
||||
// */
|
||||
// render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
// return renderers.renderLatex({
|
||||
// host: this.node,
|
||||
// source: String(model.data[this.mimeType]),
|
||||
// shouldTypeset: this.isAttached,
|
||||
// latexTypesetter: this.latexTypesetter
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* A mime renderer for displaying images.
|
||||
*/
|
||||
export class RenderedImage extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered image widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedImage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
|
||||
return renderers.renderImage({
|
||||
host: this.node,
|
||||
mimeType: this.mimeType,
|
||||
source: String(model.data[this.mimeType]),
|
||||
width: metadata && (metadata.width as number | undefined),
|
||||
height: metadata && (metadata.height as number | undefined),
|
||||
needsBackground: model.metadata['needs_background'] as string | undefined,
|
||||
unconfined: metadata && (metadata.unconfined as boolean | undefined)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying SVG content.
|
||||
*/
|
||||
export class RenderedSVG extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered SVG widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedSVG');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
|
||||
return renderers.renderSVG({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType]),
|
||||
trusted: model.trusted,
|
||||
unconfined: metadata && (metadata.unconfined as boolean | undefined)
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying plain text and console text.
|
||||
*/
|
||||
export class RenderedText extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered text widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedText');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderText({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying deprecated JavaScript output.
|
||||
*/
|
||||
export class RenderedJavaScript extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered text widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedJavaScript');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderText({
|
||||
host: this.node,
|
||||
source: 'JavaScript output is disabled in Notebooks'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying Data Resource schemas and data.
|
||||
*/
|
||||
export class RenderedDataResource extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered data resource widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedDataResource');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return tableRenderers.renderDataResource({
|
||||
host: this.node,
|
||||
source: JSON.stringify(model.data[this.mimeType]),
|
||||
themeService: model.themeService
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user