move code from parts to contrib (#8319)
@@ -0,0 +1,251 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, Separator } 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/browser/notebookService';
|
||||
import { CellActionBase, CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
|
||||
import { CellTypes, CellType } from 'sql/workbench/contrib/notebook/common/models/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ToggleMoreWidgetAction } from 'sql/workbench/contrib/dashboard/browser/core/actions';
|
||||
import { CellModel } from 'sql/workbench/contrib/notebook/browser/models/cell';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
export const HIDDEN_CLASS = 'actionhidden';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: (Action | CellActionBase)[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
private _moreActionsElement: HTMLElement;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(RunCellsAction, 'runAllBefore', localize('runAllBefore', "Run Cells Before"), false),
|
||||
instantiationService.createInstance(RunCellsAction, 'runAllAfter', localize('runAllAfter', "Run Cells After"), true),
|
||||
new Separator(),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeBefore', localize('codeBefore', "Insert Code Before"), CellTypes.Code, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', "Insert Code After"), CellTypes.Code, true),
|
||||
new Separator(),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', "Insert Text Before"), CellTypes.Markdown, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', "Insert Text After"), CellTypes.Markdown, true),
|
||||
new Separator(),
|
||||
instantiationService.createInstance(CollapseCellAction, 'collapseCell', localize('collapseCell', "Collapse Cell"), true),
|
||||
instantiationService.createInstance(CollapseCellAction, 'expandCell', localize('expandCell', "Expand Cell"), false),
|
||||
new Separator(),
|
||||
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', "Clear Result")),
|
||||
new Separator(),
|
||||
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', "Delete")),
|
||||
);
|
||||
}
|
||||
|
||||
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 instanceof Separator || a instanceof CellActionBase && a.canRun(context));
|
||||
this.removeDuplicatedAndStartingSeparators(validActions);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private removeDuplicatedAndStartingSeparators(actions: (Action | CellActionBase)[]): void {
|
||||
let indexesToRemove: number[] = [];
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
// Never should have a separator at the beginning of the list
|
||||
if (i === 0 && actions[i] instanceof Separator) {
|
||||
indexesToRemove.push(0);
|
||||
}
|
||||
// Handle multiple separators in a row
|
||||
if (i > 0 && actions[i] instanceof Separator && actions[i - 1] instanceof Separator) {
|
||||
indexesToRemove.push(i);
|
||||
}
|
||||
}
|
||||
if (indexesToRemove.length > 0) {
|
||||
for (let i = indexesToRemove.length - 1; i >= 0; i--) {
|
||||
actions.splice(indexesToRemove[i], 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = firstIndex(model.cells, (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();
|
||||
}
|
||||
}
|
||||
|
||||
export class CollapseCellAction extends CellActionBase {
|
||||
constructor(id: string,
|
||||
label: string,
|
||||
private collapseCell: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
public canRun(context: CellContext): boolean {
|
||||
return context.cell && context.cell.cellType === CellTypes.Code;
|
||||
}
|
||||
|
||||
async doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let cell = context.cell || context.model.activeCell;
|
||||
if (cell) {
|
||||
if (this.collapseCell) {
|
||||
if (!cell.isCollapsed) {
|
||||
cell.isCollapsed = true;
|
||||
}
|
||||
} else {
|
||||
if (cell.isCollapsed) {
|
||||
cell.isCollapsed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -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: row" (mouseover)="hover=true" (mouseleave)="hover=false">
|
||||
<div #toolbar class="toolbar">
|
||||
</div>
|
||||
<div style="flex: 1 1 auto; flex-flow: column; overflow: hidden;">
|
||||
<div #editor class="editor"></div>
|
||||
<collapse-component *ngIf="cellModel.cellType === 'code' && cellModel.source && cellModel.source.length > 1" [cellModel]="cellModel" [activeCellId]="activeCellId"></collapse-component>
|
||||
</div>
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,366 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/contrib/notebook/browser/cellToggleMoreActions';
|
||||
import { ICellModel, notebookConstants, CellExecutionState } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/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/contrib/notebook/common/models/contracts';
|
||||
import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import * as notebookUtils from 'sql/workbench/contrib/notebook/browser/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';
|
||||
import { CollapseComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/collapse.component';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
|
||||
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;
|
||||
@ViewChild(CollapseComponent) private collapseComponent: CollapseComponent;
|
||||
|
||||
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.toggleActionsVisibility(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.toggleActionsVisibility(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();
|
||||
}));
|
||||
}
|
||||
|
||||
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.cellModel.modelContentChangedEvent = e;
|
||||
|
||||
let originalSourceLength = this.cellModel.source.length;
|
||||
this.cellModel.source = this._editorModel.getValue();
|
||||
if (this._cellModel.isCollapsed && originalSourceLength !== this.cellModel.source.length) {
|
||||
this._cellModel.isCollapsed = false;
|
||||
}
|
||||
this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed);
|
||||
|
||||
this.onContentChanged.emit();
|
||||
this.checkForLanguageMagics();
|
||||
}));
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.fontSize')) {
|
||||
this._editor.setHeightToScrollHeight(true, this._cellModel.isCollapsed);
|
||||
}
|
||||
}));
|
||||
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._register(this.cellModel.onCollapseStateChanged(isCollapsed => {
|
||||
this.onCellCollapse(isCollapsed);
|
||||
}));
|
||||
|
||||
this.layout();
|
||||
|
||||
if (this._cellModel.isCollapsed) {
|
||||
this.onCellCollapse(true);
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._editor.layout(new DOM.Dimension(
|
||||
DOM.getContentWidth(this.codeElement.nativeElement),
|
||||
DOM.getContentHeight(this.codeElement.nativeElement)));
|
||||
this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed);
|
||||
}
|
||||
|
||||
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 offsetParent is null, the element isn't visible
|
||||
// In this case, we don't want a cell to grab focus for an editor that isn't in the foreground.
|
||||
// In addition, ensure that the ownerDocument itself has focus for scenarios where ADS isn't in the foreground
|
||||
let ownerDocument = this._editor.getContainer().ownerDocument;
|
||||
if (this.cellModel.id === this._activeCellId && this._editor.getContainer().offsetParent && ownerDocument && ownerDocument.hasFocus()) {
|
||||
this._editor.focus();
|
||||
this._editor.getContainer().scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleActionsVisibility(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
|
||||
if (this.collapseComponent) {
|
||||
this.collapseComponent.toggleIconVisibility(isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
|
||||
private onCellCollapse(isCollapsed: boolean): void {
|
||||
let editorWidget = this._editor.getControl() as ICodeEditor;
|
||||
if (isCollapsed) {
|
||||
let model = editorWidget.getModel();
|
||||
let totalLines = model.getLineCount();
|
||||
let endColumn = model.getLineMaxColumn(totalLines);
|
||||
editorWidget.setHiddenAreas([{
|
||||
startLineNumber: 2,
|
||||
startColumn: 1,
|
||||
endLineNumber: totalLines,
|
||||
endColumn: endColumn
|
||||
}]);
|
||||
} else {
|
||||
editorWidget.setHiddenAreas([]);
|
||||
}
|
||||
this._editor.setHeightToScrollHeight(false, isCollapsed);
|
||||
}
|
||||
}
|
||||
120
src/sql/workbench/contrib/notebook/browser/cellViews/code.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 .codicon {
|
||||
background-size: 20px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .codicon.hideIcon {
|
||||
width: 0px;
|
||||
padding-left: 0px;
|
||||
padding-top: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .codicon.hideIcon.execCountTen {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .codicon.hideIcon.execCountHundred {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container {
|
||||
padding-left: 10px
|
||||
}
|
||||
|
||||
code-component .hide-component-button {
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-width: 0px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
code-component .hide-component-button.icon-hide-cell {
|
||||
background-image: url("./media/light/chevron_up.svg");
|
||||
}
|
||||
|
||||
code-component .hide-component-button.icon-show-cell {
|
||||
background-image: url("./media/light/chevron_down.svg");
|
||||
}
|
||||
|
||||
.vs-dark code-component .hide-component-button.icon-hide-cell,
|
||||
.hc-black code-component .hide-component-button.icon-hide-cell {
|
||||
background-image: url("./media/dark/chevron_up_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark code-component .hide-component-button.icon-show-cell,
|
||||
.hc-black code-component .hide-component-button.icon-show-cell {
|
||||
background-image: url("./media/dark/chevron_down_inverse.svg");
|
||||
}
|
||||
@@ -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/contrib/notebook/browser/models/notebookModel';
|
||||
import { ICellModel, CellExecutionState } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { MultiStateAction, IMultiStateData } from 'sql/workbench/contrib/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,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/contrib/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/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.updateActiveCell(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.onCollapseStateChanged((state) => {
|
||||
this._changeRef.detectChanges();
|
||||
}));
|
||||
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,10 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="width: 100%; height: fit-content; display: flex; flex-flow: column">
|
||||
<button #collapseCellButton (click)="toggleCollapsed($event)" class="hide-component-button"></button>
|
||||
<button #expandCellButton (click)="toggleCollapsed($event)" style="display:none" class="hide-component-button icon-show-cell"></button>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ElementRef, ViewChild, SimpleChange, OnChanges } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
|
||||
export const COLLAPSE_SELECTOR: string = 'collapse-component';
|
||||
|
||||
@Component({
|
||||
selector: COLLAPSE_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./collapse.component.html'))
|
||||
})
|
||||
|
||||
export class CollapseComponent extends CellView implements OnInit, OnChanges {
|
||||
@ViewChild('collapseCellButton', { read: ElementRef }) private collapseCellButtonElement: ElementRef;
|
||||
@ViewChild('expandCellButton', { read: ElementRef }) private expandCellButtonElement: ElementRef;
|
||||
|
||||
@Input() cellModel: ICellModel;
|
||||
@Input() activeCellId: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._register(this.cellModel.onCollapseStateChanged(isCollapsed => {
|
||||
this.handleCellCollapse(isCollapsed);
|
||||
}));
|
||||
this.handleCellCollapse(this.cellModel.isCollapsed);
|
||||
if (this.activeCellId === this.cellModel.id) {
|
||||
this.toggleIconVisibility(true);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCellCollapse(isCollapsed: boolean): void {
|
||||
let collapseButton = <HTMLElement>this.collapseCellButtonElement.nativeElement;
|
||||
let expandButton = <HTMLElement>this.expandCellButtonElement.nativeElement;
|
||||
if (isCollapsed) {
|
||||
collapseButton.style.display = 'none';
|
||||
expandButton.style.display = 'block';
|
||||
} else {
|
||||
collapseButton.style.display = 'block';
|
||||
expandButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
public toggleCollapsed(event?: Event): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.cellModel.isCollapsed = !this.cellModel.isCollapsed;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
public toggleIconVisibility(isActiveOrHovered: boolean) {
|
||||
let collapseButton = <HTMLElement>this.collapseCellButtonElement.nativeElement;
|
||||
let buttonClass = 'icon-hide-cell';
|
||||
if (isActiveOrHovered) {
|
||||
collapseButton.classList.add(buttonClass);
|
||||
} else {
|
||||
collapseButton.classList.remove(buttonClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { URI } from 'vs/base/common/uri';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
|
||||
const knownSchemes = new Set(['http', 'https', 'file', 'mailto', 'data', '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,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.1484 3.64844L15.8516 4.35156L8 12.2031L0.148438 4.35156L0.851562 3.64844L8 10.7969L15.1484 3.64844Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2734 11.9766L8 4.71094L0.726562 11.9766L0.0234375 11.2734L8 3.28906L15.9766 11.2734L15.2734 11.9766Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@@ -0,0 +1 @@
|
||||
<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,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.1484 3.64844L15.8516 4.35156L8 12.2031L0.148438 4.35156L0.851562 3.64844L8 10.7969L15.1484 3.64844Z" fill="#4F4F4F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 234 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2734 11.9766L8 4.71094L0.726562 11.9766L0.0234375 11.2734L8 3.28906L15.9766 11.2734L15.2734 11.9766Z" fill="#4F4F4F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 235 B |
@@ -0,0 +1 @@
|
||||
<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,476 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| 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;
|
||||
}
|
||||
|
||||
output-component .grid-panel .action-label.codicon {
|
||||
min-width: 16px;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -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/contrib/notebook/browser/models/modelInterfaces';
|
||||
import * as outputProcessor from 'sql/workbench/contrib/notebook/browser/models/outputProcessor';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { ComponentHostDirective } from 'sql/workbench/contrib/dashboard/browser/core/componentHost.directive';
|
||||
import { Extensions, IMimeComponent, IMimeComponentRegistry } from 'sql/workbench/contrib/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,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/contrib/notebook/browser/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 offsetParent is null, the element isn't visible
|
||||
// In this case, we don't want a cell to grab focus for an editor that isn't in the foreground
|
||||
if (node && node.offsetParent) {
|
||||
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, ChangeDetectorRef, SimpleChange, OnChanges } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CellType } from 'sql/workbench/contrib/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(type);
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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,
|
||||
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 } 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/contrib/notebook/browser/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(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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,262 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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 { NotebookMarkdownRenderer } from 'sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown';
|
||||
import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/workbench/contrib/notebook/browser/outputs/sanitizer';
|
||||
import { CellToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/cellToggleMoreActions';
|
||||
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/contrib/notebook/browser/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.updateActiveCell(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(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@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 cellModelSourceJoined = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
|
||||
let contentJoined = Array.isArray(this._content) ? this._content.join('') : this._content;
|
||||
let contentChanged = contentJoined !== cellModelSourceJoined || cellModelSourceJoined.length === 0;
|
||||
if (trustedChanged || contentChanged) {
|
||||
this._lastTrustedMode = this.cellModel.trustedMode;
|
||||
if ((!cellModelSourceJoined) && !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,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4894FE;}
|
||||
</style>
|
||||
<title>add_12x12</title>
|
||||
<path class="st0" d="M8,0c0.7,0,1.4,0.1,2.1,0.3C12.8,1,15,3.2,15.7,5.9c0.4,1.4,0.4,2.9,0,4.2c-0.8,2.7-2.9,4.8-5.6,5.6
|
||||
c-1.4,0.4-2.9,0.4-4.2,0C3.2,15,1,12.8,0.3,10.1c-0.4-1.4-0.4-2.9,0-4.2C1,3.2,3.2,1,5.9,0.3C6.6,0.1,7.3,0,8,0z M7.9,14.9
|
||||
c0.6,0,1.2-0.1,1.8-0.2c0.6-0.2,1.1-0.4,1.6-0.7c1-0.6,1.8-1.4,2.4-2.4c0.3-0.5,0.5-1.1,0.7-1.6c0.3-1.2,0.3-2.4,0-3.6
|
||||
c-0.2-0.6-0.4-1.1-0.7-1.6c-0.6-1-1.4-1.9-2.4-2.4C10.8,2,10.2,1.8,9.7,1.6c-1.2-0.3-2.4-0.3-3.6,0C5.5,1.8,5,2,4.5,2.3
|
||||
C3.4,2.9,2.6,3.7,2,4.7C1.7,5.3,1.5,5.8,1.4,6.4C1,7.5,1,8.8,1.4,9.9c0.2,0.6,0.4,1.1,0.7,1.6c0.6,1,1.4,1.8,2.4,2.4
|
||||
c0.5,0.3,1.1,0.5,1.6,0.7C6.7,14.8,7.3,14.9,7.9,14.9L7.9,14.9z M8.6,7.5h3.6V9H8.6v3.6H7.1V9H3.5V7.5h3.6V3.9h1.4L8.6,7.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7C1.27083 7 1.52865 6.94792 1.77344 6.84375C2.01823 6.73438 2.22917 6.59115 2.40625 6.41406C2.58854 6.23177 2.73177 6.02083 2.83594 5.78125C2.94531 5.53646 3 5.27604 3 5C3 4.63021 2.99219 4.26042 2.97656 3.89062C2.96094 3.52083 2.97135 3.16146 3.00781 2.8125C3.04948 2.45833 3.13281 2.11719 3.25781 1.78906C3.38802 1.46094 3.59375 1.15625 3.875 0.875C4.15625 0.59375 4.48177 0.377604 4.85156 0.226562C5.22135 0.0755208 5.60417 0 6 0V1C5.72396 1 5.46354 1.05469 5.21875 1.16406C4.97917 1.26823 4.76823 1.41146 4.58594 1.59375C4.40885 1.77083 4.26562 1.98177 4.15625 2.22656C4.05208 2.47135 4 2.72917 4 3C4 3.29167 4.00521 3.58073 4.01562 3.86719C4.02604 4.14844 4.02604 4.42708 4.01562 4.70312C4.01042 4.97396 3.98698 5.23958 3.94531 5.5C3.90365 5.75521 3.83333 6 3.73438 6.23438C3.63542 6.46875 3.5 6.69271 3.32812 6.90625C3.15625 7.11979 2.9349 7.31771 2.66406 7.5C2.9349 7.68229 3.15625 7.88021 3.32812 8.09375C3.5 8.30729 3.63542 8.53125 3.73438 8.76562C3.83333 9 3.90365 9.2474 3.94531 9.50781C3.98698 9.76302 4.01042 10.0286 4.01562 10.3047C4.02604 10.5755 4.02604 10.8542 4.01562 11.1406C4.00521 11.4219 4 11.7083 4 12C4 12.276 4.05208 12.5365 4.15625 12.7812C4.26562 13.0208 4.40885 13.2318 4.58594 13.4141C4.76823 13.5911 4.97917 13.7344 5.21875 13.8438C5.46354 13.9479 5.72396 14 6 14V15C5.60417 15 5.22135 14.9245 4.85156 14.7734C4.48177 14.6224 4.15625 14.4062 3.875 14.125C3.59375 13.8438 3.38802 13.5391 3.25781 13.2109C3.13281 12.8828 3.04948 12.5443 3.00781 12.1953C2.97135 11.8411 2.96094 11.4792 2.97656 11.1094C2.99219 10.7396 3 10.3698 3 10C3 9.72917 2.94531 9.47135 2.83594 9.22656C2.73177 8.98177 2.58854 8.77083 2.40625 8.59375C2.22917 8.41146 2.01823 8.26823 1.77344 8.16406C1.52865 8.05469 1.27083 8 1 8V7ZM10 0C10.3958 0 10.7786 0.0755208 11.1484 0.226562C11.5182 0.377604 11.8438 0.59375 12.125 0.875C12.4062 1.15625 12.6094 1.46094 12.7344 1.78906C12.8646 2.11719 12.9479 2.45833 12.9844 2.8125C13.026 3.16146 13.0391 3.52083 13.0234 3.89062C13.0078 4.26042 13 4.63021 13 5C13 5.27604 13.0521 5.53646 13.1562 5.78125C13.2656 6.02083 13.4089 6.23177 13.5859 6.41406C13.7682 6.59115 13.9792 6.73438 14.2188 6.84375C14.4635 6.94792 14.724 7 15 7V8C14.724 8 14.4635 8.05469 14.2188 8.16406C13.9792 8.26823 13.7682 8.41146 13.5859 8.59375C13.4089 8.77083 13.2656 8.98177 13.1562 9.22656C13.0521 9.47135 13 9.72917 13 10C13 10.3698 13.0078 10.7396 13.0234 11.1094C13.0391 11.4792 13.026 11.8411 12.9844 12.1953C12.9479 12.5443 12.8646 12.8828 12.7344 13.2109C12.6094 13.5391 12.4062 13.8438 12.125 14.125C11.8438 14.4062 11.5182 14.6224 11.1484 14.7734C10.7786 14.9245 10.3958 15 10 15V14C10.2708 14 10.5286 13.9479 10.7734 13.8438C11.0182 13.7344 11.2292 13.5911 11.4062 13.4141C11.5885 13.2318 11.7318 13.0208 11.8359 12.7812C11.9453 12.5365 12 12.276 12 12C12 11.7083 11.9948 11.4219 11.9844 11.1406C11.974 10.8542 11.9714 10.5755 11.9766 10.3047C11.987 10.0286 12.013 9.76302 12.0547 9.50781C12.0964 9.2474 12.1667 9 12.2656 8.76562C12.3646 8.53125 12.5 8.30729 12.6719 8.09375C12.8438 7.88021 13.0651 7.68229 13.3359 7.5C13.0651 7.31771 12.8438 7.11979 12.6719 6.90625C12.5 6.69271 12.3646 6.46875 12.2656 6.23438C12.1667 6 12.0964 5.75521 12.0547 5.5C12.013 5.23958 11.987 4.97396 11.9766 4.70312C11.9714 4.42708 11.974 4.14844 11.9844 3.86719C11.9948 3.58073 12 3.29167 12 3C12 2.72917 11.9453 2.47135 11.8359 2.22656C11.7318 1.98177 11.5885 1.77083 11.4062 1.59375C11.2292 1.41146 11.0182 1.26823 10.7734 1.16406C10.5286 1.05469 10.2708 1 10 1V0Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><defs><style>.cls-1{fill:#fff;}</style></defs><title>add_12x12</title><path class="cls-1" d="M6,.88a5.17,5.17,0,0,1,1.4.19A5.26,5.26,0,0,1,11,4.73a5.3,5.3,0,0,1,0,2.79,5.26,5.26,0,0,1-3.67,3.67,5.3,5.3,0,0,1-2.79,0A5.26,5.26,0,0,1,.9,7.53a5.3,5.3,0,0,1,0-2.79A5.26,5.26,0,0,1,4.57,1.07,5.17,5.17,0,0,1,6,.88Zm0,9.75a4.4,4.4,0,0,0,1.2-.16A4.56,4.56,0,0,0,8.24,10,4.5,4.5,0,0,0,9.85,8.4a4.56,4.56,0,0,0,.45-1.08,4.51,4.51,0,0,0,0-2.39,4.56,4.56,0,0,0-.45-1.08A4.5,4.5,0,0,0,8.24,2.24a4.56,4.56,0,0,0-1.08-.45,4.51,4.51,0,0,0-2.39,0,4.56,4.56,0,0,0-1.08.45A4.5,4.5,0,0,0,2.08,3.86a4.56,4.56,0,0,0-.45,1.08,4.51,4.51,0,0,0,0,2.39A4.56,4.56,0,0,0,2.08,8.4,4.5,4.5,0,0,0,3.7,10a4.56,4.56,0,0,0,1.08.45A4.4,4.4,0,0,0,6,10.63Zm.38-4.88H8.22V6.5H6.34V8.38H5.59V6.5H3.72V5.75H5.59V3.88h.75Z"/></svg>
|
||||
|
After Width: | Height: | Size: 882 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 3.03125V14.9375H2.09375V11.9375H3V14.0312H15.0938V3.9375H9V3.03125H16ZM2.92188 9.9375H0V9.59375C0.0989583 9.57812 0.197917 9.5651 0.296875 9.55469C0.395833 9.53906 0.492188 9.51823 0.585938 9.49219C0.658854 9.47135 0.736979 9.40365 0.820312 9.28906C0.908854 9.17448 0.992188 9.04688 1.07031 8.90625C1.14844 8.76562 1.21875 8.6276 1.28125 8.49219C1.34896 8.35156 1.39844 8.24479 1.42969 8.17188L4.46094 1H5.10156L8.08594 8.25C8.13281 8.36458 8.1875 8.49479 8.25 8.64062C8.31771 8.78646 8.39323 8.92708 8.47656 9.0625C8.5651 9.19271 8.66406 9.30729 8.77344 9.40625C8.88802 9.5 9.01562 9.55469 9.15625 9.57031C9.30208 9.58594 9.44792 9.59375 9.59375 9.59375V9.9375H6.10938V9.59375C6.19792 9.59375 6.29427 9.59635 6.39844 9.60156C6.5026 9.60156 6.59896 9.59115 6.6875 9.57031C6.78125 9.54427 6.85938 9.5026 6.92188 9.44531C6.98438 9.38281 7.01562 9.28646 7.01562 9.15625C7.01562 9.04167 6.98698 8.88542 6.92969 8.6875C6.8724 8.48438 6.80469 8.27604 6.72656 8.0625C6.64844 7.84896 6.56771 7.64323 6.48438 7.44531C6.40625 7.24219 6.34115 7.08073 6.28906 6.96094H2.80469C2.75781 7.08073 2.6875 7.24219 2.59375 7.44531C2.50521 7.64323 2.41667 7.84896 2.32812 8.0625C2.24479 8.27604 2.16927 8.48177 2.10156 8.67969C2.03906 8.8776 2.00781 9.03385 2.00781 9.14844C2.00781 9.27344 2.03906 9.36719 2.10156 9.42969C2.16406 9.48698 2.24219 9.52604 2.33594 9.54688C2.42969 9.56771 2.52865 9.58073 2.63281 9.58594C2.74219 9.58594 2.83854 9.58854 2.92188 9.59375V9.9375ZM3.14062 5.96094H5.82812L4.45312 3.00781L3.14062 5.96094Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4894FE;}
|
||||
</style>
|
||||
<title>clear_results_inverse</title>
|
||||
<path class="st0" d="M8.7,13.8H12v1H3.5l-3.2-3.2c-0.1-0.1-0.2-0.3-0.3-0.4c-0.1-0.3-0.1-0.7,0-1.1C0.2,10,0.3,9.9,0.4,9.7l9.4-9.4
|
||||
L16,6.6L8.7,13.8z M7.3,13.8l1-1L3.5,8l-2.4,2.4c-0.1,0.2-0.1,0.4,0,0.6L4,13.8H7.3z M9.8,1.8L4.2,7.3L9,12.1l5.5-5.5L9.8,1.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{clip-path:url(#clip-path-3);}.cls-5{clip-path:url(#clip-path-4);}.cls-6{clip-path:url(#clip-path-5);}.cls-7{clip-path:url(#clip-path-6);}.cls-8{clip-path:url(#clip-path-7);}.cls-9{clip-path:url(#clip-path-8);}.cls-10{fill:#fff;}</style><clipPath id="clip-path"><rect class="cls-1" x="81.22" y="32.62" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-2"><rect class="cls-1" x="-712.01" y="400.9" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-3"><rect class="cls-1" x="-711.01" y="400.9" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-4"><rect class="cls-1" x="-708.37" y="407.17" width="10.73" height="0.43"/></clipPath><clipPath id="clip-path-5"><rect class="cls-1" x="-708.07" y="405.93" width="10.14" height="0.43"/></clipPath><clipPath id="clip-path-6"><rect class="cls-1" x="-707.66" y="404.69" width="9.31" height="0.43"/></clipPath><clipPath id="clip-path-7"><rect class="cls-1" x="-707.25" y="403.46" width="8.5" height="0.43"/></clipPath><clipPath id="clip-path-8"><rect class="cls-1" x="-706.83" y="402.22" width="7.65" height="0.43"/></clipPath></defs><title>clear_results_inverse</title><path class="cls-10" d="M8.71,13.84H12v1H3.54L.39,11.68a1.29,1.29,0,0,1-.29-.44,1.42,1.42,0,0,1,0-1.05,1.34,1.34,0,0,1,.3-.45L9.75.38,16,6.59Zm-1.42,0,1-1L3.5,8l-2.39,2.4a.4.4,0,0,0,0,.55L4,13.84Zm2.46-12L4.2,7.34,9,12.13l5.54-5.54Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<title>opac_command_icons_bv</title>
|
||||
<path d="M0,1H16V2H0ZM.148,6.148,2.5,3.8,4.852,6.148l-.7.7L3,5.711V15H2V5.711L.852,6.852ZM7,5V4h9V5Z" fill="#fff"/>
|
||||
<rect width="16" height="16" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1 @@
|
||||
<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;}.cls-2{fill:#388a34;}</style></defs><title>new_notebook_inverse</title><path class="cls-1" d="M11.87,1.24V.33H9.13A3.78,3.78,0,0,0,7.92.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.67v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.74Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17H11v9.12ZM12.87,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-2" points="16 12.19 16 13.13 13.8 13.13 13.8 15.33 12.87 15.33 12.87 13.13 10.67 13.13 10.67 12.19 12.87 12.19 12.87 9.99 13.8 9.99 13.8 12.19 16 12.19"/><path class="cls-2" d="M13.8,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{fill:#4894fe;}.cls-3{fill:#d02e00;}.cls-4{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><rect class="cls-1" x="-804.35" y="-93.32" width="15.81" height="13.48"/></clipPath></defs><title>Not_trusted_command_inverse</title><path class="cls-2" d="M13.74,2.25a9.47,9.47,0,0,1-1.19-.17,6.53,6.53,0,0,1-1.14-.35,6.23,6.23,0,0,1-1.12-.64C10.07,1,9.85.83,9.62.72S9.21.54,9,.46A4.79,4.79,0,0,0,8.26.25c-.25,0-.51,0-.76,0A4.77,4.77,0,0,0,6,.46a5.51,5.51,0,0,0-1.29.63,6.64,6.64,0,0,1-1.12.59A6.53,6.53,0,0,1,2.45,2a9.58,9.58,0,0,1-1.19.22H0v4A7.5,7.5,0,0,0,.29,8.41a9.37,9.37,0,0,0,.78,1.94,10.35,10.35,0,0,0,1.2,1.74,12.59,12.59,0,0,0,1.49,1.52,15.55,15.55,0,0,0,1.7,1.33c.59.42,1.19.8,1.8,1.15l.24.16.24-.14c.61-.35,1.21-.73,1.8-1.15a11.79,11.79,0,0,0,1.24-1l-.7-.7a12.52,12.52,0,0,1-1,.76c-.53.37-1.06.7-1.59,1-.53-.3-1.06-.64-1.59-1a16.79,16.79,0,0,1-1.5-1.17A13.12,13.12,0,0,1,3,11.46,9.41,9.41,0,0,1,2,9.9a8.86,8.86,0,0,1-.72-1.74A7.23,7.23,0,0,1,1,6.25v-3a9.37,9.37,0,0,0,2.23-.37,7.81,7.81,0,0,0,2-.95,4.67,4.67,0,0,1,1.06-.5,4.22,4.22,0,0,1,2.37,0,4.67,4.67,0,0,1,1.06.5,7.81,7.81,0,0,0,2,1A9.31,9.31,0,0,0,14,3.25v3a7.21,7.21,0,0,1-.26,1.91A8.35,8.35,0,0,1,13,9.9l-.1.18.73.73a2.69,2.69,0,0,0,.28-.46,8.85,8.85,0,0,0,.78-1.94A7.52,7.52,0,0,0,15,6.25v-4Z"/><polygon class="cls-3" points="13.82 13.49 16 15.66 15.43 16.23 13.26 14.05 11.07 16.23 10.5 15.66 12.69 13.49 10.46 11.26 11.03 10.69 13.26 12.92 15.43 10.74 16 11.3 13.82 13.49"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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;}.cls-2{fill:#d02e00;}</style></defs><title>nontrust-inverse</title><path class="cls-1" d="M13.74,2a9.49,9.49,0,0,1-1.19-.17,7.1,7.1,0,0,1-1.14-.35A6.72,6.72,0,0,1,10.29.85,7.43,7.43,0,0,0,9.62.48,5.31,5.31,0,0,0,9,.22a4.18,4.18,0,0,0-.7-.16A6.07,6.07,0,0,0,7.5,0,4.81,4.81,0,0,0,6,.22,5.34,5.34,0,0,0,4.71.85a6.72,6.72,0,0,1-1.12.59,7.09,7.09,0,0,1-1.14.35A9.48,9.48,0,0,1,1.26,2C.85,2,.43,2,0,2V6A7.62,7.62,0,0,0,.29,8.17a9.15,9.15,0,0,0,.78,1.94,10.78,10.78,0,0,0,1.2,1.74,13.35,13.35,0,0,0,1.49,1.52,15.81,15.81,0,0,0,1.7,1.33c.59.42,1.19.8,1.8,1.15L7.5,16l.24-.14c.61-.35,1.21-.73,1.8-1.15a12.71,12.71,0,0,0,1.24-1l-.7-.7c-.32.27-.65.52-1,.76q-.8.55-1.59,1-.79-.46-1.59-1a15.89,15.89,0,0,1-1.51-1.2A13.66,13.66,0,0,1,3,11.22,9.59,9.59,0,0,1,2,9.66a8.52,8.52,0,0,1-.72-1.74A7.1,7.1,0,0,1,1,6V3a9.54,9.54,0,0,0,2.23-.37,8.08,8.08,0,0,0,2-.95,4.4,4.4,0,0,1,1.06-.5A3.87,3.87,0,0,1,7.5,1a3.87,3.87,0,0,1,1.16.16,4.4,4.4,0,0,1,1.06.5,8.08,8.08,0,0,0,2,.95A9.54,9.54,0,0,0,14,3V6a7.1,7.1,0,0,1-.26,1.91A8.51,8.51,0,0,1,13,9.66l-.1.18.73.73a3.14,3.14,0,0,0,.28-.46,9.15,9.15,0,0,0,.78-1.94A7.62,7.62,0,0,0,15,6V2C14.57,2,14.15,2,13.74,2Z"/><polygon class="cls-2" points="13.82 13.25 16 15.42 15.43 15.99 13.26 13.81 11.07 15.99 10.5 15.42 12.69 13.25 10.46 11.02 11.03 10.45 13.26 12.68 15.43 10.5 16 11.06 13.82 13.25"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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:#4894fe;}</style></defs><title>run_cells_command_inverse</title><path class="cls-1" d="M8,15.92a8,8,0,1,1,8-8A8,8,0,0,1,8,15.92ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/><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: 357 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{fill:#fff;}.cls-3{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><rect class="cls-1" x="-552.55" y="-7.09" width="15.81" height="13.48"/></clipPath></defs><title>run_cells_inverse</title><path class="cls-2" d="M8,15.92a8,8,0,1,1,8-8A8,8,0,0,1,8,15.92ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/><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: 549 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<title>opac_command_icons_bv</title>
|
||||
<path d="M0,1H16V2H0ZM3,13.289l1.148-1.141.7.7L2.5,15.2.148,12.852l.7-.7L2,13.289V4H3ZM7,5V4h9V5ZM7,8V7h9V8Zm0,3V10h9v1Zm0,3V13h9v1Z" fill="#fff"/>
|
||||
<rect width="16" height="16" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1 @@
|
||||
<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>stop_cell_inverse</title><circle class="cls-1" cx="8" cy="7.92" r="7.76"/><rect x="5" y="4.92" width="6" height="6"/></svg>
|
||||
|
After Width: | Height: | Size: 269 B |
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4894FE;}
|
||||
.st1{fill:#061C08;}
|
||||
.st2{fill:#3BB44A;}
|
||||
</style>
|
||||
<title>trust_inverse</title>
|
||||
<path class="st0" d="M13.7,2c-0.4,0-0.8-0.1-1.2-0.2c-0.4-0.1-0.8-0.2-1.1-0.4c-0.4-0.2-0.8-0.4-1.1-0.6c-0.2-0.1-0.4-0.3-0.7-0.4
|
||||
C9.2,0.3,8.7,0.1,8.3,0.1C8,0,7.8,0,7.5,0C7,0,6.5,0.1,6,0.2C5.5,0.4,5.1,0.6,4.7,0.9C4.4,1.1,4,1.3,3.6,1.5
|
||||
C3.2,1.6,2.8,1.7,2.5,1.8C2.1,1.9,1.7,2,1.3,2C0.9,2,0.4,2,0,2v4c0,0.7,0.1,1.5,0.3,2.2c0.2,0.7,0.4,1.3,0.8,1.9
|
||||
c0.3,0.6,0.7,1.2,1.2,1.7c0.5,0.5,1,1.1,1.5,1.5c0.5,0.5,1.1,0.9,1.7,1.3c0.6,0.4,1.2,0.8,1.8,1.1L7.5,16l0.2-0.1
|
||||
c0.3-0.2,0.6-0.3,0.9-0.5l-0.7-0.7l-0.4,0.2c-0.5-0.3-1.1-0.6-1.6-1c-0.5-0.4-1-0.8-1.5-1.2c-0.5-0.4-1-0.9-1.4-1.4
|
||||
c-0.4-0.5-0.7-1-1-1.6C1.7,9.1,1.5,8.5,1.3,7.9C1.1,7.3,1,6.7,1,6V3c0.8,0,1.5-0.2,2.2-0.4c0.7-0.2,1.4-0.6,2-1
|
||||
c0.3-0.2,0.7-0.4,1.1-0.5C6.7,1,7.1,1,7.5,1c0.4,0,0.8,0,1.2,0.2C9,1.3,9.4,1.4,9.7,1.7c0.6,0.4,1.3,0.8,2,1C12.5,2.9,13.2,3,14,3v3
|
||||
c0,0.6-0.1,1.3-0.3,1.9c-0.2,0.6-0.4,1.2-0.7,1.8c-0.3,0.6-0.6,1.1-1,1.6c-0.3,0.3-0.5,0.6-0.8,0.9l0.7,0.7c0.3-0.3,0.6-0.7,0.9-1
|
||||
c0.5-0.5,0.9-1.1,1.2-1.7c0.3-0.6,0.6-1.3,0.8-1.9C14.9,7.4,15,6.7,15,6V2C14.6,2,14.1,2,13.7,2z M10,13.1c-0.3,0.2-0.6,0.5-0.9,0.7
|
||||
l0.5,0.6l0.2,0.2c0.3-0.2,0.6-0.4,0.9-0.7L10,13.1z"/>
|
||||
<polygon class="st1" points="16,10.9 11.1,15.8 10.5,15.1 9.9,14.5 9.7,14.3 9.1,13.8 8.8,13.4 9.6,12.7 10,13.1 10.8,13.8
|
||||
10.9,13.9 11.1,14.2 15.2,10.1 15.3,10.3 "/>
|
||||
<polygon class="st2" points="16,10.9 11.1,15.8 10.5,15.1 9.9,14.5 9.7,14.3 9.1,13.8 8.8,13.4 9.6,12.7 10,13.1 10.8,13.8
|
||||
10.9,13.9 11.1,14.2 15.2,10.1 15.3,10.3 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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;}.cls-2{fill:#061c08;}.cls-3{fill:#3bb44a;}</style></defs><title>trust_inverse </title><path class="cls-1" d="M13.74,2a9.46,9.46,0,0,1-1.19-.17,8.28,8.28,0,0,1-1.14-.35A6.72,6.72,0,0,1,10.29.87,7.43,7.43,0,0,0,9.62.5,4,4,0,0,0,8.26.08,4.62,4.62,0,0,0,7.5,0,5.15,5.15,0,0,0,6,.23,5.39,5.39,0,0,0,4.71.87a6.72,6.72,0,0,1-1.12.59,8.28,8.28,0,0,1-1.14.35A9.46,9.46,0,0,1,1.26,2C.85,2,.43,2,0,2V6A7.62,7.62,0,0,0,.29,8.19a9.37,9.37,0,0,0,.78,1.94,10.69,10.69,0,0,0,1.2,1.73,14.32,14.32,0,0,0,1.49,1.53,15.82,15.82,0,0,0,1.7,1.33q.88.62,1.8,1.14L7.5,16l.24-.14c.28-.16.57-.33.85-.52l-.71-.72-.38.23q-.79-.46-1.59-1a15.89,15.89,0,0,1-1.51-1.2A14.78,14.78,0,0,1,3,11.24,9.26,9.26,0,0,1,2,9.67a8.29,8.29,0,0,1-.72-1.74A7,7,0,0,1,1,6V3a10.09,10.09,0,0,0,2.23-.37,8.08,8.08,0,0,0,2-1,4.05,4.05,0,0,1,1.06-.5A3.87,3.87,0,0,1,7.5,1a3.87,3.87,0,0,1,1.16.16,4.06,4.06,0,0,1,1.06.5,8.08,8.08,0,0,0,2,1A10.09,10.09,0,0,0,14,3V6a7,7,0,0,1-.26,1.9A8.29,8.29,0,0,1,13,9.67,9.26,9.26,0,0,1,12,11.24a10.85,10.85,0,0,1-.8.86l.71.71a12.86,12.86,0,0,0,.87-1,10.69,10.69,0,0,0,1.2-1.73,9.37,9.37,0,0,0,.78-1.94A7.62,7.62,0,0,0,15,6V2C14.57,2,14.15,2,13.74,2ZM10,13.1a9.54,9.54,0,0,1-.89.68l.54.55.16.16c.31-.21.61-.44.9-.68Z"/><polygon class="cls-2" points="16 10.94 11.15 15.79 10.49 15.13 9.85 14.49 9.69 14.33 9.15 13.78 8.81 13.44 9.59 12.66 10.04 13.1 10.75 13.81 10.85 13.91 11.15 14.21 15.22 10.15 15.33 10.26 16 10.94"/><polygon class="cls-3" points="16 10.94 11.15 15.79 10.49 15.13 9.85 14.49 9.69 14.33 9.15 13.78 8.81 13.44 9.59 12.66 10.04 13.1 10.75 13.81 10.85 13.91 11.15 14.21 15.22 10.15 15.33 10.26 16 10.94"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><title>add_16x16</title><path d="M6,1a5.17,5.17,0,0,1,1.4.19,5.26,5.26,0,0,1,3.67,3.67,5.3,5.3,0,0,1,0,2.79A5.26,5.26,0,0,1,7.4,11.32a5.3,5.3,0,0,1-2.79,0A5.26,5.26,0,0,1,.94,7.65a5.3,5.3,0,0,1,0-2.79A5.26,5.26,0,0,1,4.6,1.19,5.17,5.17,0,0,1,6,1Zm0,9.75a4.4,4.4,0,0,0,1.2-.16,4.56,4.56,0,0,0,1.08-.45A4.5,4.5,0,0,0,9.88,8.53a4.56,4.56,0,0,0,.45-1.08,4.51,4.51,0,0,0,0-2.39A4.56,4.56,0,0,0,9.88,4,4.5,4.5,0,0,0,8.27,2.37a4.56,4.56,0,0,0-1.08-.45,4.51,4.51,0,0,0-2.39,0,4.56,4.56,0,0,0-1.08.45A4.5,4.5,0,0,0,2.11,4a4.56,4.56,0,0,0-.45,1.08,4.51,4.51,0,0,0,0,2.39,4.56,4.56,0,0,0,.45,1.08,4.5,4.5,0,0,0,1.61,1.61,4.56,4.56,0,0,0,1.08.45A4.4,4.4,0,0,0,6,10.76Zm.38-4.88H8.25v.75H6.37V8.51H5.62V6.63H3.75V5.88H5.62V4h.75Z"/></svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#015CDA;}
|
||||
</style>
|
||||
<title>add_16x16</title>
|
||||
<path class="st0" d="M8,0c0.7,0,1.4,0.1,2.1,0.3C12.8,1,15,3.2,15.7,5.9c0.4,1.4,0.4,2.9,0,4.2c-0.8,2.7-2.9,4.8-5.6,5.6
|
||||
c-1.4,0.4-2.9,0.4-4.2,0C3.2,15,1,12.8,0.3,10.1c-0.4-1.4-0.4-2.9,0-4.2C1,3.2,3.1,1,5.9,0.3C6.6,0.1,7.3,0,8,0z M7.9,14.9
|
||||
c0.6,0,1.2-0.1,1.8-0.2c0.6-0.2,1.1-0.4,1.6-0.7c1-0.6,1.8-1.4,2.4-2.4c0.3-0.5,0.5-1.1,0.7-1.6c0.3-1.2,0.3-2.4,0-3.6
|
||||
c-0.2-0.6-0.4-1.1-0.7-1.6c-0.6-1-1.4-1.9-2.4-2.4C10.8,2,10.2,1.8,9.7,1.6c-1.2-0.3-2.4-0.3-3.6,0C5.5,1.8,5,2,4.4,2.3
|
||||
C3.4,2.9,2.6,3.7,2,4.7C1.7,5.3,1.5,5.8,1.4,6.4C1,7.5,1,8.8,1.4,9.9c0.2,0.6,0.4,1.1,0.7,1.6c0.6,1,1.4,1.8,2.4,2.4
|
||||
c0.5,0.3,1.1,0.5,1.6,0.7C6.7,14.8,7.3,14.9,7.9,14.9L7.9,14.9z M8.6,7.5h3.6V9H8.6v3.6H7.1V9H3.5V7.5h3.6V3.9h1.4L8.6,7.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7C1.27083 7 1.52865 6.94792 1.77344 6.84375C2.01823 6.73438 2.22917 6.59115 2.40625 6.41406C2.58854 6.23177 2.73177 6.02083 2.83594 5.78125C2.94531 5.53646 3 5.27604 3 5C3 4.63021 2.99219 4.26042 2.97656 3.89062C2.96094 3.52083 2.97135 3.16146 3.00781 2.8125C3.04948 2.45833 3.13281 2.11719 3.25781 1.78906C3.38802 1.46094 3.59375 1.15625 3.875 0.875C4.15625 0.59375 4.48177 0.377604 4.85156 0.226562C5.22135 0.0755208 5.60417 0 6 0V1C5.72396 1 5.46354 1.05469 5.21875 1.16406C4.97917 1.26823 4.76823 1.41146 4.58594 1.59375C4.40885 1.77083 4.26562 1.98177 4.15625 2.22656C4.05208 2.47135 4 2.72917 4 3C4 3.29167 4.00521 3.58073 4.01562 3.86719C4.02604 4.14844 4.02604 4.42708 4.01562 4.70312C4.01042 4.97396 3.98698 5.23958 3.94531 5.5C3.90365 5.75521 3.83333 6 3.73438 6.23438C3.63542 6.46875 3.5 6.69271 3.32812 6.90625C3.15625 7.11979 2.9349 7.31771 2.66406 7.5C2.9349 7.68229 3.15625 7.88021 3.32812 8.09375C3.5 8.30729 3.63542 8.53125 3.73438 8.76562C3.83333 9 3.90365 9.2474 3.94531 9.50781C3.98698 9.76302 4.01042 10.0286 4.01562 10.3047C4.02604 10.5755 4.02604 10.8542 4.01562 11.1406C4.00521 11.4219 4 11.7083 4 12C4 12.276 4.05208 12.5365 4.15625 12.7812C4.26562 13.0208 4.40885 13.2318 4.58594 13.4141C4.76823 13.5911 4.97917 13.7344 5.21875 13.8438C5.46354 13.9479 5.72396 14 6 14V15C5.60417 15 5.22135 14.9245 4.85156 14.7734C4.48177 14.6224 4.15625 14.4062 3.875 14.125C3.59375 13.8438 3.38802 13.5391 3.25781 13.2109C3.13281 12.8828 3.04948 12.5443 3.00781 12.1953C2.97135 11.8411 2.96094 11.4792 2.97656 11.1094C2.99219 10.7396 3 10.3698 3 10C3 9.72917 2.94531 9.47135 2.83594 9.22656C2.73177 8.98177 2.58854 8.77083 2.40625 8.59375C2.22917 8.41146 2.01823 8.26823 1.77344 8.16406C1.52865 8.05469 1.27083 8 1 8V7ZM10 0C10.3958 0 10.7786 0.0755208 11.1484 0.226562C11.5182 0.377604 11.8438 0.59375 12.125 0.875C12.4062 1.15625 12.6094 1.46094 12.7344 1.78906C12.8646 2.11719 12.9479 2.45833 12.9844 2.8125C13.026 3.16146 13.0391 3.52083 13.0234 3.89062C13.0078 4.26042 13 4.63021 13 5C13 5.27604 13.0521 5.53646 13.1562 5.78125C13.2656 6.02083 13.4089 6.23177 13.5859 6.41406C13.7682 6.59115 13.9792 6.73438 14.2188 6.84375C14.4635 6.94792 14.724 7 15 7V8C14.724 8 14.4635 8.05469 14.2188 8.16406C13.9792 8.26823 13.7682 8.41146 13.5859 8.59375C13.4089 8.77083 13.2656 8.98177 13.1562 9.22656C13.0521 9.47135 13 9.72917 13 10C13 10.3698 13.0078 10.7396 13.0234 11.1094C13.0391 11.4792 13.026 11.8411 12.9844 12.1953C12.9479 12.5443 12.8646 12.8828 12.7344 13.2109C12.6094 13.5391 12.4062 13.8438 12.125 14.125C11.8438 14.4062 11.5182 14.6224 11.1484 14.7734C10.7786 14.9245 10.3958 15 10 15V14C10.2708 14 10.5286 13.9479 10.7734 13.8438C11.0182 13.7344 11.2292 13.5911 11.4062 13.4141C11.5885 13.2318 11.7318 13.0208 11.8359 12.7812C11.9453 12.5365 12 12.276 12 12C12 11.7083 11.9948 11.4219 11.9844 11.1406C11.974 10.8542 11.9714 10.5755 11.9766 10.3047C11.987 10.0286 12.013 9.76302 12.0547 9.50781C12.0964 9.2474 12.1667 9 12.2656 8.76562C12.3646 8.53125 12.5 8.30729 12.6719 8.09375C12.8438 7.88021 13.0651 7.68229 13.3359 7.5C13.0651 7.31771 12.8438 7.11979 12.6719 6.90625C12.5 6.69271 12.3646 6.46875 12.2656 6.23438C12.1667 6 12.0964 5.75521 12.0547 5.5C12.013 5.23958 11.987 4.97396 11.9766 4.70312C11.9714 4.42708 11.974 4.14844 11.9844 3.86719C11.9948 3.58073 12 3.29167 12 3C12 2.72917 11.9453 2.47135 11.8359 2.22656C11.7318 1.98177 11.5885 1.77083 11.4062 1.59375C11.2292 1.41146 11.0182 1.26823 10.7734 1.16406C10.5286 1.05469 10.2708 1 10 1V0Z" fill="#333333"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 3.03125V14.9375H2.09375V11.9375H3V14.0312H15.0938V3.9375H9V3.03125H16ZM2.92188 9.9375H0V9.59375C0.0989583 9.57812 0.197917 9.5651 0.296875 9.55469C0.395833 9.53906 0.492188 9.51823 0.585938 9.49219C0.658854 9.47135 0.736979 9.40365 0.820312 9.28906C0.908854 9.17448 0.992188 9.04688 1.07031 8.90625C1.14844 8.76562 1.21875 8.6276 1.28125 8.49219C1.34896 8.35156 1.39844 8.24479 1.42969 8.17188L4.46094 1H5.10156L8.08594 8.25C8.13281 8.36458 8.1875 8.49479 8.25 8.64062C8.31771 8.78646 8.39323 8.92708 8.47656 9.0625C8.5651 9.19271 8.66406 9.30729 8.77344 9.40625C8.88802 9.5 9.01562 9.55469 9.15625 9.57031C9.30208 9.58594 9.44792 9.59375 9.59375 9.59375V9.9375H6.10938V9.59375C6.19792 9.59375 6.29427 9.59635 6.39844 9.60156C6.5026 9.60156 6.59896 9.59115 6.6875 9.57031C6.78125 9.54427 6.85938 9.5026 6.92188 9.44531C6.98438 9.38281 7.01562 9.28646 7.01562 9.15625C7.01562 9.04167 6.98698 8.88542 6.92969 8.6875C6.8724 8.48438 6.80469 8.27604 6.72656 8.0625C6.64844 7.84896 6.56771 7.64323 6.48438 7.44531C6.40625 7.24219 6.34115 7.08073 6.28906 6.96094H2.80469C2.75781 7.08073 2.6875 7.24219 2.59375 7.44531C2.50521 7.64323 2.41667 7.84896 2.32812 8.0625C2.24479 8.27604 2.16927 8.48177 2.10156 8.67969C2.03906 8.8776 2.00781 9.03385 2.00781 9.14844C2.00781 9.27344 2.03906 9.36719 2.10156 9.42969C2.16406 9.48698 2.24219 9.52604 2.33594 9.54688C2.42969 9.56771 2.52865 9.58073 2.63281 9.58594C2.74219 9.58594 2.83854 9.58854 2.92188 9.59375V9.9375ZM3.14062 5.96094H5.82812L4.45312 3.00781L3.14062 5.96094Z" fill="#333333"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{clip-path:url(#clip-path-3);}.cls-5{clip-path:url(#clip-path-4);}.cls-6{clip-path:url(#clip-path-5);}.cls-7{clip-path:url(#clip-path-6);}.cls-8{clip-path:url(#clip-path-7);}.cls-9{clip-path:url(#clip-path-8);}</style><clipPath id="clip-path"><rect class="cls-1" x="81.22" y="51.49" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-2"><rect class="cls-1" x="-712.01" y="419.77" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-3"><rect class="cls-1" x="-711.01" y="419.77" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-4"><rect class="cls-1" x="-708.37" y="426.05" width="10.73" height="0.43"/></clipPath><clipPath id="clip-path-5"><rect class="cls-1" x="-708.07" y="424.81" width="10.14" height="0.43"/></clipPath><clipPath id="clip-path-6"><rect class="cls-1" x="-707.66" y="423.57" width="9.31" height="0.43"/></clipPath><clipPath id="clip-path-7"><rect class="cls-1" x="-707.25" y="422.33" width="8.5" height="0.43"/></clipPath><clipPath id="clip-path-8"><rect class="cls-1" x="-706.83" y="421.09" width="7.65" height="0.43"/></clipPath></defs><title>clear_results</title><path d="M8.71,13.84H12v1H3.54L.39,11.68a1.29,1.29,0,0,1-.29-.44,1.42,1.42,0,0,1,0-1.05,1.34,1.34,0,0,1,.3-.45L9.75.38,16,6.59Zm-1.42,0,1-1L3.5,8l-2.39,2.4a.4.4,0,0,0,0,.55L4,13.84Zm2.46-12L4.2,7.34,9,12.13l5.54-5.54Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#015CDA;}
|
||||
</style>
|
||||
<title>clear_results</title>
|
||||
<path class="st0" d="M8.7,13.8H12v1H3.5l-3.2-3.2c-0.1-0.1-0.2-0.3-0.3-0.4c-0.1-0.3-0.1-0.7,0-1.1C0.2,10,0.3,9.9,0.4,9.7l9.4-9.4
|
||||
L16,6.6L8.7,13.8z M7.3,13.8l1-1L3.5,8l-2.4,2.4c-0.1,0.2-0.1,0.4,0,0.6L4,13.8H7.3z M9.8,1.8L4.2,7.3L9,12.1l5.5-5.5L9.8,1.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<title>opac_command_icons_bv</title>
|
||||
<path d="M0,1H16V2H0ZM.148,6.148,2.5,3.8,4.852,6.148l-.7.7L3,5.711V15H2V5.711L.852,6.852ZM7,5V4h9V5Z"/>
|
||||
<rect width="16" height="16" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1 @@
|
||||
<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:#388a34;}</style></defs><title>new_notebook</title><path d="M11.86,1.24V.33H9.13A3.78,3.78,0,0,0,7.91.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.66v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.73Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17h1.82v9.12ZM12.86,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-1" points="15.99 12.19 15.99 13.13 13.79 13.13 13.79 15.33 12.87 15.33 12.87 13.13 10.66 13.13 10.66 12.19 12.87 12.19 12.87 9.99 13.79 9.99 13.79 12.19 15.99 12.19"/><path class="cls-1" d="M13.79,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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;}</style></defs><title>nontrust</title><path d="M13.74,2a9.49,9.49,0,0,1-1.19-.17,7.1,7.1,0,0,1-1.14-.35A6.72,6.72,0,0,1,10.29.84,7.43,7.43,0,0,0,9.62.47,5.31,5.31,0,0,0,9,.21,4.18,4.18,0,0,0,8.26,0,6.07,6.07,0,0,0,7.5,0,4.81,4.81,0,0,0,6,.21,5.34,5.34,0,0,0,4.71.84a6.72,6.72,0,0,1-1.12.59,7.09,7.09,0,0,1-1.14.35A9.48,9.48,0,0,1,1.26,2C.85,2,.43,2,0,2V6A7.62,7.62,0,0,0,.29,8.16a9.15,9.15,0,0,0,.78,1.94,10.78,10.78,0,0,0,1.2,1.74,13.35,13.35,0,0,0,1.49,1.52,15.81,15.81,0,0,0,1.7,1.33c.59.42,1.19.8,1.8,1.15L7.5,16l.24-.14c.61-.35,1.21-.73,1.8-1.15a12.71,12.71,0,0,0,1.24-1l-.7-.7c-.32.27-.65.52-1,.76q-.8.55-1.59,1-.79-.46-1.59-1A15.89,15.89,0,0,1,4.4,12.6,13.66,13.66,0,0,1,3,11.21,9.59,9.59,0,0,1,2,9.65a8.52,8.52,0,0,1-.72-1.74A7.1,7.1,0,0,1,1,6V3a9.54,9.54,0,0,0,2.23-.37,8.08,8.08,0,0,0,2-.95,4.4,4.4,0,0,1,1.06-.5A3.87,3.87,0,0,1,7.5,1a3.87,3.87,0,0,1,1.16.16,4.4,4.4,0,0,1,1.06.5,8.08,8.08,0,0,0,2,.95A9.54,9.54,0,0,0,14,3V6a7.1,7.1,0,0,1-.26,1.91A8.51,8.51,0,0,1,13,9.65l-.1.18.73.73a3.14,3.14,0,0,0,.28-.46,9.15,9.15,0,0,0,.78-1.94A7.62,7.62,0,0,0,15,6V2C14.57,2,14.15,2,13.74,2Z"/><polygon class="cls-1" points="13.82 13.24 16 15.41 15.43 15.98 13.26 13.8 11.07 15.98 10.5 15.41 12.69 13.24 10.46 11.01 11.03 10.44 13.26 12.67 15.43 10.49 16 11.05 13.82 13.24"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{fill:#015cda;}.cls-3{fill:#d02e00;}.cls-4{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><rect class="cls-1" x="-777.5" y="-93.32" width="15.81" height="13.48"/></clipPath></defs><title>Not_trusted_command</title><path class="cls-2" d="M13.74,2.25a9.47,9.47,0,0,1-1.19-.17,6.53,6.53,0,0,1-1.14-.35,6.23,6.23,0,0,1-1.12-.64C10.07,1,9.85.83,9.62.72S9.21.54,9,.46A4.79,4.79,0,0,0,8.26.25c-.25,0-.51,0-.76,0A4.77,4.77,0,0,0,6,.46a5.51,5.51,0,0,0-1.29.63,6.64,6.64,0,0,1-1.12.59A6.53,6.53,0,0,1,2.45,2a9.58,9.58,0,0,1-1.19.22H0v4A7.5,7.5,0,0,0,.29,8.41a9.37,9.37,0,0,0,.78,1.94,10.35,10.35,0,0,0,1.2,1.74,12.59,12.59,0,0,0,1.49,1.52,15.55,15.55,0,0,0,1.7,1.33c.59.42,1.19.8,1.8,1.15l.24.16.24-.14c.61-.35,1.21-.73,1.8-1.15a11.79,11.79,0,0,0,1.24-1l-.7-.7a12.52,12.52,0,0,1-1,.76c-.53.37-1.06.7-1.59,1-.53-.3-1.06-.64-1.59-1a16.79,16.79,0,0,1-1.5-1.17A13.12,13.12,0,0,1,3,11.46,9.41,9.41,0,0,1,2,9.9a8.86,8.86,0,0,1-.72-1.74A7.23,7.23,0,0,1,1,6.25v-3a9.37,9.37,0,0,0,2.23-.37,7.81,7.81,0,0,0,2-.95,4.67,4.67,0,0,1,1.06-.5,4.22,4.22,0,0,1,2.37,0,4.67,4.67,0,0,1,1.06.5,7.81,7.81,0,0,0,2,1A9.31,9.31,0,0,0,14,3.25v3a7.21,7.21,0,0,1-.26,1.91A8.35,8.35,0,0,1,13,9.9l-.1.18.73.73a2.69,2.69,0,0,0,.28-.46,8.85,8.85,0,0,0,.78-1.94A7.52,7.52,0,0,0,15,6.25v-4Z"/><polygon class="cls-3" points="13.82 13.49 16 15.66 15.43 16.23 13.26 14.05 11.07 16.23 10.5 15.66 12.69 13.49 10.46 11.26 11.03 10.69 13.26 12.92 15.43 10.74 16 11.3 13.82 13.49"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><rect class="cls-1" x="-579.3" y="-7.09" width="15.81" height="13.48"/></clipPath></defs><title>run_cells</title><path d="M8,15.92a8,8,0,1,1,8-8A8,8,0,0,1,8,15.92ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/><polygon points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>
|
||||
|
After Width: | Height: | Size: 494 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:#015cda;}</style></defs><title>run_cells_command</title><path class="cls-1" d="M8,15.92a8,8,0,1,1,8-8A8,8,0,0,1,8,15.92ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/><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: 349 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<title>opac_command_icons_bv</title>
|
||||
<path d="M0,1H16V2H0ZM3,13.289l1.148-1.141.7.7L2.5,15.2.148,12.852l.7-.7L2,13.289V4H3ZM7,5V4h9V5ZM7,8V7h9V8Zm0,3V10h9v1Zm0,3V13h9v1Z"/>
|
||||
<rect width="16" height="16" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -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>stop_cell</title><circle cx="8" cy="7.92" r="7.76"/><rect class="cls-1" x="5" y="4.92" width="6" height="6"/></svg>
|
||||
|
After Width: | Height: | Size: 261 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:#061c08;}.cls-2{fill:#3bb44a;}</style></defs><title>trust</title><path d="M13.74,2a9.46,9.46,0,0,1-1.19-.17,8.28,8.28,0,0,1-1.14-.35A6.72,6.72,0,0,1,10.29.85,7.43,7.43,0,0,0,9.62.48,4,4,0,0,0,8.26.07,4.62,4.62,0,0,0,7.5,0,5.15,5.15,0,0,0,6,.21,5.39,5.39,0,0,0,4.71.85a6.72,6.72,0,0,1-1.12.59,8.28,8.28,0,0,1-1.14.35A9.46,9.46,0,0,1,1.26,2C.85,2,.43,2,0,2V6A7.62,7.62,0,0,0,.29,8.17a9.37,9.37,0,0,0,.78,1.94,10.69,10.69,0,0,0,1.2,1.73,14.32,14.32,0,0,0,1.49,1.53,15.82,15.82,0,0,0,1.7,1.33q.88.62,1.8,1.14L7.5,16l.24-.14c.28-.16.57-.33.85-.52l-.71-.72-.38.23q-.79-.46-1.59-1a15.89,15.89,0,0,1-1.51-1.2A14.78,14.78,0,0,1,3,11.22,9.26,9.26,0,0,1,2,9.65a8.29,8.29,0,0,1-.72-1.74A7,7,0,0,1,1,6V3a10.09,10.09,0,0,0,2.23-.37,8.08,8.08,0,0,0,2-1,4.05,4.05,0,0,1,1.06-.5A3.87,3.87,0,0,1,7.5,1a3.87,3.87,0,0,1,1.16.16,4.06,4.06,0,0,1,1.06.5,8.08,8.08,0,0,0,2,1A10.09,10.09,0,0,0,14,3V6a7,7,0,0,1-.26,1.9A8.29,8.29,0,0,1,13,9.65,9.26,9.26,0,0,1,12,11.22a10.85,10.85,0,0,1-.8.86l.71.71a12.86,12.86,0,0,0,.87-1,10.69,10.69,0,0,0,1.2-1.73,9.37,9.37,0,0,0,.78-1.94A7.62,7.62,0,0,0,15,6V2C14.57,2,14.15,2,13.74,2ZM10,13.08a9.54,9.54,0,0,1-.89.68l.54.55.16.16c.31-.21.61-.44.9-.68Z"/><polygon class="cls-1" points="16 10.93 11.15 15.78 10.49 15.12 9.85 14.47 9.69 14.31 9.15 13.77 8.81 13.43 9.59 12.64 10.04 13.09 10.75 13.79 10.85 13.89 11.15 14.2 15.22 10.13 15.33 10.24 16 10.93"/><polygon class="cls-2" points="16 10.93 11.15 15.78 10.49 15.12 9.85 14.47 9.69 14.31 9.15 13.77 8.81 13.43 9.59 12.64 10.04 13.09 10.75 13.79 10.85 13.89 11.15 14.2 15.22 10.13 15.33 10.24 16 10.93"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#015CDA;}
|
||||
.st1{fill:#061C08;}
|
||||
.st2{fill:#3BB44A;}
|
||||
</style>
|
||||
<title>trust</title>
|
||||
<path class="st0" d="M13.7,2c-0.4,0-0.8-0.1-1.2-0.2c-0.4-0.1-0.8-0.2-1.1-0.4c-0.4-0.2-0.8-0.4-1.1-0.6c-0.2-0.1-0.4-0.3-0.7-0.4
|
||||
C9.2,0.3,8.7,0.1,8.3,0.1C8,0,7.8,0,7.5,0C7,0,6.5,0.1,6,0.2C5.5,0.4,5.1,0.6,4.7,0.9C4.4,1.1,4,1.3,3.6,1.4
|
||||
C3.2,1.6,2.8,1.7,2.5,1.8C2.1,1.9,1.7,2,1.3,2C0.9,2,0.4,2,0,2v4c0,0.7,0.1,1.5,0.3,2.2c0.2,0.7,0.4,1.3,0.8,1.9
|
||||
c0.3,0.6,0.7,1.2,1.2,1.7c0.5,0.5,1,1.1,1.5,1.5c0.5,0.5,1.1,0.9,1.7,1.3c0.6,0.4,1.2,0.8,1.8,1.1L7.5,16l0.2-0.1
|
||||
c0.3-0.2,0.6-0.3,0.9-0.5l-0.7-0.7l-0.4,0.2c-0.5-0.3-1.1-0.6-1.6-1c-0.5-0.4-1-0.8-1.5-1.2c-0.5-0.4-1-0.9-1.4-1.4
|
||||
c-0.4-0.5-0.7-1-1-1.6C1.7,9.1,1.5,8.5,1.3,7.9C1.1,7.3,1,6.6,1,6V3c0.8,0,1.5-0.2,2.2-0.4c0.7-0.2,1.4-0.6,2-1
|
||||
c0.3-0.2,0.7-0.4,1.1-0.5C6.7,1,7.1,1,7.5,1c0.4,0,0.8,0,1.2,0.2C9,1.3,9.4,1.4,9.7,1.7c0.6,0.4,1.3,0.8,2,1C12.5,2.9,13.2,3,14,3v3
|
||||
c0,0.6-0.1,1.3-0.3,1.9c-0.2,0.6-0.4,1.2-0.7,1.7c-0.3,0.6-0.6,1.1-1,1.6c-0.3,0.3-0.5,0.6-0.8,0.9l0.7,0.7c0.3-0.3,0.6-0.7,0.9-1
|
||||
c0.5-0.5,0.9-1.1,1.2-1.7c0.3-0.6,0.6-1.3,0.8-1.9C14.9,7.4,15,6.7,15,6V2C14.6,2,14.1,2,13.7,2z M10,13.1c-0.3,0.2-0.6,0.5-0.9,0.7
|
||||
l0.5,0.6l0.2,0.2c0.3-0.2,0.6-0.4,0.9-0.7L10,13.1z"/>
|
||||
<polygon class="st1" points="16,10.9 11.1,15.8 10.5,15.1 9.9,14.5 9.7,14.3 9.1,13.8 8.8,13.4 9.6,12.6 10,13.1 10.8,13.8
|
||||
10.9,13.9 11.1,14.2 15.2,10.1 15.3,10.2 "/>
|
||||
<polygon class="st2" points="16,10.9 11.1,15.8 10.5,15.1 9.9,14.5 9.7,14.3 9.1,13.8 8.8,13.4 9.6,12.6 10,13.1 10.8,13.8
|
||||
10.9,13.9 11.1,14.2 15.2,10.1 15.3,10.2 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
705
src/sql/workbench/contrib/notebook/browser/models/cell.ts
Normal file
@@ -0,0 +1,705 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb, ServerInfo } from 'azdata';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import * as notebookUtils from 'sql/workbench/contrib/notebook/browser/models/notebookUtils';
|
||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { ICellModel, notebookConstants, IOutputChangedEvent, FutureInternal, CellExecutionState, ICellModelOptions } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { optional } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
import { firstIndex, find } from 'vs/base/common/arrays';
|
||||
let modelId = 0;
|
||||
|
||||
export const HideInputTag = 'hide_input';
|
||||
|
||||
export class CellModel implements ICellModel {
|
||||
public id: string;
|
||||
|
||||
private _cellType: nb.CellType;
|
||||
private _source: string | string[];
|
||||
private _language: string;
|
||||
private _cellGuid: string;
|
||||
private _future: FutureInternal;
|
||||
private _outputs: nb.ICellOutput[] = [];
|
||||
private _isEditMode: boolean;
|
||||
private _onOutputsChanged = new Emitter<IOutputChangedEvent>();
|
||||
private _onCellModeChanged = new Emitter<boolean>();
|
||||
private _onExecutionStateChanged = new Emitter<CellExecutionState>();
|
||||
private _isTrusted: boolean;
|
||||
private _active: boolean;
|
||||
private _hover: boolean;
|
||||
private _executionCount: number | undefined;
|
||||
private _cellUri: URI;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _stdInHandler: nb.MessageHandler<nb.IStdinMessage>;
|
||||
private _onCellLoaded = new Emitter<string>();
|
||||
private _loaded: boolean;
|
||||
private _stdInVisible: boolean;
|
||||
private _metadata: { language?: string; tags?: string[]; cellGuid?: string; };
|
||||
private _isCollapsed: boolean;
|
||||
private _onCollapseStateChanged = new Emitter<boolean>();
|
||||
private _modelContentChangedEvent: IModelContentChangedEvent;
|
||||
|
||||
constructor(cellData: nb.ICellContents,
|
||||
private _options: ICellModelOptions,
|
||||
@optional(INotebookService) private _notebookService?: INotebookService
|
||||
) {
|
||||
this.id = `${modelId++}`;
|
||||
if (cellData) {
|
||||
// Read in contents if available
|
||||
this.fromJSON(cellData);
|
||||
} else {
|
||||
this._cellType = CellTypes.Code;
|
||||
this._source = '';
|
||||
}
|
||||
this._isEditMode = this._cellType !== CellTypes.Markdown;
|
||||
this._stdInVisible = false;
|
||||
if (_options && _options.isTrusted) {
|
||||
this._isTrusted = true;
|
||||
} else {
|
||||
this._isTrusted = false;
|
||||
}
|
||||
// if the fromJson() method was already called and _cellGuid was previously set, don't generate another UUID unnecessarily
|
||||
this._cellGuid = this._cellGuid || generateUuid();
|
||||
this.createUri();
|
||||
}
|
||||
|
||||
public equals(other: ICellModel) {
|
||||
return other && other.id === this.id;
|
||||
}
|
||||
|
||||
public get onCollapseStateChanged(): Event<boolean> {
|
||||
return this._onCollapseStateChanged.event;
|
||||
}
|
||||
|
||||
public get onOutputsChanged(): Event<IOutputChangedEvent> {
|
||||
return this._onOutputsChanged.event;
|
||||
}
|
||||
|
||||
public get onCellModeChanged(): Event<boolean> {
|
||||
return this._onCellModeChanged.event;
|
||||
}
|
||||
|
||||
public get isEditMode(): boolean {
|
||||
return this._isEditMode;
|
||||
}
|
||||
|
||||
public get future(): FutureInternal {
|
||||
return this._future;
|
||||
}
|
||||
|
||||
public get isCollapsed() {
|
||||
return this._isCollapsed;
|
||||
}
|
||||
|
||||
public set isCollapsed(value: boolean) {
|
||||
if (this.cellType !== CellTypes.Code) {
|
||||
return;
|
||||
}
|
||||
let stateChanged = this._isCollapsed !== value;
|
||||
this._isCollapsed = value;
|
||||
|
||||
let tagIndex = -1;
|
||||
if (this._metadata.tags) {
|
||||
tagIndex = firstIndex(this._metadata.tags, tag => tag === HideInputTag);
|
||||
}
|
||||
|
||||
if (this._isCollapsed) {
|
||||
if (tagIndex === -1) {
|
||||
if (!this._metadata.tags) {
|
||||
this._metadata.tags = [];
|
||||
}
|
||||
this._metadata.tags.push(HideInputTag);
|
||||
}
|
||||
} else {
|
||||
if (tagIndex > -1) {
|
||||
this._metadata.tags.splice(tagIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
this._onCollapseStateChanged.fire(this._isCollapsed);
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellInputVisibilityChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public set isEditMode(isEditMode: boolean) {
|
||||
this._isEditMode = isEditMode;
|
||||
this._onCellModeChanged.fire(this._isEditMode);
|
||||
// Note: this does not require a notebook update as it does not change overall state
|
||||
}
|
||||
|
||||
public get trustedMode(): boolean {
|
||||
return this._isTrusted;
|
||||
}
|
||||
|
||||
public set trustedMode(isTrusted: boolean) {
|
||||
if (this._isTrusted !== isTrusted) {
|
||||
this._isTrusted = isTrusted;
|
||||
let outputEvent: IOutputChangedEvent = {
|
||||
outputs: this._outputs,
|
||||
shouldScroll: false
|
||||
};
|
||||
this._onOutputsChanged.fire(outputEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public get active(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
public set active(value: boolean) {
|
||||
this._active = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get hover(): boolean {
|
||||
return this._hover;
|
||||
}
|
||||
|
||||
public set hover(value: boolean) {
|
||||
this._hover = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get executionCount(): number | undefined {
|
||||
return this._executionCount;
|
||||
}
|
||||
|
||||
public set executionCount(value: number | undefined) {
|
||||
this._executionCount = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get cellUri(): URI {
|
||||
return this._cellUri;
|
||||
}
|
||||
|
||||
public get notebookModel(): NotebookModel {
|
||||
return <NotebookModel>this.options.notebook;
|
||||
}
|
||||
|
||||
public set cellUri(value: URI) {
|
||||
this._cellUri = value;
|
||||
}
|
||||
|
||||
public get options(): ICellModelOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
public get cellType(): CellType {
|
||||
return this._cellType;
|
||||
}
|
||||
|
||||
public get source(): string | string[] {
|
||||
return this._source;
|
||||
}
|
||||
|
||||
public set source(newSource: string | string[]) {
|
||||
newSource = this.getMultilineSource(newSource);
|
||||
if (this._source !== newSource) {
|
||||
this._source = newSource;
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
|
||||
}
|
||||
this._modelContentChangedEvent = undefined;
|
||||
}
|
||||
|
||||
public get modelContentChangedEvent(): IModelContentChangedEvent {
|
||||
return this._modelContentChangedEvent;
|
||||
}
|
||||
|
||||
public set modelContentChangedEvent(e: IModelContentChangedEvent) {
|
||||
this._modelContentChangedEvent = e;
|
||||
}
|
||||
|
||||
public get language(): string {
|
||||
if (this._cellType === CellTypes.Markdown) {
|
||||
return 'markdown';
|
||||
}
|
||||
if (this._language) {
|
||||
return this._language;
|
||||
}
|
||||
return this.options.notebook.language;
|
||||
}
|
||||
|
||||
public get cellGuid(): string {
|
||||
return this._cellGuid;
|
||||
}
|
||||
|
||||
public setOverrideLanguage(newLanguage: string) {
|
||||
this._language = newLanguage;
|
||||
}
|
||||
|
||||
public get onExecutionStateChange(): Event<CellExecutionState> {
|
||||
return this._onExecutionStateChanged.event;
|
||||
}
|
||||
|
||||
private fireExecutionStateChanged(): void {
|
||||
this._onExecutionStateChanged.fire(this.executionState);
|
||||
}
|
||||
|
||||
public get onLoaded(): Event<string> {
|
||||
return this._onCellLoaded.event;
|
||||
}
|
||||
|
||||
public get loaded(): boolean {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
public set loaded(val: boolean) {
|
||||
this._loaded = val;
|
||||
if (val) {
|
||||
this._onCellLoaded.fire(this._cellType);
|
||||
}
|
||||
}
|
||||
|
||||
public get stdInVisible(): boolean {
|
||||
return this._stdInVisible;
|
||||
}
|
||||
|
||||
public set stdInVisible(val: boolean) {
|
||||
this._stdInVisible = val;
|
||||
}
|
||||
|
||||
private notifyExecutionComplete(): void {
|
||||
if (this._notebookService) {
|
||||
this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted, this);
|
||||
}
|
||||
}
|
||||
|
||||
public get executionState(): CellExecutionState {
|
||||
let isRunning = !!(this._future && this._future.inProgress);
|
||||
if (isRunning) {
|
||||
return CellExecutionState.Running;
|
||||
} else if (this.active || this.hover) {
|
||||
return CellExecutionState.Stopped;
|
||||
}
|
||||
// TODO save error state and show the error
|
||||
return CellExecutionState.Hidden;
|
||||
}
|
||||
|
||||
public async runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean> {
|
||||
try {
|
||||
if (!this.active && this !== this.notebookModel.activeCell) {
|
||||
this.notebookModel.updateActiveCell(this);
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
if (connectionManagementService) {
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
}
|
||||
if (this.cellType !== CellTypes.Code) {
|
||||
// TODO should change hidden state to false if we add support
|
||||
// for this property
|
||||
return false;
|
||||
}
|
||||
let kernel = await this.getOrStartKernel(notificationService);
|
||||
if (!kernel) {
|
||||
return false;
|
||||
}
|
||||
// If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt()
|
||||
// This matches the same behavior as JupyterLab
|
||||
if (this.future && this.future.inProgress) {
|
||||
// If stdIn is visible, to prevent a kernel hang, we need to send a dummy input reply
|
||||
if (this._stdInVisible && this._stdInHandler) {
|
||||
this.future.sendInputReply({ value: '' });
|
||||
}
|
||||
this.future.inProgress = false;
|
||||
await kernel.interrupt();
|
||||
this.sendNotification(notificationService, Severity.Info, localize('runCellCancelled', "Cell execution cancelled"));
|
||||
} else {
|
||||
// TODO update source based on editor component contents
|
||||
if (kernel.requiresConnection && !this.notebookModel.activeConnection) {
|
||||
let connected = await this.notebookModel.requestConnection();
|
||||
if (!connected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let content = this.source;
|
||||
if ((Array.isArray(content) && content.length > 0) || (!Array.isArray(content) && content)) {
|
||||
// requestExecute expects a string for the code parameter
|
||||
content = Array.isArray(content) ? content.join('') : content;
|
||||
const future = kernel.requestExecute({
|
||||
code: content,
|
||||
stop_on_error: true
|
||||
}, false);
|
||||
this.setFuture(future as FutureInternal);
|
||||
this.fireExecutionStateChanged();
|
||||
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
|
||||
let result: nb.IExecuteReplyMsg = <nb.IExecuteReplyMsg><any>await future.done;
|
||||
if (result && result.content) {
|
||||
this.executionCount = result.content.execution_count;
|
||||
if (result.content.status !== 'ok') {
|
||||
// TODO track error state
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message: string;
|
||||
if (error.message === 'Canceled') {
|
||||
message = localize('executionCanceled', "Query execution was canceled");
|
||||
} else {
|
||||
message = getErrorMessage(error);
|
||||
}
|
||||
this.sendNotification(notificationService, Severity.Error, message);
|
||||
// TODO track error state for the cell
|
||||
} finally {
|
||||
this.disposeFuture();
|
||||
this.fireExecutionStateChanged();
|
||||
this.notifyExecutionComplete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getOrStartKernel(notificationService: INotificationService): Promise<nb.IKernel> {
|
||||
let model = this.options.notebook;
|
||||
let clientSession = model && model.clientSession;
|
||||
if (!clientSession) {
|
||||
this.sendNotification(notificationService, Severity.Error, localize('notebookNotReady', "The session for this notebook is not yet ready"));
|
||||
return undefined;
|
||||
} else if (!clientSession.isReady || clientSession.status === 'dead') {
|
||||
|
||||
this.sendNotification(notificationService, Severity.Info, localize('sessionNotReady', "The session for this notebook will start momentarily"));
|
||||
await clientSession.kernelChangeCompleted;
|
||||
}
|
||||
if (!clientSession.kernel) {
|
||||
let defaultKernel = model && model.defaultKernel && model.defaultKernel.name;
|
||||
if (!defaultKernel) {
|
||||
this.sendNotification(notificationService, Severity.Error, localize('noDefaultKernel', "No kernel is available for this notebook"));
|
||||
return undefined;
|
||||
}
|
||||
await clientSession.changeKernel({
|
||||
name: defaultKernel
|
||||
});
|
||||
}
|
||||
return clientSession.kernel;
|
||||
}
|
||||
|
||||
private sendNotification(notificationService: INotificationService, severity: Severity, message: string): void {
|
||||
if (notificationService) {
|
||||
notificationService.notify({ severity: severity, message: message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the future which will be used to update the output
|
||||
* area for this cell
|
||||
*/
|
||||
setFuture(future: FutureInternal): void {
|
||||
if (this._future === future) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
// Setting the future indicates the cell is running which enables trusted mode.
|
||||
// See https://jupyter-notebook.readthedocs.io/en/stable/security.html
|
||||
|
||||
this._isTrusted = true;
|
||||
|
||||
if (this._future) {
|
||||
this._future.dispose();
|
||||
}
|
||||
this.clearOutputs();
|
||||
this._future = future;
|
||||
future.setReplyHandler({ handle: (msg) => this.handleReply(msg) });
|
||||
future.setIOPubHandler({ handle: (msg) => this.handleIOPub(msg) });
|
||||
future.setStdInHandler({ handle: (msg) => this.handleSdtIn(msg) });
|
||||
}
|
||||
|
||||
public clearOutputs(): void {
|
||||
this._outputs = [];
|
||||
this.fireOutputsChanged();
|
||||
}
|
||||
|
||||
private fireOutputsChanged(shouldScroll: boolean = false): void {
|
||||
let outputEvent: IOutputChangedEvent = {
|
||||
outputs: this.outputs,
|
||||
shouldScroll: !!shouldScroll
|
||||
};
|
||||
this._onOutputsChanged.fire(outputEvent);
|
||||
if (this.outputs.length !== 0) {
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated);
|
||||
} else {
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellOutputCleared);
|
||||
}
|
||||
}
|
||||
|
||||
private sendChangeToNotebook(change: NotebookChangeType): void {
|
||||
if (this._options && this._options.notebook) {
|
||||
this._options.notebook.onCellChange(this, change);
|
||||
}
|
||||
}
|
||||
|
||||
public get outputs(): Array<nb.ICellOutput> {
|
||||
return this._outputs;
|
||||
}
|
||||
|
||||
private handleReply(msg: nb.IShellMessage): void {
|
||||
// TODO #931 we should process this. There can be a payload attached which should be added to outputs.
|
||||
// In all other cases, it is a no-op
|
||||
|
||||
if (!this._future.inProgress) {
|
||||
this.disposeFuture();
|
||||
}
|
||||
}
|
||||
|
||||
private handleIOPub(msg: nb.IIOPubMessage): void {
|
||||
let msgType = msg.header.msg_type;
|
||||
let output: nb.ICellOutput;
|
||||
switch (msgType) {
|
||||
case 'execute_result':
|
||||
case 'display_data':
|
||||
case 'stream':
|
||||
case 'error':
|
||||
output = msg.content as nb.ICellOutput;
|
||||
output.output_type = msgType;
|
||||
break;
|
||||
case 'clear_output':
|
||||
// TODO wait until next message before clearing
|
||||
// let wait = (msg as KernelMessage.IClearOutputMsg).content.wait;
|
||||
this.clearOutputs();
|
||||
break;
|
||||
case 'update_display_data':
|
||||
output = msg.content as nb.ICellOutput;
|
||||
output.output_type = 'display_data';
|
||||
// TODO #930 handle in-place update of displayed data
|
||||
// targets = this._displayIdMap.get(displayId);
|
||||
// if (targets) {
|
||||
// for (let index of targets) {
|
||||
// model.set(index, output);
|
||||
// }
|
||||
// }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// TODO handle in-place update of displayed data
|
||||
// if (displayId && msgType === 'display_data') {
|
||||
// targets = this._displayIdMap.get(displayId) || [];
|
||||
// targets.push(model.length - 1);
|
||||
// this._displayIdMap.set(displayId, targets);
|
||||
// }
|
||||
if (output) {
|
||||
// deletes transient node in the serialized JSON
|
||||
delete output['transient'];
|
||||
this._outputs.push(this.rewriteOutputUrls(output));
|
||||
// Only scroll on 1st output being added
|
||||
let shouldScroll = this._outputs.length === 1;
|
||||
this.fireOutputsChanged(shouldScroll);
|
||||
}
|
||||
}
|
||||
|
||||
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
|
||||
const driverLog = '/gateway/default/yarn/container';
|
||||
const yarnUi = '/gateway/default/yarn/proxy';
|
||||
const defaultPort = ':30433';
|
||||
// Only rewrite if this is coming back during execution, not when loading from disk.
|
||||
// A good approximation is that the model has a future (needed for execution)
|
||||
if (this.future) {
|
||||
try {
|
||||
let result = output as nb.IDisplayResult;
|
||||
if (result && result.data && result.data['text/html']) {
|
||||
let model = (this as CellModel).options.notebook as NotebookModel;
|
||||
if (model.activeConnection) {
|
||||
let gatewayEndpointInfo = this.getGatewayEndpoint(model.activeConnection);
|
||||
if (gatewayEndpointInfo) {
|
||||
let hostAndIp = notebookUtils.getHostAndPortFromEndpoint(gatewayEndpointInfo.endpoint);
|
||||
let host = hostAndIp.host ? hostAndIp.host : model.activeConnection.serverName;
|
||||
let port = hostAndIp.port ? ':' + hostAndIp.port : defaultPort;
|
||||
let html = result.data['text/html'];
|
||||
// CTP 3.1 and earlier Spark link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/master.*\/proxy)(.*)/g, html, host, port, yarnUi);
|
||||
// CTP 3.2 and later spark link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/sparkhead.*\/proxy)(.*)/g, html, host, port, yarnUi);
|
||||
// Driver link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/storage.*\/containerlogs)(.*)/g, html, host, port, driverLog);
|
||||
(<nb.IDisplayResult>output).data['text/html'] = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private rewriteUrlUsingRegex(regex: RegExp, html: string, host: string, port: string, target: string): string {
|
||||
return html.replace(regex, function (a, b, c) {
|
||||
let ret = '';
|
||||
if (b !== '') {
|
||||
ret = 'https://' + host + port + target;
|
||||
}
|
||||
if (c !== '') {
|
||||
ret = ret + c;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
public setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void {
|
||||
this._stdInHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* StdIn requires user interaction, so this is deferred to upstream UI
|
||||
* components. If one is registered the cell will call and wait on it, if not
|
||||
* it will immediately return to unblock error handling
|
||||
*/
|
||||
private handleSdtIn(msg: nb.IStdinMessage): void | Thenable<void> {
|
||||
let handler = async () => {
|
||||
if (!this._stdInHandler) {
|
||||
// No-op
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._stdInHandler.handle(msg);
|
||||
} catch (err) {
|
||||
if (this.future) {
|
||||
// TODO should we error out in this case somehow? E.g. send Ctrl+C?
|
||||
this.future.sendInputReply({ value: '' });
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler();
|
||||
}
|
||||
|
||||
public toJSON(): nb.ICellContents {
|
||||
let metadata = this._metadata || {};
|
||||
let cellJson: Partial<nb.ICellContents> = {
|
||||
cell_type: this._cellType,
|
||||
source: this._source,
|
||||
metadata: metadata
|
||||
};
|
||||
cellJson.metadata.azdata_cell_guid = this._cellGuid;
|
||||
if (this._cellType === CellTypes.Code) {
|
||||
cellJson.metadata.language = this._language;
|
||||
cellJson.metadata.tags = metadata.tags;
|
||||
cellJson.outputs = this._outputs;
|
||||
cellJson.execution_count = this.executionCount ? this.executionCount : 0;
|
||||
}
|
||||
return cellJson as nb.ICellContents;
|
||||
}
|
||||
|
||||
public fromJSON(cell: nb.ICellContents): void {
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
this._cellType = cell.cell_type;
|
||||
this.executionCount = cell.execution_count;
|
||||
this._source = this.getMultilineSource(cell.source);
|
||||
this._metadata = cell.metadata || {};
|
||||
|
||||
if (this._metadata.tags && this._metadata.tags.some(x => x === HideInputTag) && this._cellType === CellTypes.Code) {
|
||||
this._isCollapsed = true;
|
||||
} else {
|
||||
this._isCollapsed = false;
|
||||
}
|
||||
|
||||
this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid();
|
||||
this.setLanguageFromContents(cell);
|
||||
if (cell.outputs) {
|
||||
for (let output of cell.outputs) {
|
||||
// For now, we're assuming it's OK to save these as-is with no modification
|
||||
this.addOutput(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setLanguageFromContents(cell: nb.ICellContents): void {
|
||||
if (cell.cell_type === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
} else if (cell.metadata && cell.metadata.language) {
|
||||
this._language = cell.metadata.language;
|
||||
}
|
||||
// else skip, we set default language anyhow
|
||||
}
|
||||
|
||||
private addOutput(output: nb.ICellOutput) {
|
||||
this._normalize(output);
|
||||
this._outputs.push(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an output.
|
||||
*/
|
||||
private _normalize(value: nb.ICellOutput): void {
|
||||
if (notebookUtils.isStream(value)) {
|
||||
if (Array.isArray(value.text)) {
|
||||
value.text = (value.text as string[]).join('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createUri(): void {
|
||||
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` });
|
||||
// Use this to set the internal (immutable) and public (shared with extension) uri properties
|
||||
this.cellUri = uri;
|
||||
}
|
||||
|
||||
// Get Knox endpoint from IConnectionProfile
|
||||
// TODO: this will be refactored out into the notebooks extension as a contribution point
|
||||
private getGatewayEndpoint(activeConnection: IConnectionProfile): notebookUtils.IEndpoint {
|
||||
let endpoint;
|
||||
if (this._connectionManagementService && activeConnection && activeConnection.providerName.toLowerCase() === notebookConstants.SQL_CONNECTION_PROVIDER.toLowerCase()) {
|
||||
let serverInfo: ServerInfo = this._connectionManagementService.getServerInfo(activeConnection.id);
|
||||
if (serverInfo) {
|
||||
let endpoints: notebookUtils.IEndpoint[] = notebookUtils.getClusterEndpoints(serverInfo);
|
||||
if (endpoints && endpoints.length > 0) {
|
||||
endpoint = find(endpoints, ep => ep.serviceName.toLowerCase() === notebookUtils.hadoopEndpointNameGateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
|
||||
private getMultilineSource(source: string | string[]): string | string[] {
|
||||
if (source === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (typeof source === 'string') {
|
||||
let sourceMultiline = source.split('\n');
|
||||
// If source is one line (i.e. no '\n'), return it immediately
|
||||
if (sourceMultiline.length === 1) {
|
||||
return [source];
|
||||
} else if (sourceMultiline.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// Otherwise, add back all of the newlines here
|
||||
// Note: for Windows machines that require '/r/n',
|
||||
// splitting on '\n' and putting back the '\n' will still
|
||||
// retain the '\r', so that isn't lost in the process
|
||||
// Note: the last line will not include a newline at the end
|
||||
for (let i = 0; i < sourceMultiline.length - 1; i++) {
|
||||
sourceMultiline[i] += '\n';
|
||||
}
|
||||
return sourceMultiline;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
// Dispose and set current future to undefined
|
||||
private disposeFuture() {
|
||||
if (this._future) {
|
||||
this._future.dispose();
|
||||
}
|
||||
this._future = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICellMagicMapper } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
const defaultKernel = '*';
|
||||
export class CellMagicMapper implements ICellMagicMapper {
|
||||
private kernelToMagicMap = new Map<string, ILanguageMagic[]>();
|
||||
|
||||
constructor(languageMagics: ILanguageMagic[]) {
|
||||
if (languageMagics) {
|
||||
for (let magic of languageMagics) {
|
||||
if (!magic.kernels || magic.kernels.length === 0) {
|
||||
this.addKernelMapping(defaultKernel, magic);
|
||||
}
|
||||
if (magic.kernels) {
|
||||
for (let kernel of magic.kernels) {
|
||||
this.addKernelMapping(kernel.toLowerCase(), magic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addKernelMapping(kernelId: string, magic: ILanguageMagic): void {
|
||||
let magics = this.kernelToMagicMap.get(kernelId) || [];
|
||||
magics.push(magic);
|
||||
this.kernelToMagicMap.set(kernelId, magics);
|
||||
}
|
||||
|
||||
private findMagicForKernel(searchText: string, kernelId: string): ILanguageMagic | undefined {
|
||||
if (kernelId === undefined || !searchText) {
|
||||
return undefined;
|
||||
}
|
||||
searchText = searchText.toLowerCase();
|
||||
let kernelMagics = this.kernelToMagicMap.get(kernelId) || [];
|
||||
if (kernelMagics) {
|
||||
return find(kernelMagics, m => m.magic.toLowerCase() === searchText);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic {
|
||||
let languageMagic = this.findMagicForKernel(magic, kernelId.toLowerCase());
|
||||
if (!languageMagic) {
|
||||
languageMagic = this.findMagicForKernel(magic, defaultKernel);
|
||||
}
|
||||
return languageMagic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
import { IClientSession, IKernelPreference, IClientSessionOptions } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { INotebookManager } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
type KernelChangeHandler = (kernel: nb.IKernelChangedArgs) => Promise<void>;
|
||||
/**
|
||||
* Implementation of a client session. This is a model over session operations,
|
||||
* which may come from the session manager or a specific session.
|
||||
*/
|
||||
export class ClientSession implements IClientSession {
|
||||
//#region private fields with public accessors
|
||||
private _terminatedEmitter = new Emitter<void>();
|
||||
private _kernelChangedEmitter = new Emitter<nb.IKernelChangedArgs>();
|
||||
private _statusChangedEmitter = new Emitter<nb.ISession>();
|
||||
private _iopubMessageEmitter = new Emitter<nb.IMessage>();
|
||||
private _unhandledMessageEmitter = new Emitter<nb.IMessage>();
|
||||
private _propertyChangedEmitter = new Emitter<'path' | 'name' | 'type'>();
|
||||
private _notebookUri: URI;
|
||||
private _type: string;
|
||||
private _name: string;
|
||||
private _isReady: boolean;
|
||||
private _ready: Deferred<void>;
|
||||
private _kernelChangeCompleted: Deferred<void>;
|
||||
private _kernelPreference: IKernelPreference;
|
||||
private _kernelDisplayName: string;
|
||||
private _errorMessage: string;
|
||||
private _cachedKernelSpec: nb.IKernelSpec;
|
||||
private _kernelChangeHandlers: KernelChangeHandler[] = [];
|
||||
private _defaultKernel: nb.IKernelSpec;
|
||||
|
||||
//#endregion
|
||||
|
||||
private _serverLoadFinished: Promise<void>;
|
||||
private _session: nb.ISession;
|
||||
private isServerStarted: boolean;
|
||||
private notebookManager: INotebookManager;
|
||||
private _kernelConfigActions: ((kernelName: string) => Promise<any>)[] = [];
|
||||
|
||||
constructor(private options: IClientSessionOptions) {
|
||||
this._notebookUri = options.notebookUri;
|
||||
this.notebookManager = options.notebookManager;
|
||||
this._isReady = false;
|
||||
this._ready = new Deferred<void>();
|
||||
this._kernelChangeCompleted = new Deferred<void>();
|
||||
this._defaultKernel = options.kernelSpec;
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
try {
|
||||
this._serverLoadFinished = this.startServer();
|
||||
await this._serverLoadFinished;
|
||||
await this.initializeSession();
|
||||
await this.updateCachedKernelSpec();
|
||||
} catch (err) {
|
||||
this._errorMessage = getErrorMessage(err) || localize('clientSession.unknownError', "An error occurred while starting the notebook session");
|
||||
}
|
||||
// Always resolving for now. It's up to callers to check for error case
|
||||
this._isReady = true;
|
||||
this._ready.resolve();
|
||||
if (!this.isInErrorState && this._session && this._session.kernel) {
|
||||
await this.notifyKernelChanged(undefined, this._session.kernel);
|
||||
}
|
||||
}
|
||||
|
||||
private async startServer(): Promise<void> {
|
||||
let serverManager = this.notebookManager.serverManager;
|
||||
if (serverManager) {
|
||||
await serverManager.startServer();
|
||||
if (!serverManager.isStarted) {
|
||||
throw new Error(localize('ServerNotStarted', "Server did not start for unknown reason"));
|
||||
}
|
||||
this.isServerStarted = serverManager.isStarted;
|
||||
} else {
|
||||
this.isServerStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeSession(): Promise<void> {
|
||||
await this._serverLoadFinished;
|
||||
if (this.isServerStarted) {
|
||||
if (!this.notebookManager.sessionManager.isReady) {
|
||||
await this.notebookManager.sessionManager.ready;
|
||||
}
|
||||
if (this._defaultKernel) {
|
||||
await this.startSessionInstance(this._defaultKernel.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startSessionInstance(kernelName: string): Promise<void> {
|
||||
let session: nb.ISession;
|
||||
try {
|
||||
// TODO #3164 should use URI instead of path for startNew
|
||||
session = await this.notebookManager.sessionManager.startNew({
|
||||
path: this.notebookUri.fsPath,
|
||||
kernelName: kernelName
|
||||
// TODO add kernel name if saved in the document
|
||||
});
|
||||
session.defaultKernelLoaded = true;
|
||||
} catch (err) {
|
||||
// TODO move registration
|
||||
if (err && err.response && err.response.status === 501) {
|
||||
this.options.notificationService.warn(localize('kernelRequiresConnection', "Kernel {0} was not found. The default kernel will be used instead.", kernelName));
|
||||
session = await this.notebookManager.sessionManager.startNew({
|
||||
path: this.notebookUri.fsPath,
|
||||
kernelName: undefined
|
||||
});
|
||||
session.defaultKernelLoaded = false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this._session = session;
|
||||
await this.runKernelConfigActions(kernelName);
|
||||
this._statusChangedEmitter.fire(session);
|
||||
}
|
||||
|
||||
private async runKernelConfigActions(kernelName: string): Promise<void> {
|
||||
for (let startAction of this._kernelConfigActions) {
|
||||
await startAction(kernelName);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// No-op for now
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the server has finished loading. It may have failed to load in
|
||||
* which case the view will be in an error state.
|
||||
*/
|
||||
public get serverLoadFinished(): Promise<void> {
|
||||
return this._serverLoadFinished;
|
||||
}
|
||||
|
||||
|
||||
//#region IClientSession Properties
|
||||
public get terminated(): Event<void> {
|
||||
return this._terminatedEmitter.event;
|
||||
}
|
||||
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
|
||||
return this._kernelChangedEmitter.event;
|
||||
}
|
||||
|
||||
public onKernelChanging(changeHandler: (kernel: nb.IKernelChangedArgs) => Promise<void>): void {
|
||||
if (changeHandler) {
|
||||
this._kernelChangeHandlers.push(changeHandler);
|
||||
}
|
||||
}
|
||||
public get statusChanged(): Event<nb.ISession> {
|
||||
return this._statusChangedEmitter.event;
|
||||
}
|
||||
public get iopubMessage(): Event<nb.IMessage> {
|
||||
return this._iopubMessageEmitter.event;
|
||||
}
|
||||
public get unhandledMessage(): Event<nb.IMessage> {
|
||||
return this._unhandledMessageEmitter.event;
|
||||
}
|
||||
public get propertyChanged(): Event<'path' | 'name' | 'type'> {
|
||||
return this._propertyChangedEmitter.event;
|
||||
}
|
||||
public get kernel(): nb.IKernel | null {
|
||||
return this._session ? this._session.kernel : undefined;
|
||||
}
|
||||
public get notebookUri(): URI {
|
||||
return this._notebookUri;
|
||||
}
|
||||
public get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
public get type(): string {
|
||||
return this._type;
|
||||
}
|
||||
public get status(): nb.KernelStatus {
|
||||
if (!this.isReady) {
|
||||
return 'starting';
|
||||
}
|
||||
return this._session ? this._session.status : 'dead';
|
||||
}
|
||||
public get isReady(): boolean {
|
||||
return this._isReady;
|
||||
}
|
||||
public get ready(): Promise<void> {
|
||||
return this._ready.promise;
|
||||
}
|
||||
public get kernelChangeCompleted(): Promise<void> {
|
||||
return this._kernelChangeCompleted.promise;
|
||||
}
|
||||
public get kernelPreference(): IKernelPreference {
|
||||
return this._kernelPreference;
|
||||
}
|
||||
public set kernelPreference(value: IKernelPreference) {
|
||||
this._kernelPreference = value;
|
||||
}
|
||||
public get kernelDisplayName(): string {
|
||||
return this._kernelDisplayName;
|
||||
}
|
||||
public get errorMessage(): string {
|
||||
return this._errorMessage;
|
||||
}
|
||||
public get isInErrorState(): boolean {
|
||||
return !!this._errorMessage;
|
||||
}
|
||||
|
||||
public get cachedKernelSpec(): nb.IKernelSpec {
|
||||
return this._cachedKernelSpec;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Not Yet Implemented
|
||||
/**
|
||||
* Change the current kernel associated with the document.
|
||||
*/
|
||||
async changeKernel(options: nb.IKernelSpec, oldValue?: nb.IKernel): Promise<nb.IKernel> {
|
||||
this._kernelChangeCompleted = new Deferred<void>();
|
||||
this._isReady = false;
|
||||
let oldKernel = oldValue ? oldValue : this.kernel;
|
||||
|
||||
let kernel = await this.doChangeKernel(options);
|
||||
try {
|
||||
await kernel.ready;
|
||||
} catch (error) {
|
||||
// Cleanup some state before re-throwing
|
||||
this._isReady = kernel.isReady;
|
||||
this._kernelChangeCompleted.resolve();
|
||||
throw error;
|
||||
}
|
||||
let newKernel = this._session ? kernel : this._session.kernel;
|
||||
this._isReady = kernel.isReady;
|
||||
await this.updateCachedKernelSpec();
|
||||
// Send resolution events to listeners
|
||||
await this.notifyKernelChanged(oldKernel, newKernel);
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private async notifyKernelChanged(oldKernel: nb.IKernel, newKernel: nb.IKernel): Promise<void> {
|
||||
let changeArgs: nb.IKernelChangedArgs = {
|
||||
oldValue: oldKernel,
|
||||
newValue: newKernel
|
||||
};
|
||||
let changePromises = this._kernelChangeHandlers.map(handler => handler(changeArgs));
|
||||
await Promise.all(changePromises);
|
||||
// Wait on connection configuration to complete before resolving full kernel change
|
||||
this._kernelChangeCompleted.resolve();
|
||||
this._kernelChangedEmitter.fire(changeArgs);
|
||||
}
|
||||
|
||||
private async updateCachedKernelSpec(): Promise<void> {
|
||||
this._cachedKernelSpec = undefined;
|
||||
let kernel = this.kernel;
|
||||
if (kernel) {
|
||||
await kernel.ready;
|
||||
if (kernel.isReady) {
|
||||
this._cachedKernelSpec = await kernel.getSpec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to either call ChangeKernel on current session, or start a new session
|
||||
*/
|
||||
private async doChangeKernel(options: nb.IKernelSpec): Promise<nb.IKernel> {
|
||||
let kernel: nb.IKernel;
|
||||
if (this._session) {
|
||||
kernel = await this._session.changeKernel(options);
|
||||
await this.runKernelConfigActions(kernel.name);
|
||||
} else {
|
||||
kernel = await this.startSessionInstance(options.name).then(() => this.kernel);
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
public async configureKernel(options: nb.IKernelSpec): Promise<void> {
|
||||
if (this._session) {
|
||||
await this._session.configureKernel(options);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateConnection(connection: IConnectionProfile): Promise<void> {
|
||||
if (!this.kernel) {
|
||||
// TODO is there any case where skipping causes errors? So far it seems like it gets called twice
|
||||
return;
|
||||
}
|
||||
if (connection.id !== '-1') {
|
||||
await this._session.configureConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the kernel and shutdown the session.
|
||||
*
|
||||
* @returns A promise that resolves when the session is shut down.
|
||||
*/
|
||||
public async shutdown(): Promise<void> {
|
||||
// Always try to shut down session
|
||||
if (this._session && this._session.id && this.notebookManager && this.notebookManager.sessionManager) {
|
||||
await this.notebookManager.sessionManager.shutdown(this._session.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a kernel for the session.
|
||||
*/
|
||||
selectKernel(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the session.
|
||||
*
|
||||
* @returns A promise that resolves with whether the kernel has restarted.
|
||||
*
|
||||
* #### Notes
|
||||
* If there is a running kernel, present a dialog.
|
||||
* If there is no kernel, we start a kernel with the last run
|
||||
* kernel name and resolves with `true`. If no kernel has been started,
|
||||
* this is a no-op, and resolves with `false`.
|
||||
*/
|
||||
restart(): Promise<boolean> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the session path.
|
||||
*
|
||||
* @param path - The new session path.
|
||||
*
|
||||
* @returns A promise that resolves when the session has renamed.
|
||||
*
|
||||
* #### Notes
|
||||
* This uses the Jupyter REST API, and the response is validated.
|
||||
* The promise is fulfilled on a valid response and rejected otherwise.
|
||||
*/
|
||||
setPath(path: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the session name.
|
||||
*/
|
||||
setName(name: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the session type.
|
||||
*/
|
||||
setType(type: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { IRenderMime } from './renderMimeInterfaces';
|
||||
import { ReadonlyJSONObject } from '../../common/models/jsonext';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
/**
|
||||
* The default mime model implementation.
|
||||
*/
|
||||
export class MimeModel implements IRenderMime.IMimeModel {
|
||||
/**
|
||||
* Construct a new mime model.
|
||||
*/
|
||||
constructor(options: MimeModel.IOptions = {}) {
|
||||
this.trusted = !!options.trusted;
|
||||
this._data = options.data || {};
|
||||
this._metadata = options.metadata || {};
|
||||
this._callback = options.callback;
|
||||
this._themeService = options.themeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the model is trusted.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* The data associated with the model.
|
||||
*/
|
||||
get data(): ReadonlyJSONObject {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata associated with the model.
|
||||
*/
|
||||
get metadata(): ReadonlyJSONObject {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
get themeService(): IThemeService {
|
||||
return this._themeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data associated with the model.
|
||||
*
|
||||
* #### Notes
|
||||
* Depending on the implementation of the mime model,
|
||||
* this call may or may not have deferred effects,
|
||||
*/
|
||||
setData(options: IRenderMime.ISetDataOptions): void {
|
||||
this._data = options.data || this._data;
|
||||
this._metadata = options.metadata || this._metadata;
|
||||
this._callback(options);
|
||||
}
|
||||
|
||||
private _callback: (options: IRenderMime.ISetDataOptions) => void;
|
||||
private _data: ReadonlyJSONObject;
|
||||
private _metadata: ReadonlyJSONObject;
|
||||
private _themeService: IThemeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for MimeModel class statics.
|
||||
*/
|
||||
export namespace MimeModel {
|
||||
/**
|
||||
* The options used to create a mime model.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Whether the model is trusted. Defaults to `false`.
|
||||
*/
|
||||
trusted?: boolean;
|
||||
|
||||
/**
|
||||
* A callback function for when the data changes.
|
||||
*/
|
||||
callback?: (options: IRenderMime.ISetDataOptions) => void;
|
||||
|
||||
/**
|
||||
* The initial mime data.
|
||||
*/
|
||||
data?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The initial mime metadata.
|
||||
*/
|
||||
metadata?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* Theme service used to react to theme change events
|
||||
*/
|
||||
themeService?: IThemeService;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb } from 'azdata';
|
||||
|
||||
import { CellModel } from 'sql/workbench/contrib/notebook/browser/models/cell';
|
||||
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ClientSession } from 'sql/workbench/contrib/notebook/browser/models/clientSession';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class ModelFactory implements IModelFactory {
|
||||
|
||||
constructor(private instantiationService: IInstantiationService) {
|
||||
|
||||
}
|
||||
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
|
||||
return this.instantiationService.createInstance(CellModel, cell, options);
|
||||
}
|
||||
|
||||
public createClientSession(options: IClientSessionOptions): IClientSession {
|
||||
return new ClientSession(options);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { CellType, NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
|
||||
import { INotebookManager, ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IStandardKernelWithProvider } from 'sql/workbench/contrib/notebook/browser/models/notebookUtils';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
|
||||
export interface IClientSessionOptions {
|
||||
notebookUri: URI;
|
||||
notebookManager: INotebookManager;
|
||||
notificationService: INotificationService;
|
||||
kernelSpec: nb.IKernelSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface of client session object.
|
||||
*
|
||||
* The client session represents the link between
|
||||
* a path and its kernel for the duration of the lifetime
|
||||
* of the session object. The session can have no current
|
||||
* kernel, and can start a new kernel at any time.
|
||||
*/
|
||||
export interface IClientSession extends IDisposable {
|
||||
/**
|
||||
* A signal emitted when the session is shut down.
|
||||
*/
|
||||
readonly terminated: Event<void>;
|
||||
|
||||
/**
|
||||
* A signal emitted when the kernel changes.
|
||||
*/
|
||||
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
|
||||
|
||||
/**
|
||||
* A signal emitted when the kernel status changes.
|
||||
*/
|
||||
readonly statusChanged: Event<nb.ISession>;
|
||||
|
||||
/**
|
||||
* A signal emitted for a kernel messages.
|
||||
*/
|
||||
readonly iopubMessage: Event<nb.IMessage>;
|
||||
|
||||
/**
|
||||
* A signal emitted for an unhandled kernel message.
|
||||
*/
|
||||
readonly unhandledMessage: Event<nb.IMessage>;
|
||||
|
||||
/**
|
||||
* A signal emitted when a session property changes.
|
||||
*/
|
||||
readonly propertyChanged: Event<'path' | 'name' | 'type'>;
|
||||
|
||||
/**
|
||||
* The current kernel associated with the document.
|
||||
*/
|
||||
readonly kernel: nb.IKernel | null;
|
||||
|
||||
/**
|
||||
* The current path associated with the client session.
|
||||
*/
|
||||
readonly notebookUri: URI;
|
||||
|
||||
/**
|
||||
* The current name associated with the client session.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The type of the client session.
|
||||
*/
|
||||
readonly type: string;
|
||||
|
||||
/**
|
||||
* The current status of the client session.
|
||||
*/
|
||||
readonly status: nb.KernelStatus;
|
||||
|
||||
/**
|
||||
* Whether the session is ready.
|
||||
*/
|
||||
readonly isReady: boolean;
|
||||
|
||||
/**
|
||||
* Whether the session is in an unusable state
|
||||
*/
|
||||
readonly isInErrorState: boolean;
|
||||
/**
|
||||
* The error information, if this session is in an error state
|
||||
*/
|
||||
readonly errorMessage: string;
|
||||
|
||||
/**
|
||||
* A promise that is fulfilled when the session is ready.
|
||||
*/
|
||||
readonly ready: Promise<void>;
|
||||
|
||||
/**
|
||||
* A promise that is fulfilled when the session completes a kernel change.
|
||||
*/
|
||||
readonly kernelChangeCompleted: Promise<void>;
|
||||
|
||||
/**
|
||||
* The kernel preference.
|
||||
*/
|
||||
kernelPreference: IKernelPreference;
|
||||
|
||||
/**
|
||||
* The display name of the kernel.
|
||||
*/
|
||||
readonly kernelDisplayName: string;
|
||||
|
||||
readonly cachedKernelSpec: nb.IKernelSpec;
|
||||
|
||||
/**
|
||||
* Initializes the ClientSession, by starting the server and
|
||||
* connecting to the SessionManager.
|
||||
* This will optionally start a session if the kernel preferences
|
||||
* indicate this is desired
|
||||
*/
|
||||
initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Change the current kernel associated with the document.
|
||||
*/
|
||||
changeKernel(
|
||||
options: nb.IKernelSpec,
|
||||
oldKernel?: nb.IKernel
|
||||
): Promise<nb.IKernel>;
|
||||
|
||||
/**
|
||||
* Configure the current kernel associated with the document.
|
||||
*/
|
||||
configureKernel(
|
||||
options: nb.IKernelSpec
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Kill the kernel and shutdown the session.
|
||||
*
|
||||
* @returns A promise that resolves when the session is shut down.
|
||||
*/
|
||||
shutdown(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Select a kernel for the session.
|
||||
*/
|
||||
selectKernel(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Restart the session.
|
||||
*
|
||||
* @returns A promise that resolves with whether the kernel has restarted.
|
||||
*
|
||||
* #### Notes
|
||||
* If there is a running kernel, present a dialog.
|
||||
* If there is no kernel, we start a kernel with the last run
|
||||
* kernel name and resolves with `true`. If no kernel has been started,
|
||||
* this is a no-op, and resolves with `false`.
|
||||
*/
|
||||
restart(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Change the session path.
|
||||
*
|
||||
* @param path - The new session path.
|
||||
*
|
||||
* @returns A promise that resolves when the session has renamed.
|
||||
*
|
||||
* #### Notes
|
||||
* This uses the Jupyter REST API, and the response is validated.
|
||||
* The promise is fulfilled on a valid response and rejected otherwise.
|
||||
*/
|
||||
setPath(path: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Change the session name.
|
||||
*/
|
||||
setName(name: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Change the session type.
|
||||
*/
|
||||
setType(type: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the connection
|
||||
*/
|
||||
updateConnection(connection: IConnectionProfile): Promise<void>;
|
||||
|
||||
/**
|
||||
* Supports registering a handler to run during kernel change and implement any calls needed to configure
|
||||
* the kernel before actions such as run should be allowed
|
||||
*/
|
||||
onKernelChanging(changeHandler: ((kernel: nb.IKernelChangedArgs) => Promise<void>)): void;
|
||||
}
|
||||
|
||||
export interface IDefaultConnection {
|
||||
defaultConnection: ConnectionProfile;
|
||||
otherConnections: ConnectionProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A kernel preference.
|
||||
*/
|
||||
export interface IKernelPreference {
|
||||
/**
|
||||
* The name of the kernel.
|
||||
*/
|
||||
readonly name?: string;
|
||||
|
||||
/**
|
||||
* The preferred kernel language.
|
||||
*/
|
||||
readonly language?: string;
|
||||
|
||||
/**
|
||||
* The id of an existing kernel.
|
||||
*/
|
||||
readonly id?: string;
|
||||
|
||||
/**
|
||||
* Whether to prefer starting a kernel.
|
||||
*/
|
||||
readonly shouldStart?: boolean;
|
||||
|
||||
/**
|
||||
* Whether a kernel can be started.
|
||||
*/
|
||||
readonly canStart?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to auto-start the default kernel if no matching kernel is found.
|
||||
*/
|
||||
readonly autoStartDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface INotebookModel {
|
||||
/**
|
||||
* Cell List for this model
|
||||
*/
|
||||
readonly cells: ReadonlyArray<ICellModel>;
|
||||
|
||||
/**
|
||||
* The active cell for this model. May be undefined
|
||||
*/
|
||||
readonly activeCell: ICellModel;
|
||||
|
||||
/**
|
||||
* Client Session in the notebook, used for sending requests to the notebook service
|
||||
*/
|
||||
readonly clientSession: IClientSession;
|
||||
/**
|
||||
* LanguageInfo saved in the notebook
|
||||
*/
|
||||
readonly languageInfo: nb.ILanguageInfo;
|
||||
/**
|
||||
* Current default language for the notebook
|
||||
*/
|
||||
readonly language: string;
|
||||
|
||||
/**
|
||||
* All notebook managers applicable for a given notebook
|
||||
*/
|
||||
readonly notebookManagers: INotebookManager[];
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the kernel and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
|
||||
|
||||
/**
|
||||
* Fired on notifications that notebook components should be re-laid out.
|
||||
*/
|
||||
readonly layoutChanged: Event<void>;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the kernels and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly kernelsChanged: Event<nb.IKernelSpec>;
|
||||
|
||||
/**
|
||||
* Default kernel
|
||||
*/
|
||||
defaultKernel?: nb.IKernelSpec;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the contexts and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly contextsChanged: Event<void>;
|
||||
|
||||
/**
|
||||
* Event fired on when switching kernel and should show loading context
|
||||
*/
|
||||
readonly contextsLoading: Event<void>;
|
||||
|
||||
/**
|
||||
* The specs for available kernels, or undefined if these have
|
||||
* not been loaded yet
|
||||
*/
|
||||
readonly specs: nb.IAllKernels | undefined;
|
||||
|
||||
/**
|
||||
* The specs for available contexts, or undefined if these have
|
||||
* not been loaded yet
|
||||
*/
|
||||
readonly contexts: IDefaultConnection | undefined;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the cells and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly contentChanged: Event<NotebookContentChange>;
|
||||
|
||||
/**
|
||||
* Event fired on notebook provider change
|
||||
*/
|
||||
readonly onProviderIdChange: Event<string>;
|
||||
|
||||
/**
|
||||
* Event fired on active cell change
|
||||
*/
|
||||
readonly onActiveCellChanged: Event<ICellModel>;
|
||||
|
||||
/**
|
||||
* The trusted mode of the Notebook
|
||||
*/
|
||||
trustedMode: boolean;
|
||||
|
||||
/**
|
||||
* Current notebook provider id
|
||||
*/
|
||||
providerId: string;
|
||||
|
||||
/**
|
||||
* Change the current kernel from the Kernel dropdown
|
||||
* @param displayName kernel name (as displayed in Kernel dropdown)
|
||||
*/
|
||||
changeKernel(displayName: string): void;
|
||||
|
||||
/**
|
||||
* Change the current context (if applicable)
|
||||
*/
|
||||
changeContext(host: string, connection?: IConnectionProfile, hideErrorMessage?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Find a cell's index given its model
|
||||
*/
|
||||
findCellIndex(cellModel: ICellModel): number;
|
||||
|
||||
/**
|
||||
* Adds a cell to the index of the model
|
||||
*/
|
||||
addCell(cellType: CellType, index?: number): void;
|
||||
|
||||
/**
|
||||
* Deletes a cell
|
||||
*/
|
||||
deleteCell(cellModel: ICellModel): void;
|
||||
|
||||
/**
|
||||
* Serialize notebook cell content to JSON
|
||||
*/
|
||||
toJSON(type?: NotebookChangeType): nb.INotebookContents;
|
||||
|
||||
/**
|
||||
* Notifies the notebook of a change in the cell
|
||||
*/
|
||||
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
|
||||
|
||||
|
||||
/**
|
||||
* Push edit operations, basically editing the model. This is the preferred way of
|
||||
* editing the model. Long-term, this will ensure edit operations can be added to the undo stack
|
||||
* @param edits The edit operations to perform
|
||||
*/
|
||||
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
|
||||
|
||||
getApplicableConnectionProviderIds(kernelName: string): string[];
|
||||
|
||||
/**
|
||||
* Get the standardKernelWithProvider by name
|
||||
* @param name The kernel name
|
||||
*/
|
||||
getStandardKernelFromName(name: string): IStandardKernelWithProvider;
|
||||
|
||||
/** Event fired once we get call back from ConfigureConnection method in sqlops extension */
|
||||
readonly onValidConnectionSelected: Event<boolean>;
|
||||
|
||||
serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void;
|
||||
|
||||
standardKernels: IStandardKernelWithProvider[];
|
||||
|
||||
/**
|
||||
* Updates the model's view of an active cell to the new active cell
|
||||
* @param cell New active cell
|
||||
*/
|
||||
updateActiveCell(cell: ICellModel);
|
||||
}
|
||||
|
||||
export interface NotebookContentChange {
|
||||
/**
|
||||
* The type of change that occurred
|
||||
*/
|
||||
changeType: NotebookChangeType;
|
||||
/**
|
||||
* Optional cells that were changed
|
||||
*/
|
||||
cells?: ICellModel | ICellModel[];
|
||||
/**
|
||||
* Optional index of the change, indicating the cell at which an insert or
|
||||
* delete occurred
|
||||
*/
|
||||
cellIndex?: number;
|
||||
/**
|
||||
* Optional value indicating if the notebook is in a dirty or clean state after this change
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
|
||||
/**
|
||||
* Text content changed event for cell edits
|
||||
*/
|
||||
modelContentChangedEvent?: IModelContentChangedEvent;
|
||||
}
|
||||
|
||||
export interface ICellModelOptions {
|
||||
notebook: INotebookModel;
|
||||
isTrusted: boolean;
|
||||
}
|
||||
|
||||
export enum CellExecutionState {
|
||||
Hidden = 0,
|
||||
Stopped = 1,
|
||||
Running = 2,
|
||||
Error = 3
|
||||
}
|
||||
|
||||
export interface IOutputChangedEvent {
|
||||
outputs: ReadonlyArray<nb.ICellOutput>;
|
||||
shouldScroll: boolean;
|
||||
}
|
||||
|
||||
export interface ICellModel {
|
||||
cellUri: URI;
|
||||
id: string;
|
||||
readonly language: string;
|
||||
readonly cellGuid: string;
|
||||
source: string | string[];
|
||||
cellType: CellType;
|
||||
trustedMode: boolean;
|
||||
active: boolean;
|
||||
hover: boolean;
|
||||
executionCount: number | undefined;
|
||||
readonly future: FutureInternal;
|
||||
readonly outputs: ReadonlyArray<nb.ICellOutput>;
|
||||
readonly onOutputsChanged: Event<IOutputChangedEvent>;
|
||||
readonly onExecutionStateChange: Event<CellExecutionState>;
|
||||
readonly executionState: CellExecutionState;
|
||||
readonly notebookModel: NotebookModel;
|
||||
setFuture(future: FutureInternal): void;
|
||||
setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void;
|
||||
runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean>;
|
||||
setOverrideLanguage(language: string);
|
||||
equals(cellModel: ICellModel): boolean;
|
||||
toJSON(): nb.ICellContents;
|
||||
loaded: boolean;
|
||||
stdInVisible: boolean;
|
||||
readonly onLoaded: Event<string>;
|
||||
isCollapsed: boolean;
|
||||
readonly onCollapseStateChanged: Event<boolean>;
|
||||
modelContentChangedEvent: IModelContentChangedEvent;
|
||||
}
|
||||
|
||||
export interface FutureInternal extends nb.IFuture {
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
export interface IModelFactory {
|
||||
|
||||
createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel;
|
||||
createClientSession(options: IClientSessionOptions): IClientSession;
|
||||
}
|
||||
|
||||
export interface IContentManager {
|
||||
/**
|
||||
* This is a specialized method intended to load for a default context - just the current Notebook's URI
|
||||
*/
|
||||
loadContent(): Promise<nb.INotebookContents>;
|
||||
}
|
||||
|
||||
export interface INotebookModelOptions {
|
||||
/**
|
||||
* Path to the local or remote notebook
|
||||
*/
|
||||
notebookUri: URI;
|
||||
|
||||
/**
|
||||
* Factory for creating cells and client sessions
|
||||
*/
|
||||
factory: IModelFactory;
|
||||
|
||||
contentManager: IContentManager;
|
||||
notebookManagers: INotebookManager[];
|
||||
providerId: string;
|
||||
defaultKernel: nb.IKernelSpec;
|
||||
cellMagicMapper: ICellMagicMapper;
|
||||
|
||||
layoutChanged: Event<void>;
|
||||
|
||||
notificationService: INotificationService;
|
||||
connectionService: IConnectionManagementService;
|
||||
capabilitiesService: ICapabilitiesService;
|
||||
editorLoadedTimestamp?: number;
|
||||
}
|
||||
|
||||
export interface ICellMagicMapper {
|
||||
/**
|
||||
* Tries to find a language mapping for an identified cell magic
|
||||
* @param magic a string defining magic. For example for %%sql the magic text is sql
|
||||
* @param kernelId the name of the current kernel to use when looking up magics
|
||||
*/
|
||||
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic | undefined;
|
||||
}
|
||||
|
||||
export namespace notebookConstants {
|
||||
export const SQL = 'SQL';
|
||||
export const SQL_CONNECTION_PROVIDER = mssqlProviderName;
|
||||
export const sqlKernel: string = localize('sqlKernel', "SQL");
|
||||
export const sqlKernelSpec: nb.IKernelSpec = ({
|
||||
name: sqlKernel,
|
||||
language: 'sql',
|
||||
display_name: sqlKernel
|
||||
});
|
||||
}
|
||||
|
||||
export interface INotebookContentsEditable {
|
||||
cells: nb.ICellContents[];
|
||||
metadata: nb.INotebookMetadata;
|
||||
nbformat: number;
|
||||
nbformat_minor: number;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { localize } from 'vs/nls';
|
||||
import { IDefaultConnection, notebookConstants } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
export class NotebookContexts {
|
||||
|
||||
private static get DefaultContext(): IDefaultConnection {
|
||||
let defaultConnection: ConnectionProfile = <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('selectConnection', "Select Connection")
|
||||
};
|
||||
|
||||
return {
|
||||
// default context if no other contexts are applicable
|
||||
defaultConnection: defaultConnection,
|
||||
otherConnections: [defaultConnection]
|
||||
};
|
||||
}
|
||||
|
||||
private static get LocalContext(): IDefaultConnection {
|
||||
let localConnection: ConnectionProfile = <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('localhost', "localhost")
|
||||
};
|
||||
|
||||
return {
|
||||
// default context if no other contexts are applicable
|
||||
defaultConnection: localConnection,
|
||||
otherConnections: [localConnection]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the applicable contexts for a given kernel
|
||||
* @param connectionService connection management service
|
||||
* @param connProviderIds array of connection provider ids applicable for a kernel
|
||||
* @param kernelChangedArgs kernel changed args (both old and new kernel info)
|
||||
* @param profile current connection profile
|
||||
*/
|
||||
public static getContextsForKernel(connectionService: IConnectionManagementService, connProviderIds: string[], kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): IDefaultConnection {
|
||||
let connections: IDefaultConnection = this.DefaultContext;
|
||||
if (!profile) {
|
||||
if (!kernelChangedArgs || !kernelChangedArgs.newValue ||
|
||||
(kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) {
|
||||
// nothing to do, kernels are the same or new kernel is undefined
|
||||
return connections;
|
||||
}
|
||||
}
|
||||
if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name && connProviderIds.length < 1) {
|
||||
return connections;
|
||||
} else {
|
||||
connections = this.getActiveContexts(connectionService, connProviderIds, profile);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active contexts and sort them
|
||||
* @param connectionService connection service
|
||||
* @param connProviderIds array of applicable connection providers to filter connections
|
||||
* @param profile connection profile passed when launching notebook
|
||||
*/
|
||||
public static getActiveContexts(connectionService: IConnectionManagementService, connProviderIds: string[], profile: IConnectionProfile): IDefaultConnection {
|
||||
let defaultConnection: ConnectionProfile = NotebookContexts.DefaultContext.defaultConnection;
|
||||
let activeConnections: ConnectionProfile[] = connectionService.getActiveConnections();
|
||||
if (activeConnections && activeConnections.length > 0) {
|
||||
activeConnections = activeConnections.filter(conn => conn.id !== '-1');
|
||||
}
|
||||
// If no connection provider ids exist for a given kernel, the attach to should show localhost
|
||||
if (connProviderIds.length === 0) {
|
||||
return NotebookContexts.LocalContext;
|
||||
}
|
||||
// If no active connections exist, show "Select connection" as the default value
|
||||
if (activeConnections.length === 0) {
|
||||
return NotebookContexts.DefaultContext;
|
||||
}
|
||||
// Filter active connections by their provider ids to match kernel's supported connection providers
|
||||
else if (activeConnections.length > 0) {
|
||||
let connections = activeConnections.filter(connection => {
|
||||
return connProviderIds.some(x => x === connection.providerName);
|
||||
});
|
||||
if (connections && connections.length > 0) {
|
||||
defaultConnection = connections[0];
|
||||
if (profile && profile.options) {
|
||||
if (find(connections, connection => connection.serverName === profile.serverName)) {
|
||||
defaultConnection = find(connections, connection => connection.serverName === profile.serverName);
|
||||
}
|
||||
}
|
||||
} else if (connections.length === 0) {
|
||||
return NotebookContexts.DefaultContext;
|
||||
}
|
||||
activeConnections = [];
|
||||
connections.forEach(connection => activeConnections.push(connection));
|
||||
}
|
||||
if (defaultConnection === NotebookContexts.DefaultContext.defaultConnection) {
|
||||
let newConnection = <ConnectionProfile><any>{
|
||||
providerName: 'SQL',
|
||||
id: '-2',
|
||||
serverName: localize('addConnection', "Add New Connection")
|
||||
};
|
||||
activeConnections.push(newConnection);
|
||||
}
|
||||
|
||||
return {
|
||||
otherConnections: activeConnections,
|
||||
defaultConnection: defaultConnection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param specs kernel specs (comes from session manager)
|
||||
* @param displayName kernel info loaded from
|
||||
*/
|
||||
public static getDefaultKernel(specs: nb.IAllKernels, displayName: string): nb.IKernelSpec {
|
||||
let defaultKernel: nb.IKernelSpec;
|
||||
if (specs) {
|
||||
// find the saved kernel (if it exists)
|
||||
if (displayName) {
|
||||
defaultKernel = find(specs.kernels, (kernel) => kernel.display_name === displayName);
|
||||
}
|
||||
// if no saved kernel exists, use the default KernelSpec
|
||||
if (!defaultKernel) {
|
||||
defaultKernel = find(specs.kernels, (kernel) => kernel.name === specs.defaultKernel);
|
||||
}
|
||||
if (defaultKernel) {
|
||||
return defaultKernel;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default kernel specified (should never happen), default to SQL
|
||||
return notebookConstants.sqlKernelSpec;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEditorModel } from 'vs/platform/editor/common/editor';
|
||||
import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/contrib/notebook/browser/models/notebookUtils';
|
||||
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { ITextFileService, ISaveOptions, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { NotebookTextFileModel } from 'sql/workbench/contrib/notebook/browser/models/notebookTextFileModel';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
|
||||
|
||||
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
|
||||
|
||||
export class NotebookEditorModel extends EditorModel {
|
||||
private _dirty: boolean;
|
||||
private _changeEventsHookedUp: boolean = false;
|
||||
private _notebookTextFileModel: NotebookTextFileModel;
|
||||
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
|
||||
private _lastEditFullReplacement: boolean;
|
||||
constructor(public readonly notebookUri: URI,
|
||||
private textEditorModel: TextFileEditorModel | UntitledEditorModel | ResourceEditorModel,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
@ITextFileService private textFileService: ITextFileService,
|
||||
@ITextResourcePropertiesService private textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
super();
|
||||
let _eol = this.textResourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled }));
|
||||
this._notebookTextFileModel = new NotebookTextFileModel(_eol);
|
||||
this._register(this.notebookService.onNotebookEditorAdd(notebook => {
|
||||
if (notebook.id === this.notebookUri.toString()) {
|
||||
// Hook to content change events
|
||||
notebook.modelReady.then((model) => {
|
||||
if (!this._changeEventsHookedUp) {
|
||||
this._changeEventsHookedUp = true;
|
||||
this._register(model.kernelChanged(e => this.updateModel(undefined, NotebookChangeType.KernelChanged)));
|
||||
this._register(model.contentChanged(e => this.updateModel(e, e.changeType)));
|
||||
this._register(notebook.model.onActiveCellChanged((cell) => {
|
||||
if (cell) {
|
||||
this._notebookTextFileModel.activeCellGuid = cell.cellGuid;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, err => undefined);
|
||||
}
|
||||
}));
|
||||
if (this.textEditorModel instanceof UntitledEditorModel) {
|
||||
this._register(this.textEditorModel.onDidChangeDirty(e => {
|
||||
let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
|
||||
this.setDirty(dirty);
|
||||
}));
|
||||
} else {
|
||||
if (this.textEditorModel instanceof TextFileEditorModel) {
|
||||
this._register(this.textEditorModel.onDidStateChange(change => {
|
||||
let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
|
||||
this.setDirty(dirty);
|
||||
if (change === StateChange.SAVED) {
|
||||
this.sendNotebookSerializationStateChange();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
this._dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
|
||||
}
|
||||
|
||||
public get contentString(): string {
|
||||
let model = this.textEditorModel.textEditorModel;
|
||||
return model.getValue();
|
||||
}
|
||||
|
||||
public get lastEditFullReplacement(): boolean {
|
||||
return this._lastEditFullReplacement;
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
|
||||
}
|
||||
|
||||
public setDirty(dirty: boolean): void {
|
||||
if (this._dirty === dirty) {
|
||||
return;
|
||||
}
|
||||
this._dirty = dirty;
|
||||
this._onDidChangeDirty.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* UntitledEditor uses TextFileService to save data from UntitledEditorInput
|
||||
* Titled editor uses TextFileEditorModel to save existing notebook
|
||||
*/
|
||||
save(options: ISaveOptions): Promise<boolean> {
|
||||
if (this.textEditorModel instanceof TextFileEditorModel) {
|
||||
this.textEditorModel.save(options);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
else {
|
||||
return this.textFileService.save(this.notebookUri, options);
|
||||
}
|
||||
}
|
||||
|
||||
public updateModel(contentChange?: NotebookContentChange, type?: NotebookChangeType): void {
|
||||
this._lastEditFullReplacement = false;
|
||||
if (contentChange && contentChange.changeType === NotebookChangeType.Saved) {
|
||||
// We send the saved events out, so ignore. Otherwise we double-count this as a change
|
||||
// and cause the text to be reapplied
|
||||
return;
|
||||
}
|
||||
if (contentChange && contentChange.changeType === NotebookChangeType.TrustChanged) {
|
||||
// This is a serializable change (in that we permanently cache trusted state, but
|
||||
// ironically isn't cached in the JSON contents since trust doesn't persist across machines.
|
||||
// Request serialization so trusted state is preserved but don't update the model
|
||||
this.sendNotebookSerializationStateChange();
|
||||
} else {
|
||||
let notebookModel = this.getNotebookModel();
|
||||
let editAppliedSuccessfully = false;
|
||||
if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) {
|
||||
if (contentChange && contentChange.cells && contentChange.cells[0]) {
|
||||
if (type === NotebookChangeType.CellSourceUpdated) {
|
||||
if (this._notebookTextFileModel.transformAndApplyEditForSourceUpdate(contentChange, this.textEditorModel)) {
|
||||
editAppliedSuccessfully = true;
|
||||
}
|
||||
} else if (type === NotebookChangeType.CellOutputUpdated) {
|
||||
if (this._notebookTextFileModel.transformAndApplyEditForOutputUpdate(contentChange, this.textEditorModel)) {
|
||||
editAppliedSuccessfully = true;
|
||||
}
|
||||
} else if (type === NotebookChangeType.CellOutputCleared) {
|
||||
if (this._notebookTextFileModel.transformAndApplyEditForClearOutput(contentChange, this.textEditorModel)) {
|
||||
editAppliedSuccessfully = true;
|
||||
}
|
||||
} else if (type === NotebookChangeType.CellExecuted) {
|
||||
if (this._notebookTextFileModel.transformAndApplyEditForCellUpdated(contentChange, this.textEditorModel)) {
|
||||
editAppliedSuccessfully = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If edit was already applied, skip replacing entire text model
|
||||
if (editAppliedSuccessfully) {
|
||||
return;
|
||||
}
|
||||
this.replaceEntireTextEditorModel(notebookModel, type);
|
||||
this._lastEditFullReplacement = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType) {
|
||||
this._notebookTextFileModel.replaceEntireTextEditorModel(notebookModel, type, this.textEditorModel);
|
||||
}
|
||||
|
||||
private sendNotebookSerializationStateChange(): void {
|
||||
let notebookModel = this.getNotebookModel();
|
||||
if (notebookModel) {
|
||||
this.notebookService.serializeNotebookStateChange(this.notebookUri, NotebookChangeType.Saved);
|
||||
}
|
||||
}
|
||||
|
||||
isModelCreated(): boolean {
|
||||
return this.getNotebookModel() !== undefined;
|
||||
}
|
||||
|
||||
private getNotebookModel(): INotebookModel {
|
||||
let editor = this.notebookService.findNotebookEditor(this.notebookUri);
|
||||
if (editor) {
|
||||
return editor.model;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get onDidChangeDirty(): Event<void> {
|
||||
return this._onDidChangeDirty.event;
|
||||
}
|
||||
|
||||
get editorModel() {
|
||||
return this.textEditorModel;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookInput extends EditorInput {
|
||||
public static ID: string = 'workbench.editorinputs.notebookInput';
|
||||
private _providerId: string;
|
||||
private _providers: string[];
|
||||
private _standardKernels: IStandardKernelWithProvider[];
|
||||
private _connectionProfile: IConnectionProfile;
|
||||
private _defaultKernel: azdata.nb.IKernelSpec;
|
||||
public hasBootstrapped = false;
|
||||
// Holds the HTML content for the editor when the editor discards this input and loads another
|
||||
private _parentContainer: HTMLElement;
|
||||
private readonly _layoutChanged: Emitter<void> = this._register(new Emitter<void>());
|
||||
private _model: NotebookEditorModel;
|
||||
private _untitledEditorModel: UntitledEditorModel;
|
||||
private _contentManager: IContentManager;
|
||||
private _providersLoaded: Promise<void>;
|
||||
private _dirtyListener: IDisposable;
|
||||
private _notebookEditorOpenedTimestamp: number;
|
||||
private _modelResolveInProgress: boolean = false;
|
||||
private _modelResolved: Deferred<void> = new Deferred<void>();
|
||||
|
||||
constructor(private _title: string,
|
||||
private resource: URI,
|
||||
private _textInput: UntitledEditorInput,
|
||||
@ITextModelService private textModelService: ITextModelService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
@IExtensionService private extensionService: IExtensionService
|
||||
) {
|
||||
super();
|
||||
this.resource = resource;
|
||||
this._standardKernels = [];
|
||||
this._providersLoaded = this.assignProviders();
|
||||
this._notebookEditorOpenedTimestamp = Date.now();
|
||||
if (this._textInput) {
|
||||
this.hookDirtyListener(this._textInput.onDidChangeDirty, () => this._onDidChangeDirty.fire());
|
||||
}
|
||||
}
|
||||
|
||||
public get textInput(): UntitledEditorInput {
|
||||
return this._textInput;
|
||||
}
|
||||
|
||||
public confirmSave(): Promise<ConfirmResult> {
|
||||
return this._textInput.confirmSave();
|
||||
}
|
||||
|
||||
public revert(): Promise<boolean> {
|
||||
return this._textInput.revert();
|
||||
}
|
||||
|
||||
public get notebookUri(): URI {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
public get contentManager(): IContentManager {
|
||||
if (!this._contentManager) {
|
||||
this._contentManager = this.instantiationService.createInstance(NotebookEditorContentManager, this);
|
||||
}
|
||||
return this._contentManager;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
if (!this._title) {
|
||||
this._title = resources.basenameOrAuthority(this.resource);
|
||||
}
|
||||
return this._title;
|
||||
}
|
||||
|
||||
public async getProviderInfo(): Promise<IProviderInfo> {
|
||||
await this._providersLoaded;
|
||||
return {
|
||||
providerId: this._providerId ? this._providerId : DEFAULT_NOTEBOOK_PROVIDER,
|
||||
providers: this._providers ? this._providers : [DEFAULT_NOTEBOOK_PROVIDER]
|
||||
};
|
||||
}
|
||||
|
||||
public set connectionProfile(value: IConnectionProfile) {
|
||||
this._connectionProfile = value;
|
||||
}
|
||||
|
||||
public get connectionProfile(): IConnectionProfile {
|
||||
return this._connectionProfile;
|
||||
}
|
||||
|
||||
public get standardKernels(): IStandardKernelWithProvider[] {
|
||||
return this._standardKernels;
|
||||
}
|
||||
|
||||
public save(): Promise<boolean> {
|
||||
let options: ISaveOptions = { force: false };
|
||||
return this._model.save(options);
|
||||
}
|
||||
|
||||
public set standardKernels(value: IStandardKernelWithProvider[]) {
|
||||
value.forEach(kernel => {
|
||||
this._standardKernels.push({
|
||||
connectionProviderIds: kernel.connectionProviderIds,
|
||||
name: kernel.name,
|
||||
displayName: kernel.displayName,
|
||||
notebookProvider: kernel.notebookProvider
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public get defaultKernel(): azdata.nb.IKernelSpec {
|
||||
return this._defaultKernel;
|
||||
}
|
||||
|
||||
public set defaultKernel(kernel: azdata.nb.IKernelSpec) {
|
||||
this._defaultKernel = kernel;
|
||||
}
|
||||
|
||||
get layoutChanged(): Event<void> {
|
||||
return this._layoutChanged.event;
|
||||
}
|
||||
|
||||
public get editorOpenedTimestamp(): number {
|
||||
return this._notebookEditorOpenedTimestamp;
|
||||
}
|
||||
|
||||
doChangeLayout(): any {
|
||||
this._layoutChanged.fire();
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return NotebookInput.ID;
|
||||
}
|
||||
|
||||
getResource(): URI {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
public get untitledEditorModel(): UntitledEditorModel {
|
||||
return this._untitledEditorModel;
|
||||
}
|
||||
|
||||
public set untitledEditorModel(value: UntitledEditorModel) {
|
||||
this._untitledEditorModel = value;
|
||||
}
|
||||
|
||||
async resolve(): Promise<NotebookEditorModel> {
|
||||
if (!this._modelResolveInProgress) {
|
||||
this._modelResolveInProgress = true;
|
||||
} else {
|
||||
await this._modelResolved;
|
||||
return this._model;
|
||||
}
|
||||
if (this._model) {
|
||||
return Promise.resolve(this._model);
|
||||
} else {
|
||||
let textOrUntitledEditorModel: UntitledEditorModel | IEditorModel;
|
||||
if (this.resource.scheme === Schemas.untitled) {
|
||||
if (this._untitledEditorModel) {
|
||||
this._untitledEditorModel.textEditorModel.onBeforeAttached();
|
||||
textOrUntitledEditorModel = this._untitledEditorModel;
|
||||
} else {
|
||||
let resolvedInput = await this._textInput.resolve();
|
||||
resolvedInput.textEditorModel.onBeforeAttached();
|
||||
textOrUntitledEditorModel = resolvedInput;
|
||||
}
|
||||
} else {
|
||||
const textEditorModelReference = await this.textModelService.createModelReference(this.resource);
|
||||
textEditorModelReference.object.textEditorModel.onBeforeAttached();
|
||||
textOrUntitledEditorModel = await textEditorModelReference.object.load();
|
||||
}
|
||||
this._model = this._register(this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel));
|
||||
this.hookDirtyListener(this._model.onDidChangeDirty, () => this._onDidChangeDirty.fire());
|
||||
this._modelResolved.resolve();
|
||||
return this._model;
|
||||
}
|
||||
}
|
||||
|
||||
private hookDirtyListener(dirtyEvent: Event<void>, listener: (e: any) => void): void {
|
||||
let disposable = dirtyEvent(listener);
|
||||
if (this._dirtyListener) {
|
||||
this._dirtyListener.dispose();
|
||||
} else {
|
||||
this._register({
|
||||
dispose: () => {
|
||||
if (this._dirtyListener) {
|
||||
this._dirtyListener.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this._dirtyListener = disposable;
|
||||
}
|
||||
|
||||
private async assignProviders(): Promise<void> {
|
||||
await this.extensionService.whenInstalledExtensionsRegistered();
|
||||
let providerIds: string[] = getProvidersForFileName(this._title, this.notebookService);
|
||||
if (providerIds && providerIds.length > 0) {
|
||||
this._providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0];
|
||||
this._providers = providerIds;
|
||||
this._standardKernels = [];
|
||||
this._providers.forEach(provider => {
|
||||
let standardKernels = getStandardKernelsForProvider(provider, this.notebookService);
|
||||
this._standardKernels.push(...standardKernels);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._model && this._model.editorModel && this._model.editorModel.textEditorModel) {
|
||||
this._model.editorModel.textEditorModel.onBeforeDetached();
|
||||
}
|
||||
this._disposeContainer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _disposeContainer() {
|
||||
if (!this._parentContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentNode = this._parentContainer.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.removeChild(this._parentContainer);
|
||||
this._parentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
set container(container: HTMLElement) {
|
||||
this._disposeContainer();
|
||||
this._parentContainer = container;
|
||||
}
|
||||
|
||||
get container(): HTMLElement {
|
||||
return this._parentContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* An editor that is dirty will be asked to be saved once it closes.
|
||||
*/
|
||||
isDirty(): boolean {
|
||||
if (this._model) {
|
||||
return this._model.isDirty();
|
||||
} else if (this._textInput) {
|
||||
return this._textInput.isDirty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets active editor with dirty value.
|
||||
* @param isDirty boolean value to set editor dirty
|
||||
*/
|
||||
setDirty(isDirty: boolean): void {
|
||||
if (this._model) {
|
||||
this._model.setDirty(isDirty);
|
||||
}
|
||||
}
|
||||
|
||||
updateModel(): void {
|
||||
this._model.updateModel();
|
||||
}
|
||||
|
||||
public matches(otherInput: any): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (otherInput instanceof NotebookInput) {
|
||||
const otherNotebookEditorInput = <NotebookInput>otherInput;
|
||||
|
||||
// Compare by resource
|
||||
return otherNotebookEditorInput.notebookUri.toString() === this.notebookUri.toString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class NotebookEditorContentManager implements IContentManager {
|
||||
constructor(
|
||||
private notebookInput: NotebookInput,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService) {
|
||||
}
|
||||
|
||||
async loadContent(): Promise<azdata.nb.INotebookContents> {
|
||||
let notebookEditorModel = await this.notebookInput.resolve();
|
||||
let contentManager = this.instantiationService.createInstance(LocalContentManager);
|
||||
let contents = await contentManager.loadFromContentString(notebookEditorModel.contentString);
|
||||
return contents;
|
||||
}
|
||||
|
||||
}
|
||||
1031
src/sql/workbench/contrib/notebook/browser/models/notebookModel.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { FindMatch } from 'vs/editor/common/model';
|
||||
import { NotebookContentChange, INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
|
||||
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
|
||||
import { repeat } from 'vs/base/common/strings';
|
||||
|
||||
export class NotebookTextFileModel {
|
||||
// save active cell's line/column in editor model for the beginning of the source property
|
||||
private _sourceBeginRange: Range;
|
||||
// save active cell's line/column in editor model for the beginning of the output property
|
||||
private _outputBeginRange: Range;
|
||||
// save active cell guid
|
||||
private _activeCellGuid: string;
|
||||
|
||||
constructor(private _eol: string) {
|
||||
}
|
||||
|
||||
public get activeCellGuid(): string {
|
||||
return this._activeCellGuid;
|
||||
}
|
||||
|
||||
public set activeCellGuid(guid: string) {
|
||||
if (this._activeCellGuid !== guid) {
|
||||
this._sourceBeginRange = undefined;
|
||||
this._outputBeginRange = undefined;
|
||||
this._activeCellGuid = guid;
|
||||
}
|
||||
}
|
||||
|
||||
public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
|
||||
let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
|
||||
// convert the range to leverage offsets in the json
|
||||
if (contentChange && contentChange.modelContentChangedEvent && areRangePropertiesPopulated(cellGuidRange)) {
|
||||
contentChange.modelContentChangedEvent.changes.forEach(change => {
|
||||
// When writing to JSON we need to escape double quotes and backslashes
|
||||
let textEscapedQuotesAndBackslashes = change.text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
|
||||
let startLineNumber = change.range.startLineNumber + cellGuidRange.startLineNumber - 1;
|
||||
let endLineNumber = change.range.endLineNumber + cellGuidRange.startLineNumber - 1;
|
||||
let startLineText = textEditorModel.textEditorModel.getLineContent(startLineNumber);
|
||||
let endLineText = textEditorModel.textEditorModel.getLineContent(endLineNumber);
|
||||
|
||||
/* This gets the text on the start and end lines of the range where we'll be inserting text. We need to convert the escaped strings to unescaped strings.
|
||||
Example:
|
||||
|
||||
Previous state
|
||||
EDITOR:
|
||||
""""
|
||||
|
||||
TEXTEDITORMODEL:
|
||||
' "\"\"\"\""'
|
||||
|
||||
Now, user wants to insert text after the 4 double quotes, like so:
|
||||
EDITOR:
|
||||
""""sample text
|
||||
|
||||
TEXTEDITORMODEL (result):
|
||||
' "\"\"\"\"sample text"'
|
||||
|
||||
Notice that we don't have a 1:1 mapping for characters from the editor to the text editor model, because the double quotes need to be escaped
|
||||
(the same is true for backslashes).
|
||||
|
||||
Therefore, we need to determine (at both the start and end lines) the "real" start and end columns in the text editor model by counting escaped characters.
|
||||
|
||||
We do this by doing the following:
|
||||
- Start with (escaped) text in the text editor model
|
||||
- Unescape this text
|
||||
- Get a substring of that text from the column in JSON until the change's startColumn (starting from the first " in the text editor model)
|
||||
- Escape this substring
|
||||
- Leverage the substring's length to calculate the "real" start/end columns
|
||||
*/
|
||||
let unescapedStartSubstring = startLineText.replace(/\\"/g, '"').replace(/\\\\/g, '\\').substr(cellGuidRange.startColumn, change.range.startColumn - 1);
|
||||
let unescapedEndSubstring = endLineText.replace(/\\"/g, '"').replace(/\\\\/g, '\\').substr(cellGuidRange.startColumn, change.range.endColumn - 1);
|
||||
|
||||
// now we have the portion before the text to be inserted for both the start and end lines
|
||||
// so next step is to escape " and \
|
||||
|
||||
let escapedStartSubstring = unescapedStartSubstring.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
let escapedEndSubstring = unescapedEndSubstring.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
|
||||
let computedStartColumn = escapedStartSubstring.length + cellGuidRange.startColumn + 1;
|
||||
let computedEndColumn = escapedEndSubstring.length + cellGuidRange.startColumn + 1;
|
||||
|
||||
let convertedRange: IRange = {
|
||||
startLineNumber: startLineNumber,
|
||||
endLineNumber: endLineNumber,
|
||||
startColumn: computedStartColumn,
|
||||
endColumn: computedEndColumn
|
||||
};
|
||||
// Need to subtract one because we're going from 1-based to 0-based
|
||||
let startSpaces: string = repeat(' ', cellGuidRange.startColumn - 1);
|
||||
// The text here transforms a string from 'This is a string\n this is another string' to:
|
||||
// This is a string
|
||||
// this is another string
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(convertedRange.startLineNumber, convertedRange.startColumn, convertedRange.endLineNumber, convertedRange.endColumn),
|
||||
text: textEscapedQuotesAndBackslashes.split(/[\r\n]+/gm).join('\\n\",'.concat(this._eol).concat(startSpaces).concat('\"'))
|
||||
}]);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
|
||||
if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) {
|
||||
let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' ');
|
||||
if (contentChange.cells[0].outputs.length > 1) {
|
||||
newOutput = ', '.concat(newOutput);
|
||||
} else {
|
||||
newOutput = '\n'.concat(newOutput).concat('\n');
|
||||
}
|
||||
|
||||
// Execution count will always be after the end of the outputs in JSON. This is a sanity mechanism.
|
||||
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (!executionCountMatch || !executionCountMatch.range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let endOutputsRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (endOutputsRange && endOutputsRange.startLineNumber < executionCountMatch.range.startLineNumber) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(endOutputsRange.startLineNumber, endOutputsRange.startColumn, endOutputsRange.startLineNumber, endOutputsRange.startColumn),
|
||||
text: newOutput
|
||||
}]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
|
||||
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (executionCountMatch && executionCountMatch.range) {
|
||||
// Execution count can be between 0 and n characters long
|
||||
let beginExecutionCountColumn = executionCountMatch.range.endColumn;
|
||||
let endExecutionCountColumn = beginExecutionCountColumn + 1;
|
||||
let lineContent = textEditorModel.textEditorModel.getLineContent(executionCountMatch.range.endLineNumber);
|
||||
while (lineContent[endExecutionCountColumn - 1]) {
|
||||
endExecutionCountColumn++;
|
||||
}
|
||||
if (contentChange.cells[0].executionCount) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(executionCountMatch.range.startLineNumber, beginExecutionCountColumn, executionCountMatch.range.endLineNumber, endExecutionCountColumn),
|
||||
text: contentChange.cells[0].executionCount.toString()
|
||||
}]);
|
||||
} else {
|
||||
// This is a special case when cells are canceled; there will be no execution count included
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
|
||||
if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) {
|
||||
return false;
|
||||
}
|
||||
this.updateOutputBeginRange(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
let outputEndRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
let outputStartRange = this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (outputStartRange && outputEndRange) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(outputStartRange.startLineNumber, outputStartRange.endColumn, outputEndRange.endLineNumber, outputEndRange.endColumn),
|
||||
text: ''
|
||||
}]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: BaseTextEditorModel) {
|
||||
let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' ');
|
||||
let model = textEditorModel.textEditorModel;
|
||||
let endLine = model.getLineCount();
|
||||
let endCol = model.getLineMaxColumn(endLine);
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(1, 1, endLine, endCol),
|
||||
text: content
|
||||
}]);
|
||||
}
|
||||
|
||||
// Find the beginning of a cell's source in the text editor model
|
||||
private updateSourceBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void {
|
||||
if (!cellGuid) {
|
||||
return;
|
||||
}
|
||||
this._sourceBeginRange = undefined;
|
||||
|
||||
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
|
||||
if (cellGuidMatches && cellGuidMatches.length > 0) {
|
||||
let sourceBefore = textEditorModel.textEditorModel.findPreviousMatch('"source": [', { lineNumber: cellGuidMatches[0].range.startLineNumber, column: cellGuidMatches[0].range.startColumn }, false, true, undefined, true);
|
||||
if (!sourceBefore || !sourceBefore.range) {
|
||||
return;
|
||||
}
|
||||
let firstQuoteOfSource = textEditorModel.textEditorModel.findNextMatch('"', { lineNumber: sourceBefore.range.startLineNumber, column: sourceBefore.range.endColumn }, false, true, undefined, true);
|
||||
this._sourceBeginRange = firstQuoteOfSource.range;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the beginning of a cell's outputs in the text editor model
|
||||
private updateOutputBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void {
|
||||
if (!cellGuid) {
|
||||
return undefined;
|
||||
}
|
||||
this._outputBeginRange = undefined;
|
||||
|
||||
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
|
||||
if (cellGuidMatches && cellGuidMatches.length > 0) {
|
||||
let outputsBegin = textEditorModel.textEditorModel.findNextMatch('"outputs": [', { lineNumber: cellGuidMatches[0].range.endLineNumber, column: cellGuidMatches[0].range.endColumn }, false, true, undefined, true);
|
||||
if (!outputsBegin || !outputsBegin.range) {
|
||||
return undefined;
|
||||
}
|
||||
this._outputBeginRange = outputsBegin.range;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the end of a cell's outputs in the text editor model
|
||||
// This will be used as a starting point for any future outputs
|
||||
private getEndOfOutputs(textEditorModel: BaseTextEditorModel, cellGuid: string) {
|
||||
let outputsBegin;
|
||||
if (this._activeCellGuid === cellGuid) {
|
||||
outputsBegin = this._outputBeginRange;
|
||||
}
|
||||
if (!outputsBegin || !(textEditorModel.textEditorModel.getLineContent(outputsBegin.startLineNumber).trim().indexOf('output') > -1)) {
|
||||
this.updateOutputBeginRange(textEditorModel, cellGuid);
|
||||
outputsBegin = this._outputBeginRange;
|
||||
if (!outputsBegin) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
let outputsEnd = textEditorModel.textEditorModel.matchBracket({ column: outputsBegin.endColumn - 1, lineNumber: outputsBegin.endLineNumber });
|
||||
if (!outputsEnd || outputsEnd.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
// single line output [i.e. no outputs exist for a cell]
|
||||
if (outputsBegin.endLineNumber === outputsEnd[1].startLineNumber) {
|
||||
// Adding 1 to startColumn to replace text starting one character after '['
|
||||
return {
|
||||
startColumn: outputsEnd[0].startColumn + 1,
|
||||
startLineNumber: outputsEnd[0].startLineNumber,
|
||||
endColumn: outputsEnd[0].endColumn,
|
||||
endLineNumber: outputsEnd[0].endLineNumber
|
||||
};
|
||||
} else {
|
||||
// Last 2 lines in multi-line output will look like the following:
|
||||
// " }"
|
||||
// " ],"
|
||||
if (textEditorModel.textEditorModel.getLineContent(outputsEnd[1].endLineNumber - 1).trim() === '}') {
|
||||
return {
|
||||
startColumn: textEditorModel.textEditorModel.getLineFirstNonWhitespaceColumn(outputsEnd[1].endLineNumber - 1) + 1,
|
||||
startLineNumber: outputsEnd[1].endLineNumber - 1,
|
||||
endColumn: outputsEnd[1].endColumn - 1,
|
||||
endLineNumber: outputsEnd[1].endLineNumber
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what text needs to be replaced when execution counts are updated
|
||||
private getExecutionCountRange(textEditorModel: BaseTextEditorModel, cellGuid: string) {
|
||||
let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid);
|
||||
if (endOutputRange && endOutputRange.endLineNumber) {
|
||||
return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find a cell's location, given its cellGuid
|
||||
// If it doesn't exist (e.g. it's not the active cell), attempt to find it
|
||||
private getCellNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) {
|
||||
if (this._activeCellGuid !== guid || !this._sourceBeginRange) {
|
||||
this.updateSourceBeginRange(textEditorModel, guid);
|
||||
}
|
||||
return this._sourceBeginRange;
|
||||
}
|
||||
|
||||
private getOutputNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) {
|
||||
if (this._activeCellGuid !== guid) {
|
||||
this.updateOutputBeginRange(textEditorModel, guid);
|
||||
}
|
||||
return this._outputBeginRange;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function areRangePropertiesPopulated(range: Range) {
|
||||
return range && range.startLineNumber !== 0 && range.startColumn !== 0 && range.endLineNumber !== 0 && range.endColumn !== 0;
|
||||
}
|
||||
|
||||
function findOrSetCellGuidMatch(textEditorModel: BaseTextEditorModel, cellGuid: string): FindMatch[] {
|
||||
if (!textEditorModel || !cellGuid) {
|
||||
return undefined;
|
||||
}
|
||||
return textEditorModel.textEditorModel.findMatches(cellGuid, false, false, true, undefined, true);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'vs/base/common/path';
|
||||
import { nb, ServerInfo } from 'azdata';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
|
||||
export const clusterEndpointsProperty = 'clusterEndpoints';
|
||||
export const hadoopEndpointNameGateway = 'gateway';
|
||||
/**
|
||||
* Test whether an output is from a stream.
|
||||
*/
|
||||
export function isStream(output: nb.ICellOutput): output is nb.IStreamResult {
|
||||
return output.output_type === 'stream';
|
||||
}
|
||||
|
||||
export function getProvidersForFileName(fileName: string, notebookService: INotebookService): string[] {
|
||||
let fileExt = path.extname(fileName);
|
||||
let providers: string[];
|
||||
// First try to get provider for actual file type
|
||||
if (fileExt && startsWith(fileExt, '.')) {
|
||||
fileExt = fileExt.slice(1, fileExt.length);
|
||||
providers = notebookService.getProvidersForFileType(fileExt);
|
||||
}
|
||||
// Fallback to provider for default file type (assume this is a global handler)
|
||||
if (!providers) {
|
||||
providers = notebookService.getProvidersForFileType(DEFAULT_NOTEBOOK_FILETYPE);
|
||||
}
|
||||
// Finally if all else fails, use the built-in handler
|
||||
if (!providers) {
|
||||
providers = [DEFAULT_NOTEBOOK_PROVIDER];
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function getStandardKernelsForProvider(providerId: string, notebookService: INotebookService): IStandardKernelWithProvider[] {
|
||||
if (!providerId || !notebookService) {
|
||||
return [];
|
||||
}
|
||||
let standardKernels = notebookService.getStandardKernelsForProvider(providerId);
|
||||
standardKernels.forEach(kernel => {
|
||||
assign(<IStandardKernelWithProvider>kernel, {
|
||||
name: kernel.name,
|
||||
connectionProviderIds: kernel.connectionProviderIds,
|
||||
notebookProvider: providerId
|
||||
});
|
||||
});
|
||||
return <IStandardKernelWithProvider[]>(standardKernels);
|
||||
}
|
||||
|
||||
// In the Attach To dropdown, show the database name (if it exists) using the current connection
|
||||
// Example: myFakeServer (myDatabase)
|
||||
export function formatServerNameWithDatabaseNameForAttachTo(connectionProfile: ConnectionProfile): string {
|
||||
if (connectionProfile && connectionProfile.serverName) {
|
||||
return !connectionProfile.databaseName ? connectionProfile.serverName : connectionProfile.serverName + ' (' + connectionProfile.databaseName + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract server name from format used in Attach To: serverName (databaseName)
|
||||
export function getServerFromFormattedAttachToName(name: string): string {
|
||||
return name.substring(0, name.lastIndexOf(' (')) ? name.substring(0, name.lastIndexOf(' (')) : name;
|
||||
}
|
||||
|
||||
// Extract database name from format used in Attach To: serverName (databaseName)
|
||||
export function getDatabaseFromFormattedAttachToName(name: string): string {
|
||||
return name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) ?
|
||||
name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) : '';
|
||||
}
|
||||
|
||||
export interface IStandardKernelWithProvider {
|
||||
readonly name: string;
|
||||
readonly displayName: string;
|
||||
readonly connectionProviderIds: string[];
|
||||
readonly notebookProvider: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export function tryMatchCellMagic(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
let firstLine = input.trimLeft();
|
||||
let magicRegex = /^%%(\w+)/g;
|
||||
let match = magicRegex.exec(firstLine);
|
||||
let magicName = match && match[1];
|
||||
return magicName;
|
||||
}
|
||||
|
||||
export async function asyncForEach(array: any, callback: any): Promise<any> {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only replace vscode-resource with file when in the same (or a sub) directory
|
||||
* This matches Jupyter Notebook viewer behavior
|
||||
*/
|
||||
export function convertVscodeResourceToFileInSubDirectories(htmlContent: string, cellModel: ICellModel): string {
|
||||
let htmlContentCopy = htmlContent;
|
||||
while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
|
||||
let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
|
||||
let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
|
||||
let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
|
||||
// If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
|
||||
if (!(path.relative(path.dirname(cellModel.notebookModel.notebookUri.fsPath), filePath).indexOf('..') > -1)) {
|
||||
// ok to change from vscode-resource: to file:
|
||||
htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
|
||||
}
|
||||
htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
|
||||
}
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
export function useInProcMarkdown(configurationService: IConfigurationService): boolean {
|
||||
return configurationService.getValue('notebook.useInProcMarkdown');
|
||||
}
|
||||
|
||||
export function getClusterEndpoints(serverInfo: ServerInfo): IEndpoint[] | undefined {
|
||||
let endpoints: RawEndpoint[] = serverInfo.options[clusterEndpointsProperty];
|
||||
if (!endpoints || endpoints.length === 0) { return []; }
|
||||
|
||||
return endpoints.map(e => {
|
||||
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
|
||||
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
|
||||
let updatedEndpoint: IEndpoint = {
|
||||
serviceName: e.serviceName,
|
||||
description: e.description,
|
||||
endpoint: endpoint,
|
||||
protocol: e.protocol
|
||||
};
|
||||
return updatedEndpoint;
|
||||
});
|
||||
}
|
||||
|
||||
export type HostAndIp = { host: string, port: string };
|
||||
|
||||
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
|
||||
let authority = URI.parse(endpoint).authority;
|
||||
let hostAndPortRegex = /^(.*)([,:](\d+))/g;
|
||||
let match = hostAndPortRegex.exec(authority);
|
||||
if (match) {
|
||||
return {
|
||||
host: match[1],
|
||||
port: match[3]
|
||||
};
|
||||
}
|
||||
return {
|
||||
host: authority,
|
||||
port: undefined
|
||||
};
|
||||
}
|
||||
|
||||
interface RawEndpoint {
|
||||
serviceName: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
protocol?: string;
|
||||
ipAddress?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { JSONObject, isPrimitive } from '../../common/models/jsonext';
|
||||
import { MimeModel } from './mimemodel';
|
||||
import { nbformat } from '../../common/models/nbformat';
|
||||
import { nb } from 'azdata';
|
||||
|
||||
/**
|
||||
* A multiline string.
|
||||
*/
|
||||
export type MultilineString = string | string[];
|
||||
|
||||
/**
|
||||
* A mime-type keyed dictionary of data.
|
||||
*/
|
||||
export interface IMimeBundle extends JSONObject {
|
||||
[key: string]: MultilineString | JSONObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data from a notebook output.
|
||||
*/
|
||||
export function getData(output: nb.ICellOutput): JSONObject {
|
||||
let bundle: IMimeBundle = {};
|
||||
if (
|
||||
nbformat.isExecuteResult(output) ||
|
||||
nbformat.isDisplayData(output) ||
|
||||
nbformat.isDisplayUpdate(output)
|
||||
) {
|
||||
bundle = (output as nbformat.IExecuteResult).data;
|
||||
} else if (nbformat.isStream(output)) {
|
||||
if (output.name === 'stderr') {
|
||||
bundle['application/vnd.jupyter.stderr'] = output.text;
|
||||
} else {
|
||||
bundle['application/vnd.jupyter.stdout'] = output.text;
|
||||
}
|
||||
} else if (nbformat.isError(output)) {
|
||||
let traceback = output.traceback ? output.traceback.join('\n') : undefined;
|
||||
bundle['application/vnd.jupyter.stderr'] = undefined;
|
||||
if (traceback && traceback !== '') {
|
||||
bundle['application/vnd.jupyter.stderr'] = traceback;
|
||||
} else if (output.evalue) {
|
||||
bundle['application/vnd.jupyter.stderr'] = output.ename && output.ename !== '' ? `${output.ename}: ${output.evalue}` : `${output.evalue}`;
|
||||
}
|
||||
}
|
||||
return convertBundle(bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata from an output message.
|
||||
*/
|
||||
export function getMetadata(output: nbformat.IOutput): JSONObject {
|
||||
let value: JSONObject = Object.create(null);
|
||||
if (nbformat.isExecuteResult(output) || nbformat.isDisplayData(output)) {
|
||||
for (let key in output.metadata) {
|
||||
value[key] = extract(output.metadata, key);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bundle options given output model options.
|
||||
*/
|
||||
export function getBundleOptions(options: IOutputModelOptions): MimeModel.IOptions {
|
||||
let data = getData(options.value);
|
||||
let metadata = getMetadata(options.value);
|
||||
let trusted = !!options.trusted;
|
||||
return { data, metadata, trusted };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a value from a JSONObject.
|
||||
*/
|
||||
export function extract(value: JSONObject, key: string): {} {
|
||||
let item = value[key];
|
||||
if (isPrimitive(item)) {
|
||||
return item;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a mime bundle to mime data.
|
||||
*/
|
||||
function convertBundle(bundle: nbformat.IMimeBundle): JSONObject {
|
||||
let map: JSONObject = Object.create(null);
|
||||
for (let mimeType in bundle) {
|
||||
map[mimeType] = extract(bundle, mimeType);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to create a notebook output model.
|
||||
*/
|
||||
export interface IOutputModelOptions {
|
||||
/**
|
||||
* The raw output value.
|
||||
*/
|
||||
value: nbformat.IOutput;
|
||||
|
||||
/**
|
||||
* Whether the output is trusted. The default is false.
|
||||
*/
|
||||
trusted?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { ReadonlyJSONObject } from '../../common/models/jsonext';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
/**
|
||||
* A namespace for rendermime associated interfaces.
|
||||
*/
|
||||
export namespace IRenderMime {
|
||||
/**
|
||||
* A model for mime data.
|
||||
*/
|
||||
export interface IMimeModel {
|
||||
/**
|
||||
* Whether the data in the model is trusted.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* The data associated with the model.
|
||||
*/
|
||||
readonly data: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The metadata associated with the model.
|
||||
*/
|
||||
readonly metadata: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* Set the data associated with the model.
|
||||
*
|
||||
* #### Notes
|
||||
* Calling this function may trigger an asynchronous operation
|
||||
* that could cause the renderer to be rendered with a new model
|
||||
* containing the new data.
|
||||
*/
|
||||
setData(options: ISetDataOptions): void;
|
||||
|
||||
/**
|
||||
* Theme service used to react to theme change events
|
||||
*/
|
||||
readonly themeService: IThemeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to update a mime model.
|
||||
*/
|
||||
export interface ISetDataOptions {
|
||||
/**
|
||||
* The new data object.
|
||||
*/
|
||||
data?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The new metadata object.
|
||||
*/
|
||||
metadata?: ReadonlyJSONObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget which displays the contents of a mime model.
|
||||
*/
|
||||
export interface IRenderer {
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*
|
||||
* #### Notes
|
||||
* This method may be called multiple times during the lifetime
|
||||
* of the widget to update it if and when new data is available.
|
||||
*/
|
||||
renderModel(model: IRenderMime.IMimeModel): Promise<void>;
|
||||
|
||||
/**
|
||||
* Node to be updated by the renderer
|
||||
*/
|
||||
node: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a renderer factory.
|
||||
*/
|
||||
export interface IRendererFactory {
|
||||
/**
|
||||
* Whether the factory is a "safe" factory.
|
||||
*
|
||||
* #### Notes
|
||||
* A "safe" factory 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 factory.
|
||||
*/
|
||||
readonly mimeTypes: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The default rank of the factory. If not given, defaults to 100.
|
||||
*/
|
||||
readonly defaultRank?: number;
|
||||
|
||||
/**
|
||||
* Create a renderer which displays the mime data.
|
||||
*
|
||||
* @param options - The options used to render the data.
|
||||
*/
|
||||
createRenderer(options: IRendererOptions): IRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to create a renderer.
|
||||
*/
|
||||
export interface IRendererOptions {
|
||||
/**
|
||||
* The preferred mimeType to render.
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* The html sanitizer.
|
||||
*/
|
||||
sanitizer: ISanitizer;
|
||||
|
||||
/**
|
||||
* An optional url resolver.
|
||||
*/
|
||||
resolver?: IResolver | null;
|
||||
|
||||
/**
|
||||
* An optional link handler.
|
||||
*/
|
||||
linkHandler?: ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: ILatexTypesetter | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that handles html sanitization.
|
||||
*/
|
||||
export interface ISanitizer {
|
||||
/**
|
||||
* Sanitize an HTML string.
|
||||
*/
|
||||
sanitize(dirty: string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that handles links on a node.
|
||||
*/
|
||||
export interface ILinkHandler {
|
||||
/**
|
||||
* Add the link handler to the node.
|
||||
*
|
||||
* @param node: the node for which to handle the link.
|
||||
*
|
||||
* @param path: the path to open when the link is clicked.
|
||||
*
|
||||
* @param id: an optional element id to scroll to when the path is opened.
|
||||
*/
|
||||
handleLink(node: HTMLElement, path: string, id?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that resolves relative URLs.
|
||||
*/
|
||||
export interface IResolver {
|
||||
/**
|
||||
* Resolve a relative url to a correct server path.
|
||||
*/
|
||||
resolveUrl(url: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get the download url of a given absolute server path.
|
||||
*/
|
||||
getDownloadUrl(path: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Whether the URL should be handled by the resolver
|
||||
* or not.
|
||||
*
|
||||
* #### Notes
|
||||
* This is similar to the `isLocal` check in `URLExt`,
|
||||
* but can also perform additional checks on whether the
|
||||
* resolver should handle a given URL.
|
||||
*/
|
||||
isLocal?: (url: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a LaTeX typesetter.
|
||||
*/
|
||||
export interface ILatexTypesetter {
|
||||
/**
|
||||
* Typeset a DOM element.
|
||||
*
|
||||
* @param element - the DOM element to typeset. The typesetting may
|
||||
* happen synchronously or asynchronously.
|
||||
*
|
||||
* #### Notes
|
||||
* The application-wide rendermime object has a settable
|
||||
* `latexTypesetter` property which is used wherever LaTeX
|
||||
* typesetting is required. Extensions wishing to provide their
|
||||
* own typesetter may replace that on the global `lab.rendermime`.
|
||||
*/
|
||||
typeset(element: HTMLElement): void;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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="hoverButtonsContainer" *ngIf="(cells && cells.length > 0) && !isLoading">
|
||||
<span class="containerBackground"></span>
|
||||
<button class="hoverButton" (click)="addCell('code', 0, $event)">
|
||||
<div class="addCodeIcon"></div>
|
||||
<span>{{addCodeLabel}}</span>
|
||||
</button>
|
||||
<button class="hoverButton" (click)="addCell('markdown', 0, $event)">
|
||||
<div class="addTextIcon"></div>
|
||||
<span>{{addTextLabel}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngFor="let cell of cells">
|
||||
<div class="notebook-cell" (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="hoverButtonsContainer">
|
||||
<span class="containerBackground"></span>
|
||||
<button class="hoverButton" (click)="addCell('code', findCellIndex(cell) + 1, $event)">
|
||||
<div class="addCodeIcon"></div>
|
||||
<span>{{addCodeLabel}}</span>
|
||||
</button>
|
||||
<button class="hoverButton" (click)="addCell('markdown', findCellIndex(cell) + 1, $event)">
|
||||
<div class="addTextIcon"></div>
|
||||
<span>{{addTextLabel}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
694
src/sql/workbench/contrib/notebook/browser/notebook.component.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/contrib/notebook/common/models/contracts';
|
||||
import { ICellModel, IModelFactory, INotebookModel } from 'sql/workbench/contrib/notebook/browser/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/browser/notebookService';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelFactory';
|
||||
import * as notebookUtils from 'sql/workbench/contrib/notebook/browser/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, CollapseCellsAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
|
||||
import * as TaskUtilities from 'sql/workbench/browser/taskUtilities';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { CellMagicMapper } from 'sql/workbench/contrib/notebook/browser/models/cellMagicMapper';
|
||||
import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { CellModel } from 'sql/workbench/contrib/notebook/browser/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/workbench/services/bootstrap/common/bootstrapParams';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { find, firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
|
||||
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;
|
||||
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 : [];
|
||||
}
|
||||
|
||||
public get addCodeLabel(): string {
|
||||
return localize('addCodeLabel', "Add code");
|
||||
}
|
||||
|
||||
public get addTextLabel(): string {
|
||||
return localize('addTextLabel', "Add text");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
this.model.updateActiveCell(cell);
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
//Saves scrollTop value on scroll change
|
||||
public scrollHandler(event: Event) {
|
||||
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
|
||||
}
|
||||
|
||||
public unselectActiveCell() {
|
||||
this.model.updateActiveCell(undefined);
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
// Add cell based on cell type
|
||||
public addCell(cellType: CellType, index?: number, event?: Event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this._model.addCell(cellType, index);
|
||||
}
|
||||
|
||||
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();
|
||||
this.setContextKeyServiceWithProviderId(this._model.providerId);
|
||||
await this._model.startSession(this._model.notebookManager, undefined, true);
|
||||
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());
|
||||
this._register(model.onError((errInfo: INotification) => this.handleModelError(errInfo)));
|
||||
this._register(model.contentChanged((change) => this.handleContentChanged()));
|
||||
this._register(model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider)));
|
||||
this._register(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 = find(providers, 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() {
|
||||
// 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 firstIndex(this._model.cells, (cell) => cell.id === cellModel.id);
|
||||
}
|
||||
|
||||
private setViewInErrorState(error: any): any {
|
||||
// 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 collapseCellsAction = this.instantiationService.createInstance(CollapseCellsAction, 'notebook.collapseCells');
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbar.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
|
||||
this._actionBar.context = this;
|
||||
this._actionBar.setContent([
|
||||
{ action: addCodeCellButton },
|
||||
{ action: addTextCellButton },
|
||||
{ element: kernelContainer },
|
||||
{ element: attachToContainer },
|
||||
{ action: this._trustedAction },
|
||||
{ action: this._runAllCellsAction },
|
||||
{ action: clearResultsButton },
|
||||
{ action: collapseCellsAction }
|
||||
]);
|
||||
}
|
||||
|
||||
protected initNavSection(): void {
|
||||
this._navProvider = this.notebookService.getNavigationProvider(this._notebookParams.notebookUri);
|
||||
|
||||
if (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 = firstIndex(this._providerRelatedActions, 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 (firstIndex(this._model.cells, 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 = firstIndex(codeCells, c => c.id === startCell.id);
|
||||
}
|
||||
if (!isUndefinedOrNull(endCell)) {
|
||||
endIndex = firstIndex(codeCells, 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 (firstIndex(this._model.cells, 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 = find(this.getSectionElements(), 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,320 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
|
||||
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
|
||||
import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor';
|
||||
import { NewNotebookAction } from 'sql/workbench/contrib/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/contrib/notebook/browser/outputs/gridOutput.component';
|
||||
import { PlotlyOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/plotlyOutput.component';
|
||||
import { registerComponentType } from 'sql/workbench/contrib/notebook/browser/outputs/mimeRegistry';
|
||||
import { MimeRendererComponent } from 'sql/workbench/contrib/notebook/browser/outputs/mimeRenderer.component';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { NodeContextKey } from 'sql/workbench/contrib/dataExplorer/browser/nodeContext';
|
||||
import { MssqlNodeContext } from 'sql/workbench/contrib/dataExplorer/browser/mssqlNodeContext';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
|
||||
import { ConnectedContext } from 'azdata';
|
||||
import { TreeNodeContextKey } from 'sql/workbench/contrib/objectExplorer/common/treeNodeContextKey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ObjectExplorerActionsContext } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
|
||||
import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext';
|
||||
import { ManageActionContext } from 'sql/workbench/browser/actions';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { MarkdownOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/markdownOutput.component';
|
||||
import { registerCellComponent } from 'sql/platform/notebooks/common/outputRegistry';
|
||||
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
const DE_NEW_NOTEBOOK_COMMAND_ID = 'dataExplorer.newNotebook';
|
||||
// New Notebook
|
||||
CommandsRegistry.registerCommand({
|
||||
id: DE_NEW_NOTEBOOK_COMMAND_ID,
|
||||
handler: (accessor, args: TreeViewItemHandleArg) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
const connectedContext: ConnectedContext = { connectionProfile: args.$treeItem.payload };
|
||||
return instantiationService.createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run({ connectionProfile: connectedContext.connectionProfile, isConnectionNode: false, nodeInfo: undefined });
|
||||
}
|
||||
});
|
||||
|
||||
// New Notebook
|
||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||
group: '0_query',
|
||||
order: 3,
|
||||
command: {
|
||||
id: DE_NEW_NOTEBOOK_COMMAND_ID,
|
||||
title: localize('newNotebook', "New Notebook")
|
||||
},
|
||||
when: ContextKeyExpr.and(NodeContextKey.IsConnectable,
|
||||
MssqlNodeContext.IsDatabaseOrServer,
|
||||
MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName))
|
||||
});
|
||||
|
||||
const OE_NEW_NOTEBOOK_COMMAND_ID = 'objectExplorer.newNotebook';
|
||||
// New Notebook
|
||||
CommandsRegistry.registerCommand({
|
||||
id: OE_NEW_NOTEBOOK_COMMAND_ID,
|
||||
handler: (accessor, actionContext: ObjectExplorerActionsContext) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
return instantiationService.createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run(actionContext);
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, {
|
||||
group: '0_query',
|
||||
order: 3,
|
||||
command: {
|
||||
id: OE_NEW_NOTEBOOK_COMMAND_ID,
|
||||
title: localize('newQuery', "New Notebook")
|
||||
},
|
||||
when: ContextKeyExpr.or(ContextKeyExpr.and(TreeNodeContextKey.Status.notEqualsTo('Unavailable'), TreeNodeContextKey.NodeType.isEqualTo('Server')), ContextKeyExpr.and(TreeNodeContextKey.Status.notEqualsTo('Unavailable'), TreeNodeContextKey.NodeType.isEqualTo('Database')))
|
||||
});
|
||||
|
||||
const ExplorerNotebookActionID = 'explorer.notebook';
|
||||
CommandsRegistry.registerCommand(ExplorerNotebookActionID, (accessor, context: ManageActionContext) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
const connectedContext: ConnectedContext = { connectionProfile: context.profile };
|
||||
instantiationService.createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run({ connectionProfile: connectedContext.connectionProfile, isConnectionNode: false, nodeInfo: undefined });
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerWidgetContext, {
|
||||
command: {
|
||||
id: ExplorerNotebookActionID,
|
||||
title: NewNotebookAction.LABEL
|
||||
},
|
||||
when: ItemContextKey.ItemType.isEqualTo('database'),
|
||||
order: 1
|
||||
});
|
||||
|
||||
registerAction({
|
||||
id: 'workbench.action.setWorkspaceAndOpen',
|
||||
handler: async (accessor, options: { forceNewWindow: boolean, folderPath: URI }) => {
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
const workspaceEditingService = accessor.get(IWorkspaceEditingService);
|
||||
const hostService = accessor.get(IHostService);
|
||||
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 hostService.openWindow([{ folderUri: folders[0] }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
else {
|
||||
return hostService.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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).")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'notebook',
|
||||
'title': 'Notebook',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'notebook.sqlStopOnError': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('notebook.sqlStopOnError', "SQL kernel: stop Notebook execution when error occurs in a cell.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction({
|
||||
id: 'workbench.books.action.focusBooksExplorer',
|
||||
handler: async (accessor) => {
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
viewletService.openViewlet('workbench.view.extension.books-explorer', true);
|
||||
}
|
||||
});
|
||||
|
||||
/* *************** 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 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
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer component for Markdown.
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: ['text/markdown'],
|
||||
rank: 60,
|
||||
safe: true,
|
||||
ctor: MarkdownOutputComponent,
|
||||
selector: MarkdownOutputComponent.SELECTOR
|
||||
});
|
||||
|
||||
/**
|
||||
* A mime renderer for IPyWidgets
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'application/vnd.jupyter.widget-view',
|
||||
'application/vnd.jupyter.widget-view+json'
|
||||
],
|
||||
rank: 47,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
registerCellComponent(TextCellComponent);
|
||||
234
src/sql/workbench/contrib/notebook/browser/notebook.css
Normal file
@@ -0,0 +1,234 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.notebookEditor .editor-toolbar {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-cell {
|
||||
margin: 1px 20px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-info-label {
|
||||
padding-right: 5px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notebookEditor .actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
min-height: 21px;
|
||||
}
|
||||
|
||||
.notebookEditor .actions-container .action-item .notebook-button {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-left: 18px;
|
||||
background-size: 13px;
|
||||
margin-right: 20px;
|
||||
font-size: 13px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.notebookEditor .labelOnLeftContainer {
|
||||
min-width: 100px;
|
||||
max-width: 250px;
|
||||
margin-right: 20px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-add {
|
||||
background-image: url("./media/light/add.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-add,
|
||||
.hc-black .notebookEditor .notebook-button.icon-add {
|
||||
background-image: url("./media/dark/add_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-run-cells {
|
||||
background-image: url("./media/light/run_cells.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-run-cells,
|
||||
.hc-black .notebookEditor .notebook-button.icon-run-cells {
|
||||
background-image: url("./media/dark/run_cells_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-trusted {
|
||||
background-image: url("./media/light/trusted.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-trusted,
|
||||
.hc-black .notebookEditor .notebook-button.icon-trusted {
|
||||
background-image: url("./media/dark/trusted_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-notTrusted {
|
||||
background-image: url("./media/light/nottrusted.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-notTrusted,
|
||||
.hc-black .notebookEditor .notebook-button.icon-notTrusted {
|
||||
background-image: url("./media/dark/nottrusted_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-show-cells {
|
||||
background-image: url("./media/light/show_code.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-show-cells,
|
||||
.hc-black .notebookEditor .notebook-button.icon-show-cells {
|
||||
background-image: url("./media/dark/show_code_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-hide-cells {
|
||||
background-image: url("./media/light/hide_code.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-hide-cells,
|
||||
.hc-black .notebookEditor .notebook-button.icon-hide-cells {
|
||||
background-image: url("./media/dark/hide_code_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-clear-results {
|
||||
background-image: url("./media/light/clear_results.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-button.icon-clear-results,
|
||||
.hc-black .notebookEditor .notebook-button.icon-clear-results {
|
||||
background-image: url("./media/dark/clear_results_inverse.svg");
|
||||
}
|
||||
|
||||
.moreActions .action-label.codicon.toggle-more {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.moreActions.actionhidden {
|
||||
visibility: hidden
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-cellTable {
|
||||
margin-left: 20px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-cellTable .ui-widget-content.slick-row {
|
||||
border-left: 1px silver dotted;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-cellTable .slick-viewport {
|
||||
min-height: 39px;
|
||||
}
|
||||
|
||||
.monaco-workbench .notebook-action.new-notebook {
|
||||
background: url('./media/light/new_notebook.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .notebook-action.new-notebook,
|
||||
.hc-black .monaco-workbench .notebook-action.new-notebook {
|
||||
background: url('./media/dark/new_notebook_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.notebookEditor .book-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 25px;
|
||||
}
|
||||
|
||||
.notebookEditor .book-nav .dialog-message-button {
|
||||
min-width: 100px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0px 20px;
|
||||
margin: 1px 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer .containerBackground {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 1px;
|
||||
z-index: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer:hover .containerBackground {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer .hoverButton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 3px 2px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
z-index: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer:hover .hoverButton {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButton:active {
|
||||
transform:scale(1.05);
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButton .addCodeIcon,
|
||||
.notebookEditor .hoverButton .addTextIcon {
|
||||
display: inline-block;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-size: contain;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButton .addCodeIcon {
|
||||
background-image: url("./media/light/add_code.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .hoverButton .addCodeIcon,
|
||||
.hc-black .notebookEditor .hoverButton .addCodeIcon {
|
||||
background-image: url("./media/dark/add_code_inverse.svg");
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButton .addTextIcon {
|
||||
background-image: url("./media/light/add_text.svg");
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .hoverButton .addTextIcon,
|
||||
.hc-black .notebookEditor .hoverButton .addTextIcon {
|
||||
background-image: url("./media/dark/add_text_inverse.svg");
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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/contrib/dashboard/browser/core/componentHost.directive';
|
||||
import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { EditableDropDown } from 'sql/platform/browser/editableDropdown/editableDropdown.component';
|
||||
import { NotebookComponent } from 'sql/workbench/contrib/notebook/browser/notebook.component';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component';
|
||||
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
|
||||
import { OutputAreaComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/outputArea.component';
|
||||
import { OutputComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/output.component';
|
||||
import { StdInComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/stdin.component';
|
||||
import { PlaceholderCellComponent } from 'sql/workbench/contrib/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/contrib/notebook/browser/outputs/mimeRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { LinkHandlerDirective } from 'sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive';
|
||||
import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
|
||||
import { ICellComponenetRegistry, Extensions as OutputComponentExtensions } from 'sql/platform/notebooks/common/outputRegistry';
|
||||
import { CollapseComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/collapse.component';
|
||||
|
||||
const outputComponentRegistry = Registry.as<ICellComponenetRegistry>(OutputComponentExtensions.CellComponentContributions);
|
||||
|
||||
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
|
||||
let outputComponents = Registry.as<IMimeComponentRegistry>(Extensions.MimeComponentContribution).getAllCtors();
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
...outputComponentRegistry.getComponents(),
|
||||
Checkbox,
|
||||
SelectBox,
|
||||
EditableDropDown,
|
||||
InputBox,
|
||||
LoadingSpinner,
|
||||
CodeComponent,
|
||||
CodeCellComponent,
|
||||
PlaceholderCellComponent,
|
||||
NotebookComponent,
|
||||
ComponentHostDirective,
|
||||
OutputAreaComponent,
|
||||
OutputComponent,
|
||||
StdInComponent,
|
||||
CollapseComponent,
|
||||
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;
|
||||
};
|
||||
614
src/sql/workbench/contrib/notebook/browser/notebookActions.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/sessionManager';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/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/contrib/notebook/common/models/contracts';
|
||||
import { NotebookComponent } from 'sql/workbench/contrib/notebook/browser/notebook.component';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
|
||||
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
|
||||
import { find, firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
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 CollapseCellsAction extends ToggleableAction {
|
||||
private static readonly collapseCells = localize('collapseAllCells', "Collapse Cells");
|
||||
private static readonly expandCells = localize('expandAllCells', "Expand Cells");
|
||||
private static readonly baseClass = 'notebook-button';
|
||||
private static readonly collapseCssClass = 'icon-hide-cells';
|
||||
private static readonly expandCssClass = 'icon-show-cells';
|
||||
|
||||
constructor(id: string) {
|
||||
super(id, {
|
||||
baseClass: CollapseCellsAction.baseClass,
|
||||
toggleOnLabel: CollapseCellsAction.expandCells,
|
||||
toggleOnClass: CollapseCellsAction.expandCssClass,
|
||||
toggleOffLabel: CollapseCellsAction.collapseCells,
|
||||
toggleOffClass: CollapseCellsAction.collapseCssClass,
|
||||
isOn: false
|
||||
});
|
||||
}
|
||||
|
||||
public get isCollapsed(): boolean {
|
||||
return this.state.isOn;
|
||||
}
|
||||
public set isCollapsed(value: boolean) {
|
||||
this.toggle(value);
|
||||
}
|
||||
|
||||
public run(context: NotebookComponent): Promise<boolean> {
|
||||
let self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
self.isCollapsed = !self.isCollapsed;
|
||||
context.cells.forEach(cell => {
|
||||
cell.isCollapsed = self.isCollapsed;
|
||||
});
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class KernelsDropdown extends SelectBox {
|
||||
private model: NotebookModel;
|
||||
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>) {
|
||||
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 = firstIndex(kernels, 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 = find(this.model.specs.kernels, 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) {
|
||||
this.loadWithSelectConnection(connections);
|
||||
}
|
||||
else {
|
||||
if (connections.length === 1 && connections[0] === msgAddNewConnection) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
}
|
||||
else {
|
||||
if (!find(connections, x => x === msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
}
|
||||
this.setOptions(connections, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadWithSelectConnection(connections: string[]): void {
|
||||
if (connections && connections.length > 0) {
|
||||
if (!find(connections, x => x === msgSelectConnection)) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
}
|
||||
|
||||
if (!find(connections, x => x === msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
this.setOptions(connections, 0);
|
||||
}
|
||||
}
|
||||
|
||||
//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 find(this.model.contexts.otherConnections, (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 = firstIndex(attachToConnections, 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,
|
||||
@IObjectExplorerService private objectExplorerService: IObjectExplorerService
|
||||
) {
|
||||
super(id, label);
|
||||
this.class = 'notebook-action new-notebook';
|
||||
}
|
||||
|
||||
async run(context?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
let connProfile: azdata.IConnectionProfile;
|
||||
if (context && context.nodeInfo) {
|
||||
let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath);
|
||||
connProfile = TreeUpdateUtils.getConnectionProfile(node).toIConnectionProfile();
|
||||
} else if (context && context.connectionProfile) {
|
||||
connProfile = context.connectionProfile;
|
||||
}
|
||||
return this.commandService.executeCommand(NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID, { connectionProfile: connProfile });
|
||||
}
|
||||
|
||||
}
|
||||
104
src/sql/workbench/contrib/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/workbench/services/bootstrap/browser/bootstrapService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
|
||||
import { NotebookModule } from 'sql/workbench/contrib/notebook/browser/notebook.module';
|
||||
import { NOTEBOOK_SELECTOR } from 'sql/workbench/contrib/notebook/browser/notebook.component';
|
||||
import { INotebookParams } from 'sql/workbench/services/notebook/browser/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
|
||||
);
|
||||
}
|
||||
}
|
||||
326
src/sql/workbench/contrib/notebook/browser/notebookStyles.ts
Normal file
@@ -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!./notebook';
|
||||
|
||||
import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
|
||||
import { SIDE_BAR_BACKGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder, buttonForeground, editorBackground, lighten } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { BareResultsGridInfo, getBareResultsGridInfoStyles } from 'sql/workbench/contrib/query/browser/queryResultsEditor';
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import * as types from 'vs/base/common/types';
|
||||
|
||||
export function registerNotebookThemes(overrideEditorThemeSetting: boolean, configurationService: IConfigurationService): IDisposable {
|
||||
return registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
|
||||
|
||||
let lightBoxShadow = '0px 4px 6px 0px rgba(0, 0, 0, 0.14)';
|
||||
let darkBoxShadow = '0px 4px 6px 0px rgba(0, 0, 0, 1)';
|
||||
let addBorderToInactiveCodeCells = true;
|
||||
|
||||
// Book Navigation Buttons
|
||||
const buttonForegroundColor = theme.getColor(buttonForeground);
|
||||
const buttonBackgroundColor = theme.getColor(buttonBackground);
|
||||
|
||||
if (buttonForegroundColor && buttonBackgroundColor) {
|
||||
collector.addRule(`
|
||||
.notebookEditor .book-nav .dialog-message-button .monaco-text-button {
|
||||
border-color: ${buttonBackgroundColor} !important;
|
||||
background-color: ${buttonForegroundColor} !important;
|
||||
color: ${buttonBackgroundColor} !important;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
if (buttonBackgroundColor) {
|
||||
let lighterBackgroundColor = lighten(buttonBackgroundColor, 0.825)(theme);
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell.active {
|
||||
border-color: ${buttonBackgroundColor};
|
||||
border-width: 1px;
|
||||
}
|
||||
.notebookEditor .notebook-cell.active:hover {
|
||||
border-color: ${buttonBackgroundColor};
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButton {
|
||||
border-color: ${buttonBackgroundColor};
|
||||
}
|
||||
.notebookEditor .hoverButton:active,
|
||||
.notebookEditor .hoverButton:hover {
|
||||
background-color: ${buttonBackgroundColor};
|
||||
}
|
||||
.notebookEditor .hoverButton {
|
||||
color: ${buttonBackgroundColor};
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .hoverButton {
|
||||
border-color: ${lighterBackgroundColor};
|
||||
}
|
||||
.vs-dark .notebookEditor .hoverButton:active,
|
||||
.vs-dark .notebookEditor .hoverButton:hover {
|
||||
background-color: ${lighterBackgroundColor};
|
||||
}
|
||||
.vs-dark .notebookEditor .hoverButton {
|
||||
color: ${lighterBackgroundColor};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const backgroundColor = theme.getColor(editorBackground);
|
||||
if (backgroundColor) {
|
||||
collector.addRule(`
|
||||
.notebookEditor .hoverButton {
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
.notebookEditor .hoverButton:active,
|
||||
.notebookEditor .hoverButton:hover {
|
||||
color: ${backgroundColor};
|
||||
}
|
||||
.hc-black .notebookEditor .hoverButton:active,
|
||||
.hc-black .notebookEditor .hoverButton:hover {
|
||||
color: ${backgroundColor};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Box shadow handling
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell.active {
|
||||
box-shadow: ${lightBoxShadow};
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-cell.active {
|
||||
box-shadow: ${darkBoxShadow};
|
||||
}
|
||||
|
||||
.hc-black .notebookEditor .notebook-cell.active {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-cell:hover:not(.active) {
|
||||
box-shadow: ${lightBoxShadow};
|
||||
}
|
||||
|
||||
.vs-dark .notebookEditor .notebook-cell:hover:not(.active) {
|
||||
box-shadow: ${darkBoxShadow};
|
||||
}
|
||||
|
||||
.hc-black .notebookEditor .notebook-cell:hover:not(.active) {
|
||||
box-shadow: 0;
|
||||
}
|
||||
`);
|
||||
|
||||
const inactiveBorder = theme.getColor(SIDE_BAR_BACKGROUND);
|
||||
const sidebarColor = theme.getColor(SIDE_BAR_SECTION_HEADER_BACKGROUND);
|
||||
const notebookLineHighlight = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND);
|
||||
// Code editor style overrides - only applied if user chooses this as preferred option
|
||||
if (overrideEditorThemeSetting) {
|
||||
let lineHighlight = theme.getColor(editorLineHighlight);
|
||||
if (!lineHighlight || lineHighlight.isTransparent()) {
|
||||
// Use notebook color override
|
||||
lineHighlight = notebookLineHighlight;
|
||||
if (lineHighlight) {
|
||||
collector.addRule(`code-component .monaco-editor .view-overlays .current-line { background-color: ${lineHighlight}; border: 0px; }`);
|
||||
}
|
||||
} // else do nothing as current theme's line highlight will work
|
||||
|
||||
if (theme.defines(editorLineHighlightBorder) && theme.type !== 'hc') {
|
||||
// We need to clear out the border because we do not want to show it for notebooks
|
||||
// Override values only for the children of code-component so regular editors aren't affected
|
||||
collector.addRule(`code-component .monaco-editor .view-overlays .current-line { border: 0px; }`);
|
||||
}
|
||||
|
||||
// Override code editor background if color is defined
|
||||
let codeBackground = inactiveBorder; // theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND);
|
||||
if (codeBackground) {
|
||||
// Main background
|
||||
collector.addRule(`.notebook-cell:not(.active) code-component { background-color: ${codeBackground}; }`);
|
||||
collector.addRule(`
|
||||
.notebook-cell:not(.active) code-component .monaco-editor,
|
||||
.notebook-cell:not(.active) code-component .monaco-editor-background,
|
||||
.notebook-cell:not(.active) code-component .monaco-editor .inputarea.ime-input,
|
||||
.notebook-cell.active .hide-component-button:hover
|
||||
{
|
||||
background-color: ${codeBackground};
|
||||
}`);
|
||||
|
||||
// Margin background will be the same (may override some styles)
|
||||
collector.addRule(`.notebook-cell:not(.active) code-component .monaco-editor .margin { background-color: ${codeBackground}; }`);
|
||||
addBorderToInactiveCodeCells = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Inactive border
|
||||
if (inactiveBorder) {
|
||||
// Standard notebook cell behavior
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell {
|
||||
border-color: transparent;
|
||||
border-width: 1px;
|
||||
}
|
||||
.notebookEditor .notebook-cell.active {
|
||||
border-width: 1px;
|
||||
}
|
||||
.notebookEditor .notebook-cell:hover {
|
||||
border-color: ${inactiveBorder};
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.notebookEditor .hoverButtonsContainer .containerBackground {
|
||||
background-color: ${inactiveBorder};
|
||||
}
|
||||
`);
|
||||
|
||||
// Ensure there's always a line between editor and output
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell.active code-component {
|
||||
border-color: ${inactiveBorder};
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-style: solid;
|
||||
border-radius: 0;
|
||||
}
|
||||
`);
|
||||
|
||||
if (addBorderToInactiveCodeCells) {
|
||||
// Sets a border for the editor component if we don't have a custom line color for editor instead
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell code-component {
|
||||
border-color: ${inactiveBorder};
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
.notebookEditor .notebook-cell:hover code-component {
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar and cell outline toolbar color set only when active
|
||||
collector.addRule(`
|
||||
.notebook-cell.active code-component .toolbar {
|
||||
background-color: ${sidebarColor};
|
||||
}
|
||||
`);
|
||||
// Styling with Outline color (e.g. high contrast theme)
|
||||
const outline = theme.getColor(activeContrastBorder);
|
||||
const hcOutline = theme.getColor(contrastBorder);
|
||||
if (outline) {
|
||||
collector.addRule(`
|
||||
.hc-black .notebookEditor .notebook-cell:not(.active) code-component {
|
||||
border-color: ${hcOutline};
|
||||
border-width: 0px 0px 1px 0px;
|
||||
}
|
||||
.hc-black .notebookEditor .notebook-cell.active code-component {
|
||||
border-color: ${outline};
|
||||
border-width: 0px 0px 1px 0px;
|
||||
}
|
||||
.hc-black .notebookEditor .notebook-cell:not(.active) {
|
||||
outline-color: ${hcOutline};
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
}
|
||||
.notebookEditor .notebook-cell.active {
|
||||
outline-color: ${outline};
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
.hc-black .notebookEditor .hoverButton {
|
||||
color: ${hcOutline};
|
||||
}
|
||||
.hc-black .notebookEditor .hoverButton:not(:active) {
|
||||
border-color: ${hcOutline};
|
||||
}
|
||||
.hc-black .notebookEditor .hoverButton:active,
|
||||
.hc-black .notebookEditor .hoverButton:hover {
|
||||
background-color: ${hcOutline};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Styling for all links in notebooks
|
||||
const linkForeground = theme.getColor(textLinkForeground);
|
||||
if (linkForeground) {
|
||||
collector.addRule(`
|
||||
.notebookEditor .scrollable a {
|
||||
color: ${linkForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Styling for tables in notebooks
|
||||
const borderColor = theme.getColor(SIDE_BAR_BACKGROUND);
|
||||
if (borderColor) {
|
||||
collector.addRule(`
|
||||
.notebookEditor text-cell-component tbody tr:nth-child(odd) {
|
||||
background: ${borderColor};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Styling for markdown cells & links in notebooks.
|
||||
// This matches the values used by default in all web views
|
||||
if (linkForeground) {
|
||||
collector.addRule(`
|
||||
.notebookEditor a:link {
|
||||
color: ${linkForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
let activeForeground = theme.getColor(textLinkActiveForeground);
|
||||
if (activeForeground) {
|
||||
collector.addRule(`
|
||||
.notebookEditor a:hover {
|
||||
color: ${activeForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
let preformatForeground = theme.getColor(textPreformatForeground);
|
||||
if (preformatForeground) {
|
||||
collector.addRule(`
|
||||
.notebook-preview code {
|
||||
color: ${preformatForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
let blockQuoteBackground = theme.getColor(textBlockQuoteBackground);
|
||||
let blockQuoteBorder = theme.getColor(textBlockQuoteBorder);
|
||||
if (preformatForeground) {
|
||||
collector.addRule(`
|
||||
.notebookEditor blockquote {
|
||||
background: ${blockQuoteBackground};
|
||||
border-color: ${blockQuoteBorder};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Results grid options. Putting these here since query editor only adds them on query editor load.
|
||||
// We may want to remove from query editor as it can just live here and be loaded once, instead of once
|
||||
// per editor group which is inefficient
|
||||
let rawOptions = BareResultsGridInfo.createFromRawSettings(configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
|
||||
let cssRuleText = '';
|
||||
if (types.isNumber(rawOptions.cellPadding)) {
|
||||
cssRuleText = rawOptions.cellPadding + 'px';
|
||||
} else {
|
||||
cssRuleText = rawOptions.cellPadding.join('px ') + 'px;';
|
||||
}
|
||||
collector.addRule(`.grid-panel .monaco-table .slick-cell {
|
||||
padding: ${cssRuleText}
|
||||
}
|
||||
.grid-panel .monaco-table, .message-tree {
|
||||
${getBareResultsGridInfoStyles(rawOptions)}
|
||||
}`);
|
||||
});
|
||||
}
|
||||
105
src/sql/workbench/contrib/notebook/browser/outputs/factories.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import * as widgets from './widgets';
|
||||
import { IRenderMime } from '../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)
|
||||
};
|
||||
|
||||
export const ipywidgetFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: [
|
||||
'application/vnd.jupyter.widget-view',
|
||||
'application/vnd.jupyter.widget-view+json'
|
||||
],
|
||||
defaultRank: 45,
|
||||
createRenderer: options => new widgets.RenderedIPyWidget(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* The standard factories provided by the rendermime package.
|
||||
*/
|
||||
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
|
||||
htmlRendererFactory,
|
||||
// latexRendererFactory,
|
||||
svgRendererFactory,
|
||||
imageRendererFactory,
|
||||
javaScriptRendererFactory,
|
||||
textRendererFactory,
|
||||
dataResourceRendererFactory,
|
||||
ipywidgetFactory
|
||||
];
|
||||
@@ -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 { 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/contrib/grid/common/interfaces';
|
||||
import { IDataResource } from 'sql/workbench/services/notebook/browser/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/contrib/notebook/browser/outputs/mimeRegistry';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { MimeModel } from 'sql/workbench/contrib/notebook/browser/models/mimemodel';
|
||||
import { GridTableState } from 'sql/workbench/contrib/query/common/gridPanelState';
|
||||
import { GridTableBase } from 'sql/workbench/contrib/query/browser/gridPanel';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { ISerializationService, SerializeDataParams } from 'sql/platform/serialization/common/serializationService';
|
||||
import { SaveResultAction } from 'sql/workbench/contrib/query/browser/actions';
|
||||
import { ResultSerializer, SaveResultsResponse } from 'sql/workbench/contrib/query/common/resultSerializer';
|
||||
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
@Component({
|
||||
selector: GridOutputComponent.SELECTOR,
|
||||
template: `<div #output class="notebook-cellTable" (mouseover)="hover=true" (mouseleave)="hover=false"></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;
|
||||
private _hover: boolean;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set hover(value: boolean) {
|
||||
// only reaction on hover changes
|
||||
if (this._hover !== value) {
|
||||
this.toggleActionbar(value);
|
||||
this._hover = value;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// By default, do not show the actions
|
||||
this.toggleActionbar(false);
|
||||
this._table.onAdd();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this._table) {
|
||||
let maxSize = Math.min(this._table.maximumSize, 500);
|
||||
this._table.layout(maxSize, undefined, ActionsOrientation.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
|
||||
private toggleActionbar(visible: boolean) {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
let actionsContainers: HTMLElement[] = Array.prototype.slice.call(outputElement.getElementsByClassName('actions-container'));
|
||||
if (actionsContainers && actionsContainers.length) {
|
||||
if (visible) {
|
||||
actionsContainers.forEach(container => container.style.visibility = 'visible');
|
||||
} else {
|
||||
actionsContainers.forEach(container => container.style.visibility = 'hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ISerializationService private _serializationService: ISerializationService
|
||||
) {
|
||||
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 this.getContextActions();
|
||||
}
|
||||
|
||||
protected getContextActions(): IAction[] {
|
||||
if (!this._serializationService.hasProvider()) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
@ISerializationService private _serializationService: ISerializationService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
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(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);
|
||||
}
|
||||
|
||||
async copyResults(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||
return 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 this._serializationService.hasProvider();
|
||||
}
|
||||
|
||||
|
||||
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||
let serializer = this._instantiationService.createInstance(ResultSerializer);
|
||||
return serializer.handleSerialization(this.documentUri, format, (filePath) => this.doSerialize(serializer, filePath, format, selection));
|
||||
}
|
||||
|
||||
private doSerialize(serializer: ResultSerializer, filePath: string, format: SaveFormat, selection: Slick.Range[]): Promise<SaveResultsResponse | undefined> {
|
||||
if (!this.canSerialize) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// TODO implement selection support
|
||||
let columns = this.resultSet.columnInfo;
|
||||
let rowLength = this.rows.length;
|
||||
let minRow = 0;
|
||||
let maxRow = this.rows.length;
|
||||
let singleSelection = selection && selection.length > 0 ? selection[0] : undefined;
|
||||
if (singleSelection && this.isSelected(singleSelection)) {
|
||||
rowLength = singleSelection.toRow - singleSelection.fromRow + 1;
|
||||
minRow = singleSelection.fromRow;
|
||||
maxRow = singleSelection.toRow + 1;
|
||||
columns = columns.slice(singleSelection.fromCell, singleSelection.toCell + 1);
|
||||
}
|
||||
let getRows: ((index: number, rowCount: number) => azdata.DbCellValue[][]) = (index, rowCount) => {
|
||||
// Offset for selections by adding the selection startRow to the index
|
||||
index = index + minRow;
|
||||
if (rowLength === 0 || index < 0 || index >= maxRow) {
|
||||
return [];
|
||||
}
|
||||
let endIndex = index + rowCount;
|
||||
if (endIndex > maxRow) {
|
||||
endIndex = maxRow;
|
||||
}
|
||||
let result = this.rows.slice(index, endIndex).map(row => {
|
||||
if (this.isSelected(singleSelection)) {
|
||||
return row.slice(singleSelection.fromCell, singleSelection.toCell + 1);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
let serializeRequestParams: SerializeDataParams = <SerializeDataParams>assign(serializer.getBasicSaveParameters(format), <Partial<SerializeDataParams>>{
|
||||
saveFormat: format,
|
||||
columns: columns,
|
||||
filePath: filePath,
|
||||
getRowRange: (rowStart, numberOfRows) => getRows(rowStart, numberOfRows),
|
||||
rowCount: rowLength
|
||||
});
|
||||
return this._serializationService.serializeResults(serializeRequestParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a range of cells were selected.
|
||||
*/
|
||||
private isSelected(selection: Slick.Range): boolean {
|
||||
return (selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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="codicon 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/contrib/notebook/browser/outputs/sanitizer';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { IMimeComponent } from 'sql/workbench/contrib/notebook/browser/outputs/mimeRegistry';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { NotebookMarkdownRenderer } from 'sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown';
|
||||
import { MimeModel } from 'sql/workbench/contrib/notebook/browser/models/mimemodel';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/contrib/notebook/browser/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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/contrib/notebook/common/models/jsonext';
|
||||
import { MimeModel } from 'sql/workbench/contrib/notebook/browser/models/mimemodel';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
|
||||
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 = values(this._componentDefinitions)
|
||||
.map((c: IMimeComponentDefinition) => c.ctor)
|
||||
.filter(ctor => {
|
||||
let shouldAdd = !addedCtors.some((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/contrib/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/contrib/notebook/browser/models/mimemodel';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/contrib/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,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'vs/base/common/path';
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
import { IMarkdownString, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import * as marked from 'vs/base/common/marked/marked';
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
|
||||
|
||||
// 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: MarkdownRenderOptions): 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: MarkdownRenderOptions = {}): 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.fsPath) + '/';
|
||||
if (!this._baseUrls.some(x => x === 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 {
|
||||
// The call to resolveUrl() (where relative hrefs are converted to absolute ones) comes after this point
|
||||
// Therefore, we only want to return immediately if the path is absolute here
|
||||
if (URI.parse(href) && path.isAbsolute(href)) {
|
||||
return href;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
let originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
||||
if (base && !originIndependentUrl.test(href) && !path.isAbsolute(href)) {
|
||||
href = this.resolveUrl(base, href);
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%5C/g, '\\').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 if (href.slice(0, 2) === '..') {
|
||||
return path.join(base, href);
|
||||
} else {
|
||||
return base + href;
|
||||
}
|
||||
}
|
||||
|
||||
// end marked.js adaptation
|
||||
|
||||
setNotebookURI(val: URI) {
|
||||
this._notebookURI = val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ElementRef, ViewChild } from '@angular/core';
|
||||
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/contrib/notebook/browser/outputs/mimeRegistry';
|
||||
import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { MimeModel } from 'sql/workbench/contrib/notebook/browser/models/mimemodel';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
type ObjectType = object;
|
||||
|
||||
interface FigureLayout extends ObjectType {
|
||||
width?: number;
|
||||
height?: number;
|
||||
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';
|
||||
|
||||
private static Plotly?: Promise<typeof import('plotly.js-dist')>;
|
||||
|
||||
@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() {
|
||||
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() {
|
||||
if (!PlotlyOutputComponent.Plotly) {
|
||||
PlotlyOutputComponent.Plotly = import('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);
|
||||
}
|
||||
PlotlyOutputComponent.Plotly.then(plotly => {
|
||||
return plotly.newPlot(this._plotDiv, figure.data, figure.layout);
|
||||
}).catch(e => this.displayError(e));
|
||||
}
|
||||
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/contrib/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 '../models/renderMimeInterfaces';
|
||||
import { MimeModel } from '../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;
|
||||
});
|
||||
}
|
||||
}
|
||||
655
src/sql/workbench/contrib/notebook/browser/outputs/renderers.ts
Normal file
@@ -0,0 +1,655 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { default as AnsiUp } from 'ansi_up';
|
||||
import { IRenderMime } from '../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 || '';
|
||||
let isLocal = isPathLocal(path, resolver);
|
||||
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) || '';
|
||||
let isLocal = isPathLocal(source, resolver);
|
||||
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') || '';
|
||||
let isLocal = isPathLocal(href, resolver);
|
||||
// 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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
function isPathLocal(path: string, resolver: IRenderMime.IResolver): boolean {
|
||||
let isLocal: boolean;
|
||||
if (path && path.length > 0) {
|
||||
isLocal = resolver && resolver.isLocal
|
||||
? resolver.isLocal(path)
|
||||
: URLExt.isLocal(path);
|
||||
} else {
|
||||
// Empty string, so default to local
|
||||
isLocal = true;
|
||||
}
|
||||
return isLocal;
|
||||
}
|
||||
}
|
||||