Move notebooks to workbench (#4888)
* move notebooks under workbench * fix style imports
@@ -16,7 +16,7 @@ import { INotebookService, INotebookProvider, INotebookManager } from 'sql/workb
|
||||
import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, FutureMessageType, INotebookFutureDetails, INotebookFutureDone } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { LocalContentManager } from 'sql/workbench/services/notebook/node/localContentManager';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { FutureInternal } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
|
||||
@extHostNamedCustomer(SqlMainContext.MainThreadNotebook)
|
||||
export class MainThreadNotebook extends Disposable implements MainThreadNotebookShape {
|
||||
|
||||
@@ -21,12 +21,12 @@ import {
|
||||
SqlMainContext, MainThreadNotebookDocumentsAndEditorsShape, SqlExtHostContext, ExtHostNotebookDocumentsAndEditorsShape,
|
||||
INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData, INotebookModelChangedData
|
||||
} from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { NotebookInput } from 'sql/parts/notebook/notebookInput';
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
|
||||
import { INotebookService, INotebookEditor, IProviderInfo } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { disposed } from 'vs/base/common/errors';
|
||||
import { ICellModel, NotebookContentChange, INotebookModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { NotebookChangeType, CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||
import { ICellModel, NotebookContentChange, INotebookModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { NotebookChangeType, CellTypes } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { notebookModeId } from 'sql/common/constants';
|
||||
|
||||
143
src/sql/workbench/parts/notebook/cellToggleMoreActions.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ElementRef } from '@angular/core';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { CellContext, CellActionBase } from 'sql/workbench/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { ToggleMoreWidgetAction } from 'sql/workbench/parts/dashboard/common/actions';
|
||||
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { CellModel } from 'sql/workbench/parts/notebook/models/cell';
|
||||
|
||||
export const HIDDEN_CLASS ='actionhidden';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: CellActionBase[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
private _moreActionsElement: HTMLElement;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')),
|
||||
instantiationService.createInstance(AddCellFromContextAction,'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', 'Clear output'))
|
||||
);
|
||||
}
|
||||
|
||||
public onInit(elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
let context = new CellContext(model,cellModel);
|
||||
this._moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (this._moreActionsElement.childNodes.length > 0) {
|
||||
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: this._moreActionsElement };
|
||||
let validActions = this._actions.filter(a => a.canRun(context));
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, validActions, context), { icon: true, label: false, isMenu: true });
|
||||
}
|
||||
|
||||
public toggleVisible(visible: boolean): void {
|
||||
if (!this._moreActionsElement) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
DOM.addClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
} else {
|
||||
DOM.removeClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellFromContextAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearCellOutputAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
public canRun(context: CellContext): boolean {
|
||||
return context.cell && context.cell.cellType === CellTypes.Code;
|
||||
}
|
||||
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let cell = context.cell || context.model.activeCell;
|
||||
if (cell) {
|
||||
(cell as CellModel).clearOutputs();
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="width: 100%; height: 100%; display: flex; flex-flow: row" (mouseover)="hover=true" (mouseleave)="hover=false">
|
||||
<div #toolbar class="toolbar">
|
||||
</div>
|
||||
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
||||
</div>
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
300
src/sql/workbench/parts/notebook/cellViews/code.component.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from '@angular/core';
|
||||
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
|
||||
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/cellToggleMoreActions';
|
||||
import { ICellModel, notebookConstants } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, CellContext } from 'sql/workbench/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/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 { SimpleProgressService } from 'vs/editor/standalone/browser/simpleServices';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { CellTypes } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-component';
|
||||
const MARKDOWN_CLASS = 'markdown';
|
||||
|
||||
@Component({
|
||||
selector: CODE_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./code.component.html'))
|
||||
})
|
||||
export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
|
||||
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
|
||||
|
||||
public get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() public set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this.toolbarElement && value && value.cellType === CellTypes.Markdown) {
|
||||
let nativeToolbar = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
DOM.addClass(nativeToolbar, MARKDOWN_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@Output() public onContentChanged = new EventEmitter<void>();
|
||||
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
this._register(value.kernelChanged(() => {
|
||||
// On kernel change, need to reevaluate the language for each cell
|
||||
// Refresh based on the cell magic (since this is kernel-dependent) and then update using notebook language
|
||||
this.checkForLanguageMagics();
|
||||
this.updateLanguageMode();
|
||||
}));
|
||||
this._register(value.onValidConnectionSelected(() => {
|
||||
this.updateConnectionState(this.isActive());
|
||||
}));
|
||||
}
|
||||
|
||||
@Input() set activeCellId(value: string) {
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
@Input() set hover(value: boolean) {
|
||||
this.cellModel.hover = value;
|
||||
if (!this.isActive()) {
|
||||
// Only make a change if we're not active, since this has priority
|
||||
this.toggleMoreActionsButton(this.cellModel.hover);
|
||||
}
|
||||
}
|
||||
|
||||
protected _actionBar: Taskbar;
|
||||
private readonly _minimumHeight = 30;
|
||||
private readonly _maximumHeight = 4000;
|
||||
private _cellModel: ICellModel;
|
||||
private _editor: QueryTextEditor;
|
||||
private _editorInput: UntitledEditorInput;
|
||||
private _editorModel: ITextModel;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _layoutEmitter = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IModelService) private _modelService: IModelService,
|
||||
@Inject(IModeService) private _modeService: IModeService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
this._register(Event.debounce(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false)
|
||||
(() => this.layout()));
|
||||
// Handle disconnect on removal of the cell, if it was the active cell
|
||||
this._register({ dispose: () => this.updateConnectionState(false) });
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.initActionBar();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
this.updateLanguageMode();
|
||||
this.updateModel();
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
let isActive = this.cellModel.id === changedProp.currentValue;
|
||||
this.updateConnectionState(isActive);
|
||||
this.toggleMoreActionsButton(isActive);
|
||||
if (this._editor) {
|
||||
this._editor.toggleEditorSelected(isActive);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateConnectionState(shouldConnect: boolean) {
|
||||
if (this.isSqlCodeCell()) {
|
||||
let cellUri = this.cellModel.cellUri.toString();
|
||||
let connectionService = this.connectionService;
|
||||
if (!shouldConnect && connectionService && connectionService.isConnected(cellUri)) {
|
||||
connectionService.disconnect(cellUri).catch(e => console.log(e));
|
||||
} else if (shouldConnect && this._model.activeConnection && this._model.activeConnection.id !== '-1') {
|
||||
connectionService.connect(this._model.activeConnection, cellUri).catch(e => console.log(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;
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.createEditor();
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
|
||||
this._layoutEmitter.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._layoutEmitter.fire();
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get activeCellId(): string {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private async createEditor(): Promise<void> {
|
||||
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
|
||||
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;
|
||||
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, this.cellModel.source, '');
|
||||
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);
|
||||
|
||||
this._register(this._editor);
|
||||
this._register(this._editorInput);
|
||||
this._register(this._editorModel.onDidChangeContent(e => {
|
||||
this._editor.setHeightToScrollHeight();
|
||||
this.cellModel.source = this._editorModel.getValue();
|
||||
this.onContentChanged.emit();
|
||||
this.checkForLanguageMagics();
|
||||
// TODO see if there's a better way to handle reassessing size.
|
||||
setTimeout(() => this._layoutEmitter.fire(), 250);
|
||||
}));
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.wordWrap')) {
|
||||
this._editor.setHeightToScrollHeight(true);
|
||||
}
|
||||
}));
|
||||
this._register(this.model.layoutChanged(() => this._layoutEmitter.fire(), this));
|
||||
this.layout();
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._editor.layout(new DOM.Dimension(
|
||||
DOM.getContentWidth(this.codeElement.nativeElement),
|
||||
DOM.getContentHeight(this.codeElement.nativeElement)));
|
||||
this._editor.setHeightToScrollHeight();
|
||||
}
|
||||
|
||||
protected initActionBar() {
|
||||
let context = new CellContext(this.model, this.cellModel);
|
||||
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar);
|
||||
this._actionBar.context = context;
|
||||
this._actionBar.setContent([
|
||||
{ action: runCellAction }
|
||||
]);
|
||||
|
||||
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
|
||||
/// Editor Functions
|
||||
private updateModel() {
|
||||
if (this._editorModel) {
|
||||
this._modelService.updateModel(this._editorModel, this.cellModel.source);
|
||||
}
|
||||
}
|
||||
|
||||
private checkForLanguageMagics(): void {
|
||||
try {
|
||||
if (!this.cellModel || this.cellModel.cellType !== CellTypes.Code) {
|
||||
return;
|
||||
}
|
||||
if (this._editorModel && this._editor && this._editorModel.getLineCount() > 1) {
|
||||
// Only try to match once we've typed past the first line
|
||||
let magicName = notebookUtils.tryMatchCellMagic(this._editorModel.getLineContent(1));
|
||||
if (magicName) {
|
||||
let kernelName = this._model.clientSession && this._model.clientSession.kernel ? this._model.clientSession.kernel.name : undefined;
|
||||
let magic = this._model.notebookOptions.cellMagicMapper.toLanguageMagic(magicName, kernelName);
|
||||
if (magic && this.cellModel.language !== magic.language) {
|
||||
this.cellModel.setOverrideLanguage(magic.language);
|
||||
this.updateLanguageMode();
|
||||
}
|
||||
} else {
|
||||
this.cellModel.setOverrideLanguage(undefined);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// No-op for now. Should we log?
|
||||
}
|
||||
}
|
||||
|
||||
private updateLanguageMode(): void {
|
||||
if (this._editorModel && this._editor) {
|
||||
let modeValue = this._modeService.create(this.cellModel.language);
|
||||
this._modelService.setMode(this._editorModel, modeValue);
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
|
||||
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
private setFocusAndScroll(): void {
|
||||
if (this.cellModel.id === this._activeCellId) {
|
||||
this._editor.focus();
|
||||
this._editor.getContainer().scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
92
src/sql/workbench/parts/notebook/cellViews/code.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
code-component {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
code-component .toolbar {
|
||||
border-right-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-flow:column;
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
orientation: portrait
|
||||
}
|
||||
|
||||
code-component .toolbar.markdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code-component .toolbar .carbon-taskbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconRun {
|
||||
height: 20px;
|
||||
background-image: url('../media/light/execute_cell.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.vs-dark code-component .toolbarIconRun,
|
||||
.hc-black code-component .toolbarIconRun {
|
||||
background-image: url('../media/dark/execute_cell_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .toolbarIconRunError {
|
||||
height: 20px;
|
||||
background-image: url('../media/light/execute_cell_error.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconStop {
|
||||
height: 20px;
|
||||
background-image: url('../media/light/stop_cell_solidanimation.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.vs-dark code-component .toolbarIconStop,
|
||||
.hc-black code-component .toolbarIconStop {
|
||||
background-image: url('../media/dark/stop_cell_solidanimation_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .editor {
|
||||
padding: 5px 0px 5px 0px
|
||||
}
|
||||
/* overview ruler */
|
||||
code-component .monaco-editor .decorationsOverviewRuler {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon {
|
||||
background-size: 20px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon {
|
||||
width: 0px;
|
||||
padding-left: 0px;
|
||||
padding-top: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountTen {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountHundred {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||
{
|
||||
padding-left: 10px
|
||||
}
|
||||
132
src/sql/workbench/parts/notebook/cellViews/codeActions.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Action } from 'vs/base/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { ICellModel, CellExecutionState } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { MultiStateAction, IMultiStateData, IActionStateData } from 'sql/workbench/parts/notebook/notebookActions';
|
||||
|
||||
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) {
|
||||
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') }},
|
||||
{ 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 ));
|
||||
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,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="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">
|
||||
</output-area-component>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges, AfterViewInit } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
|
||||
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 }) {
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
17
src/sql/workbench/parts/notebook/cellViews/interfaces.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/node/lifecycle';
|
||||
|
||||
export abstract class CellView extends AngularDisposable implements OnDestroy {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public abstract layout(): void;
|
||||
}
|
||||
|
||||
|
||||
465
src/sql/workbench/parts/notebook/cellViews/media/output.css
Normal file
@@ -0,0 +1,465 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| 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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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: initial;">
|
||||
<div #output ></div>
|
||||
</div>
|
||||
</div>
|
||||
103
src/sql/workbench/parts/notebook/cellViews/output.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from '@angular/core';
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import { nb } from 'azdata';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
|
||||
import * as outputProcessor from 'sql/workbench/parts/notebook/outputs/common/outputProcessor';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export const OUTPUT_SELECTOR: string = 'output-component';
|
||||
|
||||
@Component({
|
||||
selector: OUTPUT_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./output.component.html'))
|
||||
})
|
||||
export class OutputComponent extends AngularDisposable implements OnInit {
|
||||
@ViewChild('output', { read: ElementRef }) private outputElement: ElementRef;
|
||||
@Input() cellOutput: nb.ICellOutput;
|
||||
@Input() cellModel: ICellModel;
|
||||
private _trusted: boolean;
|
||||
private _initialized: boolean = false;
|
||||
private readonly _minimumHeight = 30;
|
||||
registry: RenderMimeRegistry;
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject(INotebookService) private _notebookService: INotebookService,
|
||||
@Inject(IThemeService) private _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
this.registry = _notebookService.getMimeRegistry();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.renderOutput();
|
||||
this._initialized = true;
|
||||
this.cellModel.notebookModel.layoutChanged(() => {
|
||||
this.renderOutput();
|
||||
});
|
||||
}
|
||||
|
||||
private renderOutput() {
|
||||
let node = this.outputElement.nativeElement;
|
||||
let output = this.cellOutput;
|
||||
let options = outputProcessor.getBundleOptions({ value: output, trusted: this.trustedMode });
|
||||
options.themeService = this._themeService;
|
||||
// TODO handle safe/unsafe mapping
|
||||
this.createRenderedMimetype(options, node);
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
}
|
||||
|
||||
get trustedMode(): boolean {
|
||||
return this._trusted;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set trustedMode(value: boolean) {
|
||||
this._trusted = value;
|
||||
if (this._initialized) {
|
||||
this.renderOutput();
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void {
|
||||
let mimeType = this.registry.preferredMimeType(
|
||||
options.data,
|
||||
options.trusted ? 'any' : 'ensure'
|
||||
);
|
||||
if (mimeType) {
|
||||
let output = this.registry.createRenderer(mimeType);
|
||||
output.node = node;
|
||||
let model = new MimeModel(options);
|
||||
output.renderModel(model).catch(error => {
|
||||
// Manually append error message to output
|
||||
output.node.innerHTML = `<pre>Javascript Error: ${error.message}</pre>`;
|
||||
// Remove mime-type-specific CSS classes
|
||||
output.node.className = 'p-Widget jp-RenderedText';
|
||||
output.node.setAttribute(
|
||||
'data-mime-type',
|
||||
'application/vnd.jupyter.stderr'
|
||||
);
|
||||
});
|
||||
//this.setState({ node: node });
|
||||
} else {
|
||||
// TODO Localize
|
||||
node.innerHTML =
|
||||
`No ${options.trusted ? '' : '(safe) '}renderer could be ` +
|
||||
'found for output. It has the following MIME types: ' +
|
||||
Object.keys(options.data).join(', ');
|
||||
//this.setState({ node: node });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 class="notebook-output" style="flex: 0 0 auto;">
|
||||
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" [cellModel]="cellModel">
|
||||
</output-component>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/node/lifecycle';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
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 readonly _minimumHeight = 30;
|
||||
|
||||
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(() => {
|
||||
if (!(this._changeRef['destroyed'])) {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let outputElement = <HTMLElement>this.outputArea.nativeElement;
|
||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
}
|
||||
13
src/sql/workbench/parts/notebook/cellViews/outputArea.css
Normal file
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
output-area-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
output-area-component .notebook-output {
|
||||
border-top-width: 0px;
|
||||
user-select: text;
|
||||
padding: 5px 20px 0px;
|
||||
}
|
||||
20
src/sql/workbench/parts/notebook/cellViews/placeholder.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
placeholder-cell-component {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.14);
|
||||
}
|
||||
|
||||
placeholder-cell-component .text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
-webkit-margin-before: 0em;
|
||||
-webkit-margin-after: 0em;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div class="placeholder-cell-component" style="flex: 0 0 auto;">
|
||||
<div class="placeholder-cell-component text">
|
||||
<p>{{clickOn}} <a href="#" (click)="addCell('code', $event)">{{plusCode}}</a> {{or}} <a href="#" (click)="addCell('markdown', $event)">{{plusText}}</a> {{toAddCell}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./placeholder';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CellType } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
|
||||
|
||||
export const PLACEHOLDER_SELECTOR: string = 'placeholder-cell-component';
|
||||
|
||||
@Component({
|
||||
selector: PLACEHOLDER_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./placeholderCell.component.html'))
|
||||
})
|
||||
|
||||
export class PlaceholderCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@Input() cellModel: ICellModel;
|
||||
@Input() set model(value: NotebookModel) {
|
||||
this._model = value;
|
||||
}
|
||||
|
||||
private _model: NotebookModel;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.cellModel) {
|
||||
this._register(this.cellModel.onOutputsChanged(() => {
|
||||
this._changeRef.detectChanges();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get clickOn(): string {
|
||||
return localize('clickOn','Click on');
|
||||
}
|
||||
|
||||
get plusCode(): string {
|
||||
return localize('plusCode', '+ Code');
|
||||
}
|
||||
|
||||
get or(): string {
|
||||
return localize('or', 'or');
|
||||
}
|
||||
|
||||
get plusText(): string {
|
||||
return localize('plusText', '+ Text');
|
||||
}
|
||||
|
||||
get toAddCell(): string {
|
||||
return localize('toAddCell', 'to add a code or text cell');
|
||||
}
|
||||
|
||||
public addCell(cellType: string, event?: Event): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
let type: CellType = <CellType>cellType;
|
||||
if (!type) {
|
||||
type = 'code';
|
||||
}
|
||||
this._model.addCell(<CellType>cellType);
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
<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 class ="notebook-preview" style="flex: 1 1 auto; user-select: initial;" (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>
|
||||
200
src/sql/workbench/parts/notebook/cellViews/textCell.component.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } 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 { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { CellView } from 'sql/workbench/parts/notebook/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/outputs/sanitizer';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/cellToggleMoreActions';
|
||||
|
||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
private _content: string;
|
||||
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;
|
||||
protected isLoading: boolean;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _hover: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = true;
|
||||
this.isLoading = true;
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
}
|
||||
|
||||
//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.isLoading = isLoading;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setLoading(false);
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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._content !== this.cellModel.source || this.cellModel.source.length === 0) {
|
||||
if (!this.cellModel.source && !this.isEditMode) {
|
||||
this._content = localize('doubleClickEdit', 'Double-click to edit');
|
||||
} else {
|
||||
this._content = this.sanitizeContent(this.cellModel.source);
|
||||
}
|
||||
// todo: pass in the notebook filename instead of undefined value
|
||||
this._commandService.executeCommand<string>('notebook.showPreview', undefined, this._content).then((htmlcontent) => {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = htmlcontent;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//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 setFocusAndScroll(): void {
|
||||
this.toggleEditMode(this.isActive());
|
||||
|
||||
if (this.output && this.output.nativeElement) {
|
||||
(<HTMLElement>this.output.nativeElement).scrollTo();
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
14
src/sql/workbench/parts/notebook/cellViews/textCell.css
Normal file
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
text-cell-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
text-cell-component .notebook-preview {
|
||||
user-select: initial;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
@@ -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 @@
|
||||
<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 @@
|
||||
<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 @@
|
||||
<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" 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 |
1
src/sql/workbench/parts/notebook/media/dark/stop_cell_inverse.svg
Executable file
@@ -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,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 @@
|
||||
<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 |
1
src/sql/workbench/parts/notebook/media/light/add.svg
Normal file
@@ -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 @@
|
||||
<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 @@
|
||||
<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 |
1
src/sql/workbench/parts/notebook/media/light/execute_cell_error.svg
Executable file
@@ -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 @@
|
||||
<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{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 |
1
src/sql/workbench/parts/notebook/media/light/stop_cell.svg
Executable file
@@ -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 |
16
src/sql/workbench/parts/notebook/media/light/stop_cell_solidanimation.svg
Executable file
@@ -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 |
1
src/sql/workbench/parts/notebook/media/light/trusted.svg
Normal file
@@ -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 |
498
src/sql/workbench/parts/notebook/models/cell.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
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 '../notebookUtils';
|
||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { ICellModelOptions, IModelFactory, FutureInternal, CellExecutionState } from './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 { MssqlProviderId } from 'sql/common/constants';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
let modelId = 0;
|
||||
|
||||
|
||||
export class CellModel implements ICellModel {
|
||||
private _cellType: nb.CellType;
|
||||
private _source: string;
|
||||
private _language: string;
|
||||
private _future: FutureInternal;
|
||||
private _outputs: nb.ICellOutput[] = [];
|
||||
private _isEditMode: boolean;
|
||||
private _onOutputsChanged = new Emitter<ReadonlyArray<nb.ICellOutput>>();
|
||||
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;
|
||||
public id: string;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
|
||||
constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) {
|
||||
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;
|
||||
if (_options && _options.isTrusted) {
|
||||
this._isTrusted = true;
|
||||
} else {
|
||||
this._isTrusted = false;
|
||||
}
|
||||
this.createUri();
|
||||
}
|
||||
|
||||
public equals(other: ICellModel) {
|
||||
return other && other.id === this.id;
|
||||
}
|
||||
|
||||
public get onOutputsChanged(): Event<ReadonlyArray<nb.ICellOutput>> {
|
||||
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 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;
|
||||
this._onOutputsChanged.fire(this._outputs);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return this._source;
|
||||
}
|
||||
|
||||
public set source(newSource: string) {
|
||||
if (this._source !== newSource) {
|
||||
this._source = newSource;
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
public get language(): string {
|
||||
if (this._cellType === CellTypes.Markdown) {
|
||||
return 'markdown';
|
||||
}
|
||||
if (this._language) {
|
||||
return this._language;
|
||||
}
|
||||
return this.options.notebook.language;
|
||||
}
|
||||
|
||||
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 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 (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) {
|
||||
this.future.inProgress = false;
|
||||
await kernel.interrupt();
|
||||
} else {
|
||||
// TODO update source based on editor component contents
|
||||
let content = this.source;
|
||||
if (content) {
|
||||
let future = await 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 = notebookUtils.getErrorMessage(error);
|
||||
}
|
||||
this.sendNotification(notificationService, Severity.Error, message);
|
||||
// TODO track error state for the cell
|
||||
} finally {
|
||||
this.disposeFuture();
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
public clearOutputs(): void {
|
||||
this._outputs = [];
|
||||
this.fireOutputsChanged();
|
||||
}
|
||||
|
||||
private fireOutputsChanged(): void {
|
||||
this._onOutputsChanged.fire(this.outputs);
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated);
|
||||
}
|
||||
|
||||
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
|
||||
let output: nb.ICellOutput = msg.content as nb.ICellOutput;
|
||||
|
||||
if (!this._future.inProgress) {
|
||||
this.disposeFuture();
|
||||
}
|
||||
}
|
||||
|
||||
private handleIOPub(msg: nb.IIOPubMessage): void {
|
||||
let msgType = msg.header.msg_type;
|
||||
let displayId = this.getDisplayId(msg);
|
||||
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));
|
||||
this.fireOutputsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
|
||||
// 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 endpoint = this.getKnoxEndpoint(model.activeConnection);
|
||||
let host = endpoint && endpoint.ipAddress ? endpoint.ipAddress : model.activeConnection.serverName;
|
||||
let html = result.data['text/html'];
|
||||
html = html.replace(/(https?:\/\/mssql-master.*\/proxy)(.*)/g, function (a, b, c) {
|
||||
let ret = '';
|
||||
if (b !== '') {
|
||||
ret = 'https://' + host + ':30443/gateway/default/yarn/proxy';
|
||||
}
|
||||
if (c !== '') {
|
||||
ret = ret + c;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
(<nb.IDisplayResult>output).data['text/html'] = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
|
||||
let transient = (msg.content.transient || {});
|
||||
return transient['display_id'] as string;
|
||||
}
|
||||
|
||||
public toJSON(): nb.ICellContents {
|
||||
let cellJson: Partial<nb.ICellContents> = {
|
||||
cell_type: this._cellType,
|
||||
source: this._source,
|
||||
metadata: {
|
||||
}
|
||||
};
|
||||
if (this._cellType === CellTypes.Code) {
|
||||
cellJson.metadata.language = this._language,
|
||||
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 = Array.isArray(cell.source) ? cell.source.join('') : cell.source;
|
||||
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 getKnoxEndpoint(activeConnection: IConnectionProfile): notebookUtils.IEndpoint {
|
||||
let endpoint;
|
||||
if (this._connectionManagementService && activeConnection && activeConnection.providerName === MssqlProviderId) {
|
||||
let serverInfo: ServerInfo = this._connectionManagementService.getServerInfo(activeConnection.id);
|
||||
if (serverInfo && serverInfo.options && serverInfo.options['clusterEndpoints']) {
|
||||
let endpoints: notebookUtils.IEndpoint[] = serverInfo.options['clusterEndpoints'];
|
||||
if (endpoints && endpoints.length > 0) {
|
||||
endpoint = endpoints.find(ep => ep.serviceName.toLowerCase() === 'knox');
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// Dispose and set current future to undefined
|
||||
private disposeFuture() {
|
||||
if (this._future) {
|
||||
this._future.dispose();
|
||||
}
|
||||
this._future = undefined;
|
||||
}
|
||||
}
|
||||
55
src/sql/workbench/parts/notebook/models/cellMagicMapper.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { ICellMagicMapper, ILanguageMagic } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
|
||||
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 kernelMagics.find(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;
|
||||
}
|
||||
}
|
||||
369
src/sql/workbench/parts/notebook/models/clientSession.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
'use strict';
|
||||
|
||||
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 { IClientSession, IKernelPreference, IClientSessionOptions } from './modelInterfaces';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
import * as notebookUtils from '../notebookUtils';
|
||||
import { INotebookManager } from 'sql/workbench/services/notebook/common/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 = notebookUtils.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 && !serverManager.isStarted) {
|
||||
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 newKernel = 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;
|
||||
}
|
||||
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 && kernel.isReady) {
|
||||
this._cachedKernelSpec = await this.kernel.getSpec();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to either call ChangeKernel on current session, or start a new session
|
||||
* @param options
|
||||
*/
|
||||
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) {
|
||||
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
|
||||
}
|
||||
48
src/sql/workbench/parts/notebook/models/contracts.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export type CellType = 'code' | 'markdown' | 'raw';
|
||||
|
||||
export class CellTypes {
|
||||
public static readonly Code = 'code';
|
||||
public static readonly Markdown = 'markdown';
|
||||
public static readonly Raw = 'raw';
|
||||
}
|
||||
|
||||
// to do: add all mime types
|
||||
export type MimeType = 'text/plain' | 'text/html';
|
||||
|
||||
// to do: add all mime types
|
||||
export class MimeTypes {
|
||||
public static readonly PlainText = 'text/plain';
|
||||
public static readonly HTML = 'text/html';
|
||||
}
|
||||
|
||||
export type OutputType =
|
||||
| 'execute_result'
|
||||
| 'display_data'
|
||||
| 'stream'
|
||||
| 'error'
|
||||
| 'update_display_data';
|
||||
|
||||
export class OutputTypes {
|
||||
public static readonly ExecuteResult = 'execute_result';
|
||||
public static readonly DisplayData = 'display_data';
|
||||
public static readonly Stream = 'stream';
|
||||
public static readonly Error = 'error';
|
||||
public static readonly UpdateDisplayData = 'update_display_data';
|
||||
}
|
||||
|
||||
export enum NotebookChangeType {
|
||||
CellsAdded,
|
||||
CellDeleted,
|
||||
CellSourceUpdated,
|
||||
CellOutputUpdated,
|
||||
DirtyStateChanged,
|
||||
KernelChanged
|
||||
}
|
||||
54
src/sql/workbench/parts/notebook/models/jsonext.ts
Normal file
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
/**
|
||||
* A type alias for a JSON primitive.
|
||||
*/
|
||||
export declare type JSONPrimitive = boolean | number | string | null;
|
||||
/**
|
||||
* A type alias for a JSON value.
|
||||
*/
|
||||
export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray;
|
||||
/**
|
||||
* A type definition for a JSON object.
|
||||
*/
|
||||
export interface JSONObject {
|
||||
[key: string]: JSONValue;
|
||||
}
|
||||
/**
|
||||
* A type definition for a JSON array.
|
||||
*/
|
||||
export interface JSONArray extends Array<JSONValue> {
|
||||
}
|
||||
/**
|
||||
* A type definition for a readonly JSON object.
|
||||
*/
|
||||
export interface ReadonlyJSONObject {
|
||||
readonly [key: string]: ReadonlyJSONValue;
|
||||
}
|
||||
/**
|
||||
* A type definition for a readonly JSON array.
|
||||
*/
|
||||
export interface ReadonlyJSONArray extends ReadonlyArray<ReadonlyJSONValue> {
|
||||
}
|
||||
/**
|
||||
* A type alias for a readonly JSON value.
|
||||
*/
|
||||
export declare type ReadonlyJSONValue = JSONPrimitive | ReadonlyJSONObject | ReadonlyJSONArray;
|
||||
/**
|
||||
* Test whether a JSON value is a primitive.
|
||||
*
|
||||
* @param value - The JSON value of interest.
|
||||
*
|
||||
* @returns `true` if the value is a primitive,`false` otherwise.
|
||||
*/
|
||||
export function isPrimitive(value: any): boolean {
|
||||
return (
|
||||
value === null ||
|
||||
typeof value === 'boolean' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string'
|
||||
);
|
||||
}
|
||||
23
src/sql/workbench/parts/notebook/models/modelFactory.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'azdata';
|
||||
|
||||
import { CellModel } from './cell';
|
||||
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from './modelInterfaces';
|
||||
import { ClientSession } from './clientSession';
|
||||
|
||||
export class ModelFactory implements IModelFactory {
|
||||
|
||||
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
|
||||
return new CellModel(this, cell, options);
|
||||
}
|
||||
|
||||
public createClientSession(options: IClientSessionOptions): IClientSession {
|
||||
return new ClientSession(options);
|
||||
}
|
||||
}
|
||||
529
src/sql/workbench/parts/notebook/models/modelInterfaces.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
'use strict';
|
||||
|
||||
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/parts/notebook/models/contracts';
|
||||
import { INotebookManager } from 'sql/workbench/services/notebook/common/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/parts/notebook/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/parts/notebook/models/notebookModel';
|
||||
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 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(): 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>;
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof NotebookContentChange
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export interface ICellModelOptions {
|
||||
notebook: INotebookModel;
|
||||
isTrusted: boolean;
|
||||
}
|
||||
|
||||
export enum CellExecutionState {
|
||||
Hidden = 0,
|
||||
Stopped = 1,
|
||||
Running = 2,
|
||||
Error = 3
|
||||
}
|
||||
|
||||
export interface ICellModel {
|
||||
cellUri: URI;
|
||||
id: string;
|
||||
readonly language: string;
|
||||
source: string;
|
||||
cellType: CellType;
|
||||
trustedMode: boolean;
|
||||
active: boolean;
|
||||
hover: boolean;
|
||||
executionCount: number | undefined;
|
||||
readonly future: FutureInternal;
|
||||
readonly outputs: ReadonlyArray<nb.ICellOutput>;
|
||||
readonly onOutputsChanged: Event<ReadonlyArray<nb.ICellOutput>>;
|
||||
readonly onExecutionStateChange: Event<CellExecutionState>;
|
||||
readonly executionState: CellExecutionState;
|
||||
readonly notebookModel: NotebookModel;
|
||||
setFuture(future: FutureInternal): void;
|
||||
runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean>;
|
||||
setOverrideLanguage(language: string);
|
||||
equals(cellModel: ICellModel): boolean;
|
||||
toJSON(): nb.ICellContents;
|
||||
}
|
||||
|
||||
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;
|
||||
standardKernels: IStandardKernelWithProvider[];
|
||||
defaultKernel: nb.IKernelSpec;
|
||||
cellMagicMapper: ICellMagicMapper;
|
||||
|
||||
layoutChanged: Event<void>;
|
||||
|
||||
notificationService: INotificationService;
|
||||
connectionService: IConnectionManagementService;
|
||||
capabilitiesService: ICapabilitiesService;
|
||||
}
|
||||
|
||||
export interface ILanguageMagic {
|
||||
magic: string;
|
||||
language: string;
|
||||
kernels?: string[];
|
||||
executionTarget?: string;
|
||||
}
|
||||
|
||||
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 = 'MSSQL';
|
||||
export const sqlKernel: string = localize('sqlKernel', 'SQL');
|
||||
export const sqlKernelSpec: nb.IKernelSpec = ({
|
||||
name: sqlKernel,
|
||||
language: 'sql',
|
||||
display_name: sqlKernel
|
||||
});
|
||||
}
|
||||
494
src/sql/workbench/parts/notebook/models/nbformat.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
// Notebook format interfaces
|
||||
// https://nbformat.readthedocs.io/en/latest/format_description.html
|
||||
// https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.schema.json
|
||||
|
||||
|
||||
import { JSONObject } from './jsonext';
|
||||
import { nb } from 'azdata';
|
||||
|
||||
/**
|
||||
* A namespace for nbformat interfaces.
|
||||
*/
|
||||
export namespace nbformat {
|
||||
/**
|
||||
* The major version of the notebook format.
|
||||
*/
|
||||
export const MAJOR_VERSION: number = 4;
|
||||
|
||||
/**
|
||||
* The minor version of the notebook format.
|
||||
*/
|
||||
export const MINOR_VERSION: number = 2;
|
||||
|
||||
/**
|
||||
* The kernelspec metadata.
|
||||
*/
|
||||
export interface IKernelspecMetadata extends JSONObject {
|
||||
name: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The language info metatda
|
||||
*/
|
||||
export interface ILanguageInfoMetadata extends JSONObject {
|
||||
name: string;
|
||||
codemirror_mode?: string | JSONObject;
|
||||
file_extension?: string;
|
||||
mimetype?: string;
|
||||
pygments_lexer?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default metadata for the notebook.
|
||||
*/
|
||||
export interface INotebookMetadata extends JSONObject {
|
||||
kernelspec?: IKernelspecMetadata;
|
||||
language_info?: ILanguageInfoMetadata;
|
||||
orig_nbformat: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The notebook content.
|
||||
*/
|
||||
export interface INotebookContent {
|
||||
metadata: INotebookMetadata;
|
||||
nbformat_minor: number;
|
||||
nbformat: number;
|
||||
cells: ICell[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A multiline string.
|
||||
*/
|
||||
export type MultilineString = string | string[];
|
||||
|
||||
/**
|
||||
* A mime-type keyed dictionary of data.
|
||||
*/
|
||||
export interface IMimeBundle extends JSONObject {
|
||||
[key: string]: MultilineString | JSONObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media attachments (e.g. inline images).
|
||||
*/
|
||||
export interface IAttachments {
|
||||
[key: string]: IMimeBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* The code cell's prompt number. Will be null if the cell has not been run.
|
||||
*/
|
||||
export type ExecutionCount = number | null;
|
||||
|
||||
/**
|
||||
* Cell output metadata.
|
||||
*/
|
||||
export type OutputMetadata = JSONObject;
|
||||
|
||||
/**
|
||||
* Validate a mime type/value pair.
|
||||
*
|
||||
* @param type - The mimetype name.
|
||||
*
|
||||
* @param value - The value associated with the type.
|
||||
*
|
||||
* @returns Whether the type/value pair are valid.
|
||||
*/
|
||||
export function validateMimeValue(
|
||||
type: string,
|
||||
value: MultilineString | JSONObject
|
||||
): boolean {
|
||||
// Check if "application/json" or "application/foo+json"
|
||||
const jsonTest = /^application\/(.*?)+\+json$/;
|
||||
const isJSONType = type === 'application/json' || jsonTest.test(type);
|
||||
|
||||
let isString = (x: any) => {
|
||||
return Object.prototype.toString.call(x) === '[object String]';
|
||||
};
|
||||
|
||||
// If it is an array, make sure if is not a JSON type and it is an
|
||||
// array of strings.
|
||||
if (Array.isArray(value)) {
|
||||
if (isJSONType) {
|
||||
return false;
|
||||
}
|
||||
let valid = true;
|
||||
(value as string[]).forEach(v => {
|
||||
if (!isString(v)) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
// If it is a string, make sure we are not a JSON type.
|
||||
if (isString(value)) {
|
||||
return !isJSONType;
|
||||
}
|
||||
|
||||
// It is not a string, make sure it is a JSON type.
|
||||
if (!isJSONType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// It is a JSON type, make sure it is a valid JSON object.
|
||||
// return JSONExt.isObject(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell-level metadata.
|
||||
*/
|
||||
export interface IBaseCellMetadata extends JSONObject {
|
||||
/**
|
||||
* Whether the cell is trusted.
|
||||
*
|
||||
* #### Notes
|
||||
* This is not strictly part of the nbformat spec, but it is added by
|
||||
* the contents manager.
|
||||
*
|
||||
* See https://jupyter-notebook.readthedocs.io/en/latest/security.html.
|
||||
*/
|
||||
trusted: boolean;
|
||||
|
||||
/**
|
||||
* The cell's name. If present, must be a non-empty string.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The cell's tags. Tags must be unique, and must not contain commas.
|
||||
*/
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The base cell interface.
|
||||
*/
|
||||
export interface IBaseCell {
|
||||
/**
|
||||
* String identifying the type of cell.
|
||||
*/
|
||||
cell_type: string;
|
||||
|
||||
/**
|
||||
* Contents of the cell, represented as an array of lines.
|
||||
*/
|
||||
source: MultilineString;
|
||||
|
||||
/**
|
||||
* Cell-level metadata.
|
||||
*/
|
||||
metadata: Partial<ICellMetadata>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for the raw cell.
|
||||
*/
|
||||
export interface IRawCellMetadata extends IBaseCellMetadata {
|
||||
/**
|
||||
* Raw cell metadata format for nbconvert.
|
||||
*/
|
||||
format: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A raw cell.
|
||||
*/
|
||||
export interface IRawCell extends IBaseCell {
|
||||
/**
|
||||
* String identifying the type of cell.
|
||||
*/
|
||||
cell_type: 'raw';
|
||||
|
||||
/**
|
||||
* Cell-level metadata.
|
||||
*/
|
||||
metadata: Partial<IRawCellMetadata>;
|
||||
|
||||
/**
|
||||
* Cell attachments.
|
||||
*/
|
||||
attachments?: IAttachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* A markdown cell.
|
||||
*/
|
||||
export interface IMarkdownCell extends IBaseCell {
|
||||
/**
|
||||
* String identifying the type of cell.
|
||||
*/
|
||||
cell_type: 'markdown';
|
||||
|
||||
/**
|
||||
* Cell attachments.
|
||||
*/
|
||||
attachments?: IAttachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a code cell.
|
||||
*/
|
||||
export interface ICodeCellMetadata extends IBaseCellMetadata {
|
||||
/**
|
||||
* Whether the cell is collapsed/expanded.
|
||||
*/
|
||||
collapsed: boolean;
|
||||
|
||||
/**
|
||||
* Whether the cell's output is scrolled, unscrolled, or autoscrolled.
|
||||
*/
|
||||
scrolled: boolean | 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* A code cell.
|
||||
*/
|
||||
export interface ICodeCell extends IBaseCell {
|
||||
/**
|
||||
* String identifying the type of cell.
|
||||
*/
|
||||
cell_type: 'code';
|
||||
|
||||
/**
|
||||
* Cell-level metadata.
|
||||
*/
|
||||
metadata: Partial<ICodeCellMetadata>;
|
||||
|
||||
/**
|
||||
* Execution, display, or stream outputs.
|
||||
*/
|
||||
outputs: IOutput[];
|
||||
|
||||
/**
|
||||
* The code cell's prompt number. Will be null if the cell has not been run.
|
||||
*/
|
||||
execution_count: ExecutionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* An unrecognized cell.
|
||||
*/
|
||||
export interface IUnrecognizedCell extends IBaseCell { }
|
||||
|
||||
/**
|
||||
* A cell union type.
|
||||
*/
|
||||
export type ICell = IRawCell | IMarkdownCell | ICodeCell | IUnrecognizedCell;
|
||||
|
||||
/**
|
||||
* Test whether a cell is a raw cell.
|
||||
*/
|
||||
export function isRaw(cell: ICell): cell is IRawCell {
|
||||
return cell.cell_type === 'raw';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a cell is a markdown cell.
|
||||
*/
|
||||
export function isMarkdown(cell: ICell): cell is IMarkdownCell {
|
||||
return cell.cell_type === 'markdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a cell is a code cell.
|
||||
*/
|
||||
export function isCode(cell: ICell): cell is ICodeCell {
|
||||
return cell.cell_type === 'code';
|
||||
}
|
||||
|
||||
/**
|
||||
* A union metadata type.
|
||||
*/
|
||||
export type ICellMetadata =
|
||||
| IBaseCellMetadata
|
||||
| IRawCellMetadata
|
||||
| ICodeCellMetadata;
|
||||
|
||||
/**
|
||||
* The valid output types.
|
||||
*/
|
||||
export type OutputType =
|
||||
| 'execute_result'
|
||||
| 'display_data'
|
||||
| 'stream'
|
||||
| 'error'
|
||||
| 'update_display_data';
|
||||
|
||||
|
||||
/**
|
||||
* Result of executing a code cell.
|
||||
*/
|
||||
export interface IExecuteResult extends nb.ICellOutput {
|
||||
/**
|
||||
* Type of cell output.
|
||||
*/
|
||||
output_type: 'execute_result';
|
||||
|
||||
/**
|
||||
* A result's prompt number.
|
||||
*/
|
||||
execution_count: ExecutionCount;
|
||||
|
||||
/**
|
||||
* A mime-type keyed dictionary of data.
|
||||
*/
|
||||
data: IMimeBundle;
|
||||
|
||||
/**
|
||||
* Cell output metadata.
|
||||
*/
|
||||
metadata: OutputMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data displayed as a result of code cell execution.
|
||||
*/
|
||||
export interface IDisplayData extends nb.ICellOutput {
|
||||
/**
|
||||
* Type of cell output.
|
||||
*/
|
||||
output_type: 'display_data';
|
||||
|
||||
/**
|
||||
* A mime-type keyed dictionary of data.
|
||||
*/
|
||||
data: IMimeBundle;
|
||||
|
||||
/**
|
||||
* Cell output metadata.
|
||||
*/
|
||||
metadata: OutputMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data displayed as an update to existing display data.
|
||||
*/
|
||||
export interface IDisplayUpdate extends nb.ICellOutput {
|
||||
/**
|
||||
* Type of cell output.
|
||||
*/
|
||||
output_type: 'update_display_data';
|
||||
|
||||
/**
|
||||
* A mime-type keyed dictionary of data.
|
||||
*/
|
||||
data: IMimeBundle;
|
||||
|
||||
/**
|
||||
* Cell output metadata.
|
||||
*/
|
||||
metadata: OutputMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream output from a code cell.
|
||||
*/
|
||||
export interface IStream extends nb.ICellOutput {
|
||||
/**
|
||||
* Type of cell output.
|
||||
*/
|
||||
output_type: 'stream';
|
||||
|
||||
/**
|
||||
* The name of the stream.
|
||||
*/
|
||||
name: StreamType;
|
||||
|
||||
/**
|
||||
* The stream's text output.
|
||||
*/
|
||||
text: MultilineString;
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias for a stream type.
|
||||
*/
|
||||
export type StreamType = 'stdout' | 'stderr';
|
||||
|
||||
/**
|
||||
* Output of an error that occurred during code cell execution.
|
||||
*/
|
||||
export interface IError extends nb.ICellOutput {
|
||||
/**
|
||||
* Type of cell output.
|
||||
*/
|
||||
output_type: 'error';
|
||||
|
||||
/**
|
||||
* The name of the error.
|
||||
*/
|
||||
ename: string;
|
||||
|
||||
/**
|
||||
* The value, or message, of the error.
|
||||
*/
|
||||
evalue: string;
|
||||
|
||||
/**
|
||||
* The error's traceback.
|
||||
*/
|
||||
traceback: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unrecognized output.
|
||||
*/
|
||||
export interface IUnrecognizedOutput extends nb.ICellOutput { }
|
||||
|
||||
/**
|
||||
* Test whether an output is an execute result.
|
||||
*/
|
||||
export function isExecuteResult(output: IOutput): output is IExecuteResult {
|
||||
return output.output_type === 'execute_result';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an output is from display data.
|
||||
*/
|
||||
export function isDisplayData(output: IOutput): output is IDisplayData {
|
||||
return output.output_type === 'display_data';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an output is from updated display data.
|
||||
*/
|
||||
export function isDisplayUpdate(output: IOutput): output is IDisplayUpdate {
|
||||
return output.output_type === 'update_display_data';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an output is from a stream.
|
||||
*/
|
||||
export function isStream(output: IOutput): output is IStream {
|
||||
return output.output_type === 'stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an output is from a stream.
|
||||
*/
|
||||
export function isError(output: IOutput): output is IError {
|
||||
return output.output_type === 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* An output union type.
|
||||
*/
|
||||
export type IOutput =
|
||||
| IUnrecognizedOutput
|
||||
| IExecuteResult
|
||||
| IDisplayData
|
||||
| IStream
|
||||
| IError;
|
||||
}
|
||||
|
||||
export interface ICellOutputWithIdAndTrust extends nb.ICellOutput {
|
||||
id: number;
|
||||
trusted: boolean;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
export namespace constants {
|
||||
export const userPropName = 'user';
|
||||
export const knoxPortPropName = 'knoxport';
|
||||
export const clusterPropName = 'clustername';
|
||||
export const passwordPropName = 'password';
|
||||
export const defaultKnoxPort = '30443';
|
||||
}
|
||||
/**
|
||||
* This is a temporary connection definition, with known properties for Knox gateway connections.
|
||||
* Long term this should be refactored to an extension contribution
|
||||
*
|
||||
* @export
|
||||
* @class NotebookConnection
|
||||
*/
|
||||
export class NotebookConnection {
|
||||
private _host: string;
|
||||
private _knoxPort: string;
|
||||
|
||||
constructor(private _connectionProfile: IConnectionProfile) {
|
||||
if (!this._connectionProfile) {
|
||||
throw new Error(localize('connectionInfoMissing', 'connectionInfo is required'));
|
||||
}
|
||||
}
|
||||
|
||||
public get connectionProfile(): IConnectionProfile {
|
||||
return this._connectionProfile;
|
||||
}
|
||||
|
||||
|
||||
public get host(): string {
|
||||
if (!this._host) {
|
||||
this.ensureHostAndPort();
|
||||
}
|
||||
return this._host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets host and port values, using any ',' or ':' delimited port in the hostname in
|
||||
* preference to the built in port.
|
||||
*/
|
||||
private ensureHostAndPort(): void {
|
||||
this._host = this.connectionProfile.serverName;
|
||||
this._knoxPort = NotebookConnection.getKnoxPortOrDefault(this.connectionProfile);
|
||||
// determine whether the host has either a ',' or ':' in it
|
||||
this.setHostAndPort(',');
|
||||
this.setHostAndPort(':');
|
||||
}
|
||||
|
||||
// set port and host correctly after we've identified that a delimiter exists in the host name
|
||||
private setHostAndPort(delimeter: string): void {
|
||||
let originalHost = this._host;
|
||||
let index = originalHost.indexOf(delimeter);
|
||||
if (index > -1) {
|
||||
this._host = originalHost.slice(0, index);
|
||||
this._knoxPort = originalHost.slice(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public get user(): string {
|
||||
return this._connectionProfile.options[constants.userPropName];
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this._connectionProfile.options[constants.passwordPropName];
|
||||
}
|
||||
|
||||
public get knoxport(): string {
|
||||
if (!this._knoxPort) {
|
||||
this.ensureHostAndPort();
|
||||
}
|
||||
return this._knoxPort;
|
||||
}
|
||||
|
||||
private static getKnoxPortOrDefault(connectionProfile: IConnectionProfile): string {
|
||||
let port = connectionProfile.options[constants.knoxPortPropName];
|
||||
if (!port) {
|
||||
port = constants.defaultKnoxPort;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
}
|
||||
150
src/sql/workbench/parts/notebook/models/notebookContexts.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'azdata';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDefaultConnection, notebookConstants } from 'sql/workbench/parts/notebook/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';
|
||||
|
||||
export class NotebookContexts {
|
||||
private static MSSQL_PROVIDER = 'MSSQL';
|
||||
|
||||
private static get DefaultContext(): IDefaultConnection {
|
||||
let defaultConnection: ConnectionProfile = <any>{
|
||||
providerName: NotebookContexts.MSSQL_PROVIDER,
|
||||
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: NotebookContexts.MSSQL_PROVIDER,
|
||||
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 async getContextsForKernel(connectionService: IConnectionManagementService, connProviderIds: string[], kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise<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 = await this.getActiveContexts(connectionService, connProviderIds, profile);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active contexts and sort them
|
||||
* @param apiWrapper ApiWrapper
|
||||
* @param profile current connection profile
|
||||
*/
|
||||
public static async getActiveContexts(connectionService: IConnectionManagementService, connProviderIds: string[], profile: IConnectionProfile): Promise<IDefaultConnection> {
|
||||
let defaultConnection: ConnectionProfile = NotebookContexts.DefaultContext.defaultConnection;
|
||||
let activeConnections: ConnectionProfile[] = await 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.includes(connection.providerName);
|
||||
});
|
||||
if (connections && connections.length > 0) {
|
||||
defaultConnection = connections[0];
|
||||
if (profile && profile.options) {
|
||||
if (connections.find(connection => connection.serverName === profile.serverName)) {
|
||||
defaultConnection = connections.find(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 = specs.kernels.find((kernel) => kernel.display_name === displayName);
|
||||
}
|
||||
// if no saved kernel exists, use the default KernelSpec
|
||||
if (!defaultKernel) {
|
||||
defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
|
||||
}
|
||||
if (defaultKernel) {
|
||||
return defaultKernel;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default kernel specified (should never happen), default to SQL
|
||||
if (!defaultKernel) {
|
||||
defaultKernel = notebookConstants.sqlKernelSpec;
|
||||
}
|
||||
return defaultKernel;
|
||||
}
|
||||
}
|
||||
944
src/sql/workbench/parts/notebook/models/notebookModel.ts
Normal file
@@ -0,0 +1,944 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb, connection } from 'azdata';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, NotebookContentChange, notebookConstants } from './modelInterfaces';
|
||||
import { NotebookChangeType, CellType } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { nbversion } from '../notebookConstants';
|
||||
import * as notebookUtils from '../notebookUtils';
|
||||
import { INotebookManager, SQL_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { NotebookContexts } from 'sql/workbench/parts/notebook/models/notebookContexts';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { uriPrefixes } from 'sql/platform/connection/common/utils';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
|
||||
/*
|
||||
* Used to control whether a message in a dialog/wizard is displayed as an error,
|
||||
* warning, or informational message. Default is error.
|
||||
*/
|
||||
export enum MessageLevel {
|
||||
Error = 0,
|
||||
Warning = 1,
|
||||
Information = 2
|
||||
}
|
||||
|
||||
export class ErrorInfo {
|
||||
constructor(public readonly message: string, public readonly severity: MessageLevel) {
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookModel extends Disposable implements INotebookModel {
|
||||
private _contextsChangedEmitter = new Emitter<void>();
|
||||
private _contextsLoadingEmitter = new Emitter<void>();
|
||||
private _contentChangedEmitter = new Emitter<NotebookContentChange>();
|
||||
private _kernelsChangedEmitter = new Emitter<nb.IKernelSpec>();
|
||||
private _kernelChangedEmitter = new Emitter<nb.IKernelChangedArgs>();
|
||||
private _layoutChanged = new Emitter<void>();
|
||||
private _inErrorState: boolean = false;
|
||||
private _activeClientSession: IClientSession;
|
||||
private _sessionLoadFinished: Promise<void>;
|
||||
private _onClientSessionReady = new Emitter<IClientSession>();
|
||||
private _onProviderIdChanged = new Emitter<string>();
|
||||
private _activeContexts: IDefaultConnection;
|
||||
private _trustedMode: boolean;
|
||||
|
||||
private _cells: ICellModel[];
|
||||
private _defaultLanguageInfo: nb.ILanguageInfo;
|
||||
private _language: string;
|
||||
private _onErrorEmitter = new Emitter<INotification>();
|
||||
private _savedKernelInfo: nb.IKernelInfo;
|
||||
private readonly _nbformat: number = nbversion.MAJOR_VERSION;
|
||||
private readonly _nbformatMinor: number = nbversion.MINOR_VERSION;
|
||||
private _activeConnection: ConnectionProfile;
|
||||
private _otherConnections: ConnectionProfile[] = [];
|
||||
private _activeCell: ICellModel;
|
||||
private _providerId: string;
|
||||
private _defaultKernel: nb.IKernelSpec;
|
||||
private _kernelDisplayNameToConnectionProviderIds: Map<string, string[]> = new Map<string, string[]>();
|
||||
private _kernelDisplayNameToNotebookProviderIds: Map<string, string> = new Map<string, string>();
|
||||
private _onValidConnectionSelected = new Emitter<boolean>();
|
||||
private _oldKernel: nb.IKernel;
|
||||
private _clientSessionListeners: IDisposable[] = [];
|
||||
private _connectionUrisToDispose: string[] = [];
|
||||
|
||||
constructor(private _notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, public connectionProfile?: IConnectionProfile) {
|
||||
super();
|
||||
if (!_notebookOptions || !_notebookOptions.notebookUri || !_notebookOptions.notebookManagers) {
|
||||
throw new Error('path or notebook service not defined');
|
||||
}
|
||||
this._trustedMode = false;
|
||||
this._providerId = _notebookOptions.providerId;
|
||||
this._onProviderIdChanged.fire(this._providerId);
|
||||
this._notebookOptions.standardKernels.forEach(kernel => {
|
||||
let displayName = kernel.displayName;
|
||||
if (!displayName) {
|
||||
displayName = kernel.name;
|
||||
}
|
||||
this._kernelDisplayNameToConnectionProviderIds.set(displayName, kernel.connectionProviderIds);
|
||||
this._kernelDisplayNameToNotebookProviderIds.set(displayName, kernel.notebookProvider);
|
||||
});
|
||||
if (this._notebookOptions.layoutChanged) {
|
||||
this._notebookOptions.layoutChanged(() => this._layoutChanged.fire());
|
||||
}
|
||||
this._defaultKernel = _notebookOptions.defaultKernel;
|
||||
}
|
||||
|
||||
public get notebookManagers(): INotebookManager[] {
|
||||
let notebookManagers = this._notebookOptions.notebookManagers.filter(manager => manager.providerId !== DEFAULT_NOTEBOOK_PROVIDER);
|
||||
if (!notebookManagers.length) {
|
||||
return this._notebookOptions.notebookManagers;
|
||||
}
|
||||
return notebookManagers;
|
||||
}
|
||||
|
||||
public get notebookManager(): INotebookManager {
|
||||
let manager = this.notebookManagers.find(manager => manager.providerId === this._providerId);
|
||||
if (!manager) {
|
||||
// Note: this seems like a less than ideal scenario. We should ideally pass in the "correct" provider ID and allow there to be a default,
|
||||
// instead of assuming in the NotebookModel constructor that the option is either SQL or Jupyter
|
||||
manager = this.notebookManagers.find(manager => manager.providerId === DEFAULT_NOTEBOOK_PROVIDER);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
public getNotebookManager(providerId: string): INotebookManager {
|
||||
if (providerId) {
|
||||
return this.notebookManagers.find(manager => manager.providerId === providerId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get notebookOptions(): INotebookModelOptions {
|
||||
return this._notebookOptions;
|
||||
}
|
||||
|
||||
public get notebookUri(): URI {
|
||||
return this._notebookOptions.notebookUri;
|
||||
}
|
||||
public set notebookUri(value: URI) {
|
||||
this._notebookOptions.notebookUri = value;
|
||||
}
|
||||
|
||||
public get hasServerManager(): boolean {
|
||||
// If the service has a server manager, then we can show the start button
|
||||
return !!this.notebookManager.serverManager;
|
||||
}
|
||||
|
||||
public get contentChanged(): Event<NotebookContentChange> {
|
||||
return this._contentChangedEmitter.event;
|
||||
}
|
||||
|
||||
|
||||
public get isSessionReady(): boolean {
|
||||
return !!this._activeClientSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientSession object which handles management of a session instance,
|
||||
* plus startup of the session manager which can return key metadata about the
|
||||
* notebook environment
|
||||
*/
|
||||
public get clientSession(): IClientSession {
|
||||
return this._activeClientSession;
|
||||
}
|
||||
|
||||
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
|
||||
return this._kernelChangedEmitter.event;
|
||||
}
|
||||
|
||||
public get kernelsChanged(): Event<nb.IKernelSpec> {
|
||||
return this._kernelsChangedEmitter.event;
|
||||
}
|
||||
|
||||
public get layoutChanged(): Event<void> {
|
||||
return this._layoutChanged.event;
|
||||
}
|
||||
|
||||
public get defaultKernel(): nb.IKernelSpec {
|
||||
return this._defaultKernel;
|
||||
}
|
||||
|
||||
public get contextsChanged(): Event<void> {
|
||||
return this._contextsChangedEmitter.event;
|
||||
}
|
||||
|
||||
public get contextsLoading(): Event<void> {
|
||||
return this._contextsLoadingEmitter.event;
|
||||
}
|
||||
|
||||
public get cells(): ICellModel[] {
|
||||
return this._cells;
|
||||
}
|
||||
|
||||
public get contexts(): IDefaultConnection {
|
||||
return this._activeContexts;
|
||||
}
|
||||
|
||||
public get specs(): nb.IAllKernels | undefined {
|
||||
let specs: nb.IAllKernels = {
|
||||
defaultKernel: undefined,
|
||||
kernels: []
|
||||
};
|
||||
this.notebookManagers.forEach(manager => {
|
||||
if (manager.sessionManager && manager.sessionManager.specs && manager.sessionManager.specs.kernels) {
|
||||
manager.sessionManager.specs.kernels.forEach(kernel => {
|
||||
specs.kernels.push(kernel);
|
||||
});
|
||||
if (!specs.defaultKernel) {
|
||||
specs.defaultKernel = manager.sessionManager.specs.defaultKernel;
|
||||
}
|
||||
}
|
||||
});
|
||||
return specs;
|
||||
}
|
||||
|
||||
public standardKernelsDisplayName(): string[] {
|
||||
return Array.from(keys(this._kernelDisplayNameToNotebookProviderIds));
|
||||
}
|
||||
|
||||
public get inErrorState(): boolean {
|
||||
return this._inErrorState;
|
||||
}
|
||||
|
||||
public get onError(): Event<INotification> {
|
||||
return this._onErrorEmitter.event;
|
||||
}
|
||||
|
||||
public get trustedMode(): boolean {
|
||||
return this._trustedMode;
|
||||
}
|
||||
|
||||
public get providerId(): string {
|
||||
return this._providerId;
|
||||
}
|
||||
|
||||
public set trustedMode(isTrusted: boolean) {
|
||||
this._trustedMode = isTrusted;
|
||||
if (this._cells) {
|
||||
this._cells.forEach(c => {
|
||||
c.trustedMode = this._trustedMode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get activeConnection(): IConnectionProfile {
|
||||
return this._activeConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 sessionLoadFinished(): Promise<void> {
|
||||
return this._sessionLoadFinished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies when the client session is ready for use
|
||||
*/
|
||||
public get onClientSessionReady(): Event<IClientSession> {
|
||||
return this._onClientSessionReady.event;
|
||||
}
|
||||
|
||||
public get onProviderIdChange(): Event<string> {
|
||||
return this._onProviderIdChanged.event;
|
||||
}
|
||||
|
||||
public get onValidConnectionSelected(): Event<boolean> {
|
||||
return this._onValidConnectionSelected.event;
|
||||
}
|
||||
|
||||
public getApplicableConnectionProviderIds(kernelDisplayName: string): string[] {
|
||||
let ids = [];
|
||||
if (kernelDisplayName) {
|
||||
ids = this._kernelDisplayNameToConnectionProviderIds.get(kernelDisplayName);
|
||||
}
|
||||
return !ids ? [] : ids;
|
||||
}
|
||||
|
||||
public async requestModelLoad(isTrusted: boolean = false): Promise<void> {
|
||||
try {
|
||||
this._trustedMode = isTrusted;
|
||||
let contents = null;
|
||||
|
||||
if (this._notebookOptions && this._notebookOptions.contentManager) {
|
||||
contents = await this._notebookOptions.contentManager.loadContent();
|
||||
}
|
||||
let factory = this._notebookOptions.factory;
|
||||
// if cells already exist, create them with language info (if it is saved)
|
||||
this._cells = [];
|
||||
if (contents) {
|
||||
this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents);
|
||||
this._savedKernelInfo = this.getSavedKernelInfo(contents);
|
||||
if (contents.cells && contents.cells.length > 0) {
|
||||
this._cells = contents.cells.map(c => factory.createCell(c, { notebook: this, isTrusted: isTrusted }));
|
||||
}
|
||||
}
|
||||
this.setDefaultKernelAndProviderId();
|
||||
this.trySetLanguageFromLangInfo();
|
||||
} catch (error) {
|
||||
this._inErrorState = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public findCellIndex(cellModel: ICellModel): number {
|
||||
return this._cells.findIndex((cell) => cell.equals(cellModel));
|
||||
}
|
||||
|
||||
public addCell(cellType: CellType, index?: number): ICellModel {
|
||||
if (this.inErrorState) {
|
||||
return null;
|
||||
}
|
||||
let cell = this.createCell(cellType);
|
||||
|
||||
if (index !== undefined && index !== null && index >= 0 && index < this._cells.length) {
|
||||
this._cells.splice(index, 0, cell);
|
||||
} else {
|
||||
this._cells.push(cell);
|
||||
index = undefined;
|
||||
}
|
||||
// Set newly created cell as active cell
|
||||
this.updateActiveCell(cell);
|
||||
|
||||
this._contentChangedEmitter.fire({
|
||||
changeType: NotebookChangeType.CellsAdded,
|
||||
cells: [cell],
|
||||
cellIndex: index
|
||||
});
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
private updateActiveCell(cell: ICellModel) {
|
||||
if (this._activeCell) {
|
||||
this._activeCell.active = false;
|
||||
}
|
||||
this._activeCell = cell;
|
||||
this._activeCell.active = true;
|
||||
}
|
||||
|
||||
private createCell(cellType: CellType): ICellModel {
|
||||
let singleCell: nb.ICellContents = {
|
||||
cell_type: cellType,
|
||||
source: '',
|
||||
metadata: {},
|
||||
execution_count: undefined
|
||||
};
|
||||
return this._notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true });
|
||||
}
|
||||
|
||||
deleteCell(cellModel: ICellModel): void {
|
||||
if (this.inErrorState || !this._cells) {
|
||||
return;
|
||||
}
|
||||
let index = this._cells.findIndex((cell) => cell.equals(cellModel));
|
||||
if (index > -1) {
|
||||
this._cells.splice(index, 1);
|
||||
this._contentChangedEmitter.fire({
|
||||
changeType: NotebookChangeType.CellDeleted,
|
||||
cells: [cellModel],
|
||||
cellIndex: index
|
||||
});
|
||||
} else {
|
||||
this.notifyError(localize('deleteCellFailed', "Failed to delete cell."));
|
||||
}
|
||||
}
|
||||
|
||||
pushEditOperations(edits: ISingleNotebookEditOperation[]): void {
|
||||
if (this.inErrorState || !this._cells) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let edit of edits) {
|
||||
let newCells: ICellModel[] = [];
|
||||
if (edit.cell) {
|
||||
// TODO: should we validate and complete required missing parameters?
|
||||
let contents: nb.ICellContents = edit.cell as nb.ICellContents;
|
||||
newCells.push(this._notebookOptions.factory.createCell(contents, { notebook: this, isTrusted: this._trustedMode }));
|
||||
}
|
||||
this._cells.splice(edit.range.start, edit.range.end - edit.range.start, ...newCells);
|
||||
if (newCells.length > 0) {
|
||||
this.updateActiveCell(newCells[0]);
|
||||
}
|
||||
this._contentChangedEmitter.fire({
|
||||
changeType: NotebookChangeType.CellsAdded
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get activeCell(): ICellModel {
|
||||
return this._activeCell;
|
||||
}
|
||||
|
||||
public set activeCell(value: ICellModel) {
|
||||
this._activeCell = value;
|
||||
}
|
||||
|
||||
private notifyError(error: string): void {
|
||||
this._onErrorEmitter.fire({ message: error, severity: Severity.Error });
|
||||
}
|
||||
|
||||
public async startSession(manager: INotebookManager, displayName?: string, setErrorStateOnFail?: boolean): Promise<void> {
|
||||
if (displayName) {
|
||||
let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName);
|
||||
this._defaultKernel = displayName ? { name: standardKernel.name, display_name: standardKernel.displayName } : this._defaultKernel;
|
||||
}
|
||||
if (this._defaultKernel) {
|
||||
let clientSession = this._notebookOptions.factory.createClientSession({
|
||||
notebookUri: this._notebookOptions.notebookUri,
|
||||
notebookManager: manager,
|
||||
notificationService: this._notebookOptions.notificationService,
|
||||
kernelSpec: this._defaultKernel
|
||||
});
|
||||
if (!this._activeClientSession) {
|
||||
this.updateActiveClientSession(clientSession);
|
||||
}
|
||||
let profile = new ConnectionProfile(this._notebookOptions.capabilitiesService, this.connectionProfile);
|
||||
|
||||
// TODO: this code needs to be fixed since it is called before the this._savedKernelInfo is set.
|
||||
// This means it always fails, and we end up using the default connection instead. If you right-click
|
||||
// and run "New Notebook" on a disconnected server this means you get the wrong connection (global active)
|
||||
// instead of the one you chose, or it'll fail to connect in general
|
||||
if (this.isValidConnection(profile)) {
|
||||
this._activeConnection = profile;
|
||||
} else {
|
||||
this._activeConnection = undefined;
|
||||
}
|
||||
|
||||
clientSession.onKernelChanging(async (e) => {
|
||||
await this.loadActiveContexts(e);
|
||||
});
|
||||
clientSession.statusChanged(async (session) => {
|
||||
this._kernelsChangedEmitter.fire(session.kernel);
|
||||
});
|
||||
await clientSession.initialize();
|
||||
// By somehow we have to wait for ready, otherwise may not be called for some cases.
|
||||
await clientSession.ready;
|
||||
if (clientSession.kernel) {
|
||||
await clientSession.kernel.ready;
|
||||
await this.updateKernelInfoOnKernelChange(clientSession.kernel);
|
||||
}
|
||||
if (clientSession.isInErrorState) {
|
||||
if (setErrorStateOnFail) {
|
||||
this.setErrorState(clientSession.errorMessage);
|
||||
} else {
|
||||
throw new Error(clientSession.errorMessage);
|
||||
}
|
||||
}
|
||||
this._onClientSessionReady.fire(clientSession);
|
||||
this._kernelChangedEmitter.fire({
|
||||
oldValue: undefined,
|
||||
newValue: clientSession.kernel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When changing kernel, update the active session and register the kernel change event
|
||||
// So KernelDropDown could get the event fired when added listerner on Model.KernelChange
|
||||
private updateActiveClientSession(clientSession: IClientSession) {
|
||||
this.clearClientSessionListeners();
|
||||
this._activeClientSession = clientSession;
|
||||
this._clientSessionListeners.push(this._activeClientSession.kernelChanged(e => this._kernelChangedEmitter.fire(e)));
|
||||
}
|
||||
|
||||
private clearClientSessionListeners() {
|
||||
this._clientSessionListeners.forEach(listener => listener.dispose());
|
||||
this._clientSessionListeners = [];
|
||||
}
|
||||
|
||||
public setDefaultKernelAndProviderId() {
|
||||
if (this._savedKernelInfo) {
|
||||
this.sanitizeSavedKernelInfo();
|
||||
let provider = this._kernelDisplayNameToNotebookProviderIds.get(this._savedKernelInfo.display_name);
|
||||
if (provider && provider !== this._providerId) {
|
||||
this._providerId = provider;
|
||||
}
|
||||
this._defaultKernel = this._savedKernelInfo;
|
||||
} else if (this._defaultKernel) {
|
||||
let providerId = this._kernelDisplayNameToNotebookProviderIds.get(this._defaultKernel.display_name);
|
||||
if (providerId) {
|
||||
if (this._providerId !== providerId) {
|
||||
this._providerId = providerId;
|
||||
}
|
||||
} else {
|
||||
this._defaultKernel = notebookConstants.sqlKernelSpec;
|
||||
this._providerId = SQL_NOTEBOOK_PROVIDER;
|
||||
}
|
||||
} else {
|
||||
this._defaultKernel = notebookConstants.sqlKernelSpec;
|
||||
this._providerId = SQL_NOTEBOOK_PROVIDER;
|
||||
}
|
||||
// update default language
|
||||
this._defaultLanguageInfo = {
|
||||
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
|
||||
version: ''
|
||||
};
|
||||
}
|
||||
|
||||
private isValidConnection(profile: IConnectionProfile | connection.Connection) {
|
||||
let standardKernels = this._notebookOptions.standardKernels.find(kernel => this._defaultKernel && kernel.displayName === this._defaultKernel.display_name);
|
||||
let connectionProviderIds = standardKernels ? standardKernels.connectionProviderIds : undefined;
|
||||
return profile && connectionProviderIds && connectionProviderIds.find(provider => provider === profile.providerName) !== undefined;
|
||||
}
|
||||
|
||||
public getStandardKernelFromName(name: string): notebookUtils.IStandardKernelWithProvider {
|
||||
if (name) {
|
||||
let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.name.toLowerCase() === name.toLowerCase());
|
||||
return kernel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getStandardKernelFromDisplayName(displayName: string): notebookUtils.IStandardKernelWithProvider {
|
||||
if (displayName) {
|
||||
let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName.toLowerCase() === displayName.toLowerCase());
|
||||
return kernel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
public get languageInfo(): nb.ILanguageInfo {
|
||||
return this._defaultLanguageInfo;
|
||||
}
|
||||
|
||||
public get language(): string {
|
||||
return this._language;
|
||||
}
|
||||
|
||||
private updateLanguageInfo(info: nb.ILanguageInfo) {
|
||||
if (info) {
|
||||
this._defaultLanguageInfo = info;
|
||||
this.trySetLanguageFromLangInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private trySetLanguageFromLangInfo() {
|
||||
// In languageInfo, set the language to the "name" property
|
||||
// If the "name" property isn't defined, check the "mimeType" property
|
||||
// Otherwise, default to python as the language
|
||||
let languageInfo = this.languageInfo;
|
||||
let language: string;
|
||||
if (languageInfo) {
|
||||
if (languageInfo.codemirror_mode) {
|
||||
let codeMirrorMode: nb.ICodeMirrorMode = <nb.ICodeMirrorMode>(languageInfo.codemirror_mode);
|
||||
if (codeMirrorMode && codeMirrorMode.name) {
|
||||
language = codeMirrorMode.name;
|
||||
}
|
||||
}
|
||||
if (!language && languageInfo.name) {
|
||||
language = languageInfo.name;
|
||||
}
|
||||
if (!language && languageInfo.mimetype) {
|
||||
language = languageInfo.mimetype;
|
||||
}
|
||||
}
|
||||
|
||||
if (language) {
|
||||
let mimeTypePrefix = 'x-';
|
||||
if (language.includes(mimeTypePrefix)) {
|
||||
language = language.replace(mimeTypePrefix, '');
|
||||
} else if (language.toLowerCase() === 'ipython') {
|
||||
// Special case ipython because in many cases this is defined as the code mirror mode for python notebooks
|
||||
language = 'python';
|
||||
}
|
||||
}
|
||||
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
public changeKernel(displayName: string): void {
|
||||
this._contextsLoadingEmitter.fire();
|
||||
this.doChangeKernel(displayName, true);
|
||||
}
|
||||
|
||||
private async doChangeKernel(displayName: string, mustSetProvider: boolean = true, restoreOnFail: boolean = true): Promise<void> {
|
||||
if (!displayName) {
|
||||
// Can't change to an undefined kernel
|
||||
return;
|
||||
}
|
||||
let oldDisplayName = this._activeClientSession && this._activeClientSession.kernel ? this._activeClientSession.kernel.name : undefined;
|
||||
try {
|
||||
let changeKernelNeeded = true;
|
||||
if (mustSetProvider) {
|
||||
let providerChanged = await this.tryStartSessionByChangingProviders(displayName);
|
||||
// If provider was changed, a new session with new kernel is already created. We can skip calling changeKernel.
|
||||
changeKernelNeeded = !providerChanged;
|
||||
}
|
||||
if (changeKernelNeeded) {
|
||||
let spec = this.findSpec(displayName);
|
||||
if (this._activeClientSession && this._activeClientSession.isReady) {
|
||||
let kernel = await this._activeClientSession.changeKernel(spec, this._oldKernel);
|
||||
try {
|
||||
await kernel.ready;
|
||||
await this.updateKernelInfoOnKernelChange(kernel);
|
||||
} catch (err2) {
|
||||
// TODO should we handle this in any way?
|
||||
console.log(`doChangeKernel: ignoring error ${notebookUtils.getErrorMessage(err2)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (oldDisplayName && restoreOnFail) {
|
||||
this.notifyError(localize('changeKernelFailedRetry', "Failed to change kernel. Kernel {0} will be used. Error was: {1}", oldDisplayName, notebookUtils.getErrorMessage(err)));
|
||||
// Clear out previous kernel
|
||||
let failedProviderId = this.tryFindProviderForKernel(displayName, true);
|
||||
let oldProviderId = this.tryFindProviderForKernel(oldDisplayName, true);
|
||||
if (failedProviderId !== oldProviderId) {
|
||||
// We need to clear out the old kernel information so we switch providers. Otherwise in the SQL -> Jupyter -> SQL failure case,
|
||||
// we would never reset the providers
|
||||
this._oldKernel = undefined;
|
||||
}
|
||||
return this.doChangeKernel(oldDisplayName, mustSetProvider, false);
|
||||
} else {
|
||||
this.notifyError(localize('changeKernelFailed', "Failed to change kernel due to error: {0}", notebookUtils.getErrorMessage(err)));
|
||||
this._kernelChangedEmitter.fire({
|
||||
newValue: undefined,
|
||||
oldValue: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
// Else no need to do anything
|
||||
}
|
||||
|
||||
private async updateKernelInfoOnKernelChange(kernel: nb.IKernel) {
|
||||
await this.updateKernelInfo(kernel);
|
||||
if (kernel.info) {
|
||||
this.updateLanguageInfo(kernel.info.language_info);
|
||||
}
|
||||
}
|
||||
|
||||
private findSpec(displayName: string) {
|
||||
let spec = this.getKernelSpecFromDisplayName(displayName);
|
||||
if (spec) {
|
||||
// Ensure that the kernel we try to switch to is a valid kernel; if not, use the default
|
||||
let kernelSpecs = this.getKernelSpecs();
|
||||
if (kernelSpecs && kernelSpecs.length > 0 && kernelSpecs.findIndex(k => k.display_name === spec.display_name) < 0) {
|
||||
spec = kernelSpecs.find(spec => spec.name === this.notebookManager.sessionManager.specs.defaultKernel);
|
||||
}
|
||||
}
|
||||
else {
|
||||
spec = notebookConstants.sqlKernelSpec;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
public async changeContext(title: string, newConnection?: ConnectionProfile, hideErrorMessage?: boolean): Promise<void> {
|
||||
try {
|
||||
if (!newConnection) {
|
||||
newConnection = this._activeContexts.otherConnections.find((connection) => connection.title === title);
|
||||
}
|
||||
if ((!newConnection) && (this._activeContexts.defaultConnection.title === title)) {
|
||||
newConnection = this._activeContexts.defaultConnection;
|
||||
}
|
||||
|
||||
if (newConnection) {
|
||||
if (this._activeConnection && this._activeConnection.id !== newConnection.id) {
|
||||
this._otherConnections.push(this._activeConnection);
|
||||
}
|
||||
this._activeConnection = newConnection;
|
||||
this.refreshConnections(newConnection);
|
||||
this._activeClientSession.updateConnection(newConnection.toIConnectionProfile()).then(
|
||||
result => {
|
||||
//Remove 'Select connection' from 'Attach to' drop-down since its a valid connection
|
||||
this._onValidConnectionSelected.fire(true);
|
||||
},
|
||||
error => {
|
||||
if (error) {
|
||||
if (!hideErrorMessage) {
|
||||
this.notifyError(notebookUtils.getErrorMessage(error));
|
||||
}
|
||||
//Selected a wrong connection, Attach to should be defaulted with 'Select connection'
|
||||
this._onValidConnectionSelected.fire(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._onValidConnectionSelected.fire(false);
|
||||
}
|
||||
} catch (err) {
|
||||
let msg = notebookUtils.getErrorMessage(err);
|
||||
this.notifyError(localize('changeContextFailed', "Changing context failed: {0}", msg));
|
||||
}
|
||||
}
|
||||
|
||||
private refreshConnections(newConnection: ConnectionProfile) {
|
||||
if (this.isValidConnection(newConnection) &&
|
||||
this._activeConnection.id !== '-1' &&
|
||||
this._activeConnection.id !== this._activeContexts.defaultConnection.id) {
|
||||
// Put the defaultConnection to the head of otherConnections
|
||||
if (this.isValidConnection(this._activeContexts.defaultConnection)) {
|
||||
this._activeContexts.otherConnections = this._activeContexts.otherConnections.filter(conn => conn.id !== this._activeContexts.defaultConnection.id);
|
||||
this._activeContexts.otherConnections.unshift(this._activeContexts.defaultConnection);
|
||||
}
|
||||
// Change the defaultConnection to newConnection
|
||||
this._activeContexts.defaultConnection = newConnection;
|
||||
}
|
||||
}
|
||||
|
||||
// Get default language if saved in notebook file
|
||||
// Otherwise, default to python
|
||||
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
|
||||
return (notebook && notebook.metadata && notebook.metadata.language_info) ? notebook.metadata.language_info : {
|
||||
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
|
||||
version: '',
|
||||
mimetype: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'x-sql' : 'x-python'
|
||||
};
|
||||
}
|
||||
|
||||
// Get default kernel info if saved in notebook file
|
||||
private getSavedKernelInfo(notebook: nb.INotebookContents): nb.IKernelInfo {
|
||||
return (notebook && notebook.metadata && notebook.metadata.kernelspec) ? notebook.metadata.kernelspec : undefined;
|
||||
}
|
||||
|
||||
private getKernelSpecFromDisplayName(displayName: string): nb.IKernelSpec {
|
||||
displayName = this.sanitizeDisplayName(displayName);
|
||||
let kernel: nb.IKernelSpec = this.specs.kernels.find(k => k.display_name.toLowerCase() === displayName.toLowerCase());
|
||||
if (!kernel) {
|
||||
return undefined; // undefined is handled gracefully in the session to default to the default kernel
|
||||
} else if (!kernel.name) {
|
||||
kernel.name = this.specs.defaultKernel;
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private sanitizeSavedKernelInfo() {
|
||||
if (this._savedKernelInfo) {
|
||||
let displayName = this.sanitizeDisplayName(this._savedKernelInfo.display_name);
|
||||
|
||||
if (this._savedKernelInfo.display_name !== displayName) {
|
||||
this._savedKernelInfo.display_name = displayName;
|
||||
}
|
||||
|
||||
let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName));
|
||||
if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) {
|
||||
this._savedKernelInfo.name = standardKernel.name;
|
||||
this._savedKernelInfo.display_name = standardKernel.displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getDisplayNameFromSpecName(kernel: nb.IKernel): string {
|
||||
let specs = this.notebookManager.sessionManager.specs;
|
||||
if (!specs || !specs.kernels) {
|
||||
return kernel.name;
|
||||
}
|
||||
let newKernel = this.notebookManager.sessionManager.specs.kernels.find(k => k.name === kernel.name);
|
||||
let newKernelDisplayName;
|
||||
if (newKernel) {
|
||||
newKernelDisplayName = newKernel.display_name;
|
||||
}
|
||||
return newKernelDisplayName;
|
||||
}
|
||||
|
||||
public addAttachToConnectionsToBeDisposed(connUri: string) {
|
||||
this._connectionUrisToDispose.push(connUri);
|
||||
}
|
||||
|
||||
private setErrorState(errMsg: string): void {
|
||||
this._inErrorState = true;
|
||||
let msg = localize('startSessionFailed', "Could not start session: {0}", errMsg);
|
||||
this.notifyError(msg);
|
||||
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.disconnectAttachToConnections();
|
||||
this.handleClosed();
|
||||
}
|
||||
|
||||
public async handleClosed(): Promise<void> {
|
||||
try {
|
||||
if (this.notebookOptions && this.notebookOptions.connectionService) {
|
||||
if (this._otherConnections) {
|
||||
notebookUtils.asyncForEach(this._otherConnections, async (conn) => {
|
||||
await this.disconnectNotebookConnection(conn);
|
||||
});
|
||||
this._otherConnections = [];
|
||||
}
|
||||
if (this._activeConnection) {
|
||||
await this.disconnectNotebookConnection(this._activeConnection);
|
||||
this._activeConnection = undefined;
|
||||
}
|
||||
}
|
||||
await this.shutdownActiveSession();
|
||||
} catch (err) {
|
||||
this.notifyError(localize('shutdownError', "An error occurred when closing the notebook: {0}", err));
|
||||
}
|
||||
}
|
||||
|
||||
private async shutdownActiveSession() {
|
||||
if (this._activeClientSession) {
|
||||
try {
|
||||
await this._activeClientSession.ready;
|
||||
}
|
||||
catch (err) {
|
||||
this.notifyError(localize('shutdownClientSessionError', "A client session error occurred when closing the notebook: {0}", err));
|
||||
}
|
||||
await this._activeClientSession.shutdown();
|
||||
this.clearClientSessionListeners();
|
||||
this._activeClientSession = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise<void> {
|
||||
if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) {
|
||||
let kernelDisplayName = this.getDisplayNameFromSpecName(kernelChangedArgs.newValue);
|
||||
this._activeContexts = await NotebookContexts.getContextsForKernel(this._notebookOptions.connectionService, this.getApplicableConnectionProviderIds(kernelDisplayName), kernelChangedArgs, this.connectionProfile);
|
||||
this._contextsChangedEmitter.fire();
|
||||
if (this.contexts.defaultConnection !== undefined && this.contexts.defaultConnection.serverName !== undefined && this.contexts.defaultConnection.title !== undefined) {
|
||||
await this.changeContext(this.contexts.defaultConnection.title, this.contexts.defaultConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes display name to remove IP address in order to fairly compare kernels
|
||||
* In some notebooks, display name is in the format <kernel> (<ip address>)
|
||||
* example: PySpark (25.23.32.4)
|
||||
* @param displayName Display Name for the kernel
|
||||
*/
|
||||
public sanitizeDisplayName(displayName: string): string {
|
||||
let name = displayName;
|
||||
if (name) {
|
||||
let index = name.indexOf('(');
|
||||
name = (index > -1) ? name.substr(0, index - 1).trim() : name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private async updateKernelInfo(kernel: nb.IKernel): Promise<void> {
|
||||
if (kernel) {
|
||||
try {
|
||||
let spec = await kernel.getSpec();
|
||||
this._savedKernelInfo = {
|
||||
name: kernel.name,
|
||||
display_name: spec.display_name,
|
||||
language: spec.language
|
||||
};
|
||||
this.clientSession.configureKernel(this._savedKernelInfo);
|
||||
} catch (err) {
|
||||
// Don't worry about this for now. Just use saved values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set _providerId and start session if it is new provider
|
||||
* @param displayName Kernel dispay name
|
||||
*/
|
||||
private async tryStartSessionByChangingProviders(displayName: string): Promise<boolean> {
|
||||
if (displayName) {
|
||||
if (this._activeClientSession && this._activeClientSession.isReady) {
|
||||
this._oldKernel = this._activeClientSession.kernel;
|
||||
}
|
||||
let providerId = this.tryFindProviderForKernel(displayName);
|
||||
|
||||
if (providerId && providerId !== this._providerId) {
|
||||
this._providerId = providerId;
|
||||
this._onProviderIdChanged.fire(this._providerId);
|
||||
|
||||
await this.shutdownActiveSession();
|
||||
let manager = this.getNotebookManager(providerId);
|
||||
if (manager) {
|
||||
await this.startSession(manager, displayName, false);
|
||||
} else {
|
||||
throw new Error(localize('ProviderNoManager', "Can't find notebook manager for provider {0}", providerId));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private tryFindProviderForKernel(displayName: string, alwaysReturnId: boolean = false): string {
|
||||
if (!displayName) {
|
||||
return undefined;
|
||||
}
|
||||
let standardKernel = this.getStandardKernelFromDisplayName(displayName);
|
||||
if (standardKernel) {
|
||||
let providerId = this._kernelDisplayNameToNotebookProviderIds.get(displayName);
|
||||
if (alwaysReturnId || (!this._oldKernel || this._oldKernel.name !== standardKernel.name)) {
|
||||
return providerId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get kernel specs from current sessionManager
|
||||
private getKernelSpecs(): nb.IKernelSpec[] {
|
||||
if (this.notebookManager && this.notebookManager.sessionManager && this.notebookManager.sessionManager.specs &&
|
||||
this.notebookManager.sessionManager.specs.kernels) {
|
||||
return this.notebookManager.sessionManager.specs.kernels;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check for and disconnect from any new connections opened while in the notebook
|
||||
// Note: notebooks should always connect with the connection URI in the following format,
|
||||
// so that connections can be tracked accordingly throughout ADS:
|
||||
// let connectionUri = Utils.generateUri(connection, 'notebook');
|
||||
private async disconnectNotebookConnection(conn: ConnectionProfile): Promise<void> {
|
||||
if (this.notebookOptions.connectionService.getConnectionUri(conn).includes(uriPrefixes.notebook)) {
|
||||
let uri = this._notebookOptions.connectionService.getConnectionUri(conn);
|
||||
await this.notebookOptions.connectionService.disconnect(uri).catch(e => console.log(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect any connections that were added through the "Add new connection" functionality in the Attach To dropdown
|
||||
private async disconnectAttachToConnections(): Promise<void> {
|
||||
notebookUtils.asyncForEach(this._connectionUrisToDispose, async conn => {
|
||||
await this.notebookOptions.connectionService.disconnect(conn).catch(e => console.log(e));
|
||||
});
|
||||
this._connectionUrisToDispose = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the model to JSON.
|
||||
*/
|
||||
toJSON(): nb.INotebookContents {
|
||||
let cells: nb.ICellContents[] = this.cells.map(c => c.toJSON());
|
||||
let metadata = Object.create(null) as nb.INotebookMetadata;
|
||||
// TODO update language and kernel when these change
|
||||
metadata.kernelspec = this._savedKernelInfo;
|
||||
metadata.language_info = this.languageInfo;
|
||||
return {
|
||||
metadata,
|
||||
nbformat_minor: this._nbformatMinor,
|
||||
nbformat: this._nbformat,
|
||||
cells
|
||||
};
|
||||
}
|
||||
|
||||
onCellChange(cell: ICellModel, change: NotebookChangeType): void {
|
||||
let changeInfo: NotebookContentChange = {
|
||||
changeType: change,
|
||||
cells: [cell]
|
||||
};
|
||||
switch (change) {
|
||||
case NotebookChangeType.CellOutputUpdated:
|
||||
case NotebookChangeType.CellSourceUpdated:
|
||||
changeInfo.changeType = NotebookChangeType.DirtyStateChanged;
|
||||
changeInfo.isDirty = true;
|
||||
break;
|
||||
default:
|
||||
// Do nothing for now
|
||||
}
|
||||
this._contentChangedEmitter.fire(changeInfo);
|
||||
}
|
||||
|
||||
}
|
||||
23
src/sql/workbench/parts/notebook/notebook.component.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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" (click)="unselectActiveCell()" (scroll)="scrollHandler($event)">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell, $event)" [class.active]="cell.active">
|
||||
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||
</code-cell-component>
|
||||
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||
</text-cell-component>
|
||||
</div>
|
||||
<div class="notebook-cell" *ngIf="(!cells || !cells.length) && !isLoading">
|
||||
<placeholder-cell-component [cellModel]="cell" [model]="model">
|
||||
</placeholder-cell-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
516
src/sql/workbench/parts/notebook/notebook.component.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, 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, IActionItem } from 'vs/base/common/actions';
|
||||
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { fillInActions, LabeledMenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { ICellModel, IModelFactory, INotebookModel, NotebookContentChange } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/workbench/parts/notebook/models/modelFactory';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, RunAllCellsAction, ClearAllOutputsAction } from 'sql/workbench/parts/notebook/notebookActions';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { CellMagicMapper } from 'sql/workbench/parts/notebook/models/cellMagicMapper';
|
||||
import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { CellModel } from 'sql/workbench/parts/notebook/models/cell';
|
||||
|
||||
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;
|
||||
private _model: NotebookModel;
|
||||
private _isInErrorState: boolean = false;
|
||||
private _errorMessage: string;
|
||||
protected _actionBar: Taskbar;
|
||||
protected isLoading: boolean;
|
||||
private notebookManagers: INotebookManager[] = [];
|
||||
private _modelReadyDeferred = new Deferred<NotebookModel>();
|
||||
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
|
||||
private profile: IConnectionProfile;
|
||||
private _trustedAction: TrustedAction;
|
||||
private _runAllCellsAction: RunAllCellsAction;
|
||||
private _providerRelatedActions: IAction[] = [];
|
||||
private _scrollTop: number;
|
||||
|
||||
|
||||
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
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
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 modelRegistered(): Promise<NotebookModel> {
|
||||
return this._modelRegisteredDeferred.promise;
|
||||
}
|
||||
|
||||
public get cells(): ICellModel[] {
|
||||
return this._model ? this._model.cells : [];
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let toolbarEl = <HTMLElement>this.toolbar.nativeElement;
|
||||
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
public selectCell(cell: ICellModel, event?: Event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (cell !== this.model.activeCell) {
|
||||
if (this.model.activeCell) {
|
||||
this.model.activeCell.active = false;
|
||||
}
|
||||
this._model.activeCell = cell;
|
||||
this._model.activeCell.active = true;
|
||||
this.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
//Saves scrollTop value on scroll change
|
||||
public scrollHandler(event: Event) {
|
||||
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
|
||||
}
|
||||
|
||||
public unselectActiveCell() {
|
||||
if (this.model && this.model.activeCell) {
|
||||
this.model.activeCell.active = false;
|
||||
this.model.activeCell = undefined;
|
||||
}
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
// Add cell based on cell type
|
||||
public addCell(cellType: CellType) {
|
||||
this._model.addCell(cellType);
|
||||
}
|
||||
|
||||
// Updates Notebook model's trust details
|
||||
public updateModelTrustDetails(isTrusted: boolean) {
|
||||
this._model.trustedMode = isTrusted;
|
||||
this._model.cells.forEach(cell => {
|
||||
cell.trustedMode = isTrusted;
|
||||
});
|
||||
//Updates dirty state
|
||||
this._notebookParams.input && this._notebookParams.input.updateModel();
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
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.setNotebookManager();
|
||||
await this.loadModel();
|
||||
this.setLoading(false);
|
||||
this._modelReadyDeferred.resolve(this._model);
|
||||
} catch (error) {
|
||||
this.setViewInErrorState(localize('displayFailed', 'Could not display contents: {0}', notebookUtils.getErrorMessage(error)));
|
||||
this.setLoading(false);
|
||||
this._modelReadyDeferred.reject(error);
|
||||
} finally {
|
||||
// Always add the editor for now to close loop, even if loading contents failed
|
||||
this.notebookService.addNotebookEditor(this);
|
||||
}
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.isLoading = isLoading;
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
await this.awaitNonDefaultProvider();
|
||||
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,
|
||||
standardKernels: this._notebookParams.input.standardKernels,
|
||||
cellMagicMapper: new CellMagicMapper(this.notebookService.languageMagics),
|
||||
providerId: 'sql', // this is tricky; really should also depend on the connection profile
|
||||
defaultKernel: this._notebookParams.input.defaultKernel,
|
||||
layoutChanged: this._notebookParams.input.layoutChanged,
|
||||
capabilitiesService: this.capabilitiesService
|
||||
}, false, this.profile);
|
||||
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
|
||||
await model.requestModelLoad(this._notebookParams.isTrusted);
|
||||
model.contentChanged((change) => this.handleContentChanged(change));
|
||||
model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider));
|
||||
this._model = this._register(model);
|
||||
this.updateToolbarComponents(this._model.trustedMode);
|
||||
this._modelRegisteredDeferred.resolve(this._model);
|
||||
await model.startSession(this.model.notebookManager, undefined, true);
|
||||
this.setContextKeyServiceWithProviderId(model.providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
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;
|
||||
// Refresh the provider if we had been using default
|
||||
let providerInfo = await this._notebookParams.providerInfo;
|
||||
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
|
||||
let providers = notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
|
||||
let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER);
|
||||
providerInfo.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0];
|
||||
}
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
|
||||
// If it's still the default, warn them they should install an extension
|
||||
this.notificationService.prompt(Severity.Warning,
|
||||
localize('noKernelInstalled', 'Please install the SQL Server 2019 extension to run cells'),
|
||||
[{
|
||||
label: localize('installSql2019Extension', 'Install Extension'),
|
||||
run: () => this.openExtensionGallery()
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private async openExtensionGallery(): Promise<void> {
|
||||
try {
|
||||
let viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true) as IExtensionsViewlet;
|
||||
viewlet.search('sql-vnext');
|
||||
viewlet.focus();
|
||||
} catch (error) {
|
||||
this.notificationService.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates toolbar components
|
||||
private updateToolbarComponents(isTrusted: boolean) {
|
||||
if (this._trustedAction) {
|
||||
this._trustedAction.enabled = true;
|
||||
this._trustedAction.trusted = isTrusted;
|
||||
}
|
||||
}
|
||||
|
||||
private get modelFactory(): IModelFactory {
|
||||
if (!this._notebookParams.modelFactory) {
|
||||
this._notebookParams.modelFactory = new ModelFactory();
|
||||
}
|
||||
return this._notebookParams.modelFactory;
|
||||
}
|
||||
|
||||
private handleModelError(notification: INotification): void {
|
||||
this.notificationService.notify(notification);
|
||||
}
|
||||
|
||||
private handleContentChanged(change: NotebookContentChange) {
|
||||
// Note: for now we just need to set dirty state and refresh the UI.
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private handleProviderIdChanged(providerId: string) {
|
||||
// If there are any actions that were related to the previous provider,
|
||||
// disable them in the actionBar
|
||||
this._providerRelatedActions.forEach(action => {
|
||||
action.enabled = false;
|
||||
});
|
||||
this.setContextKeyServiceWithProviderId(providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
}
|
||||
|
||||
findCellIndex(cellModel: ICellModel): number {
|
||||
return this._model.cells.findIndex((cell) => cell.id === cellModel.id);
|
||||
}
|
||||
|
||||
private setViewInErrorState(error: any): any {
|
||||
this._isInErrorState = true;
|
||||
this._errorMessage = notebookUtils.getErrorMessage(error);
|
||||
// For now, send message as error notification #870 covers having dedicated area for this
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
|
||||
protected initActionBar() {
|
||||
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);
|
||||
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 = new RunAllCellsAction('notebook.runAllCells', localize('runAll', 'Run Cells'), 'notebook-button icon-run-cells');
|
||||
let clearResultsButton = new ClearAllOutputsAction('notebook.ClearAllOutputs', localize('clearResults', 'Clear Results'), 'notebook-button icon-clear-results');
|
||||
|
||||
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
|
||||
this._trustedAction.enabled = false;
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbar.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar, { actionItemProvider: action => this.actionItemProvider(action as Action) });
|
||||
this._actionBar.context = this;
|
||||
this._actionBar.setContent([
|
||||
{ element: kernelContainer },
|
||||
{ element: attachToContainer },
|
||||
{ action: addCodeCellButton },
|
||||
{ action: addTextCellButton },
|
||||
{ action: this._trustedAction },
|
||||
{ action: this._runAllCellsAction },
|
||||
{ action: clearResultsButton }
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private actionItemProvider(action: Action): IActionItem {
|
||||
// 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.notificationService, this.contextMenuService, 'notebook-button');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the menu contributions that use the ID 'notebook/toolbar'.
|
||||
* Then, find all groups (currently we don't leverage the contributed
|
||||
* groups functionality for the notebook toolbar), and fill in the 'primary'
|
||||
* array with items that don't list a group. Finally, add any actions from
|
||||
* the primary array to the end of the toolbar.
|
||||
*/
|
||||
private fillInActionsForCurrentContext(): void {
|
||||
let primary: IAction[] = [];
|
||||
let secondary: IAction[] = [];
|
||||
let notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService);
|
||||
let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true });
|
||||
fillInActions(groups, { primary, secondary }, false, (group: string) => group === undefined || group === '');
|
||||
this.addPrimaryContributedActions(primary);
|
||||
}
|
||||
|
||||
private detectChanges(): void {
|
||||
if (!(this._changeRef['destroyed'])) {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private addPrimaryContributedActions(primary: IAction[]) {
|
||||
for (let action of primary) {
|
||||
// Need to ensure that we don't add the same action multiple times
|
||||
let foundIndex = this._providerRelatedActions.findIndex(act => act.id === action.id);
|
||||
if (foundIndex < 0) {
|
||||
this._actionBar.addAction(action);
|
||||
this._providerRelatedActions.push(action);
|
||||
} else {
|
||||
this._providerRelatedActions[foundIndex].enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setContextKeyServiceWithProviderId(providerId: string) {
|
||||
let provider = new RawContextKey<string>('providerId', providerId);
|
||||
provider.bindTo(this.contextKeyService);
|
||||
}
|
||||
|
||||
public get notebookParams(): INotebookParams {
|
||||
return this._notebookParams;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._notebookParams.notebookUri.toString();
|
||||
}
|
||||
|
||||
public get modelReady(): Promise<INotebookModel> {
|
||||
return this._modelReadyDeferred.promise;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.editorService.activeEditor === this.notebookParams.input;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
let notebookEditor = this.notebookParams.input;
|
||||
return this.editorService.visibleEditors.some(e => e === notebookEditor);
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.notebookParams.input.isDirty();
|
||||
}
|
||||
|
||||
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
|
||||
if (!edits || edits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
this._model.pushEditOperations(edits);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async runCell(cell: ICellModel): Promise<boolean> {
|
||||
await this.modelReady;
|
||||
let uriString = cell.cellUri.toString();
|
||||
if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
|
||||
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(): Promise<boolean> {
|
||||
await this.modelReady;
|
||||
let codeCells = this._model.cells.filter(cell => cell.cellType === CellTypes.Code);
|
||||
if (codeCells && codeCells.length) {
|
||||
for (let i = 0; i < codeCells.length; i++) {
|
||||
let cellStatus = await this.runCell(codeCells[i]);
|
||||
if (!cellStatus) {
|
||||
return Promise.reject(new Error(localize('cellRunFailed', 'running cell id {0} failed', codeCells[i].id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
src/sql/workbench/parts/notebook/notebook.contribution.ts
Normal file
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
|
||||
import { NotebookEditor } from 'sql/workbench/parts/notebook/notebookEditor';
|
||||
|
||||
// Model View editor registration
|
||||
const viewModelEditorDescriptor = new EditorDescriptor(
|
||||
NotebookEditor,
|
||||
NotebookEditor.ID,
|
||||
'Notebook'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]);
|
||||
110
src/sql/workbench/parts/notebook/notebook.css
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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: 10px 20px 10px;
|
||||
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: 0px;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-left: 15px;
|
||||
background-size: 13px;
|
||||
margin-right: 0.3em;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.notebookEditor .monaco-select-box {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.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-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.icon.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;
|
||||
}
|
||||
79
src/sql/workbench/parts/notebook/notebook.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
|
||||
import { IBootstrapParams, ISelector, providerIterator } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component';
|
||||
import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component';
|
||||
import { NotebookComponent } from 'sql/workbench/parts/notebook/notebook.component';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CodeComponent } from 'sql/workbench/parts/notebook/cellViews/code.component';
|
||||
import { CodeCellComponent } from 'sql/workbench/parts/notebook/cellViews/codeCell.component';
|
||||
import { TextCellComponent } from 'sql/workbench/parts/notebook/cellViews/textCell.component';
|
||||
import { OutputAreaComponent } from 'sql/workbench/parts/notebook/cellViews/outputArea.component';
|
||||
import { OutputComponent } from 'sql/workbench/parts/notebook/cellViews/output.component';
|
||||
import { PlaceholderCellComponent } from 'sql/workbench/parts/notebook/cellViews/placeholderCell.component';
|
||||
import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component';
|
||||
|
||||
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
|
||||
@NgModule({
|
||||
declarations: [
|
||||
Checkbox,
|
||||
SelectBox,
|
||||
EditableDropDown,
|
||||
InputBox,
|
||||
LoadingSpinner,
|
||||
CodeComponent,
|
||||
CodeCellComponent,
|
||||
TextCellComponent,
|
||||
PlaceholderCellComponent,
|
||||
NotebookComponent,
|
||||
ComponentHostDirective,
|
||||
OutputAreaComponent,
|
||||
OutputComponent
|
||||
],
|
||||
entryComponents: [NotebookComponent],
|
||||
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;
|
||||
};
|
||||
510
src/sql/workbench/parts/notebook/notebookActions.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { INotebookModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { CellType, CellTypes } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { NotebookComponent } from 'sql/workbench/parts/notebook/notebook.component';
|
||||
import { getErrorMessage, getServerFromFormattedAttachToName, getDatabaseFromFormattedAttachToName } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { noKernel } from 'sql/workbench/services/notebook/common/sessionManager';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
||||
import { generateUri } from 'sql/platform/connection/common/utils';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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, '');
|
||||
}
|
||||
|
||||
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>) {
|
||||
super(id, '');
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
|
||||
private updateLabelAndIcon() {
|
||||
this.label = this.states.label;
|
||||
this.tooltip = 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.updateModelTrustDetails(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
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
public run(context: NotebookComponent): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
context.runAllCells();
|
||||
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 } as ISelectBoxOptionsWithLabel);
|
||||
|
||||
if (modelReady) {
|
||||
modelReady
|
||||
.then((model) => this.updateModel(model))
|
||||
.catch((err) => {
|
||||
// No-op for now
|
||||
});
|
||||
}
|
||||
|
||||
this.onDidSelect(e => this.doChangeKernel(e.selected));
|
||||
}
|
||||
|
||||
updateModel(model: INotebookModel): void {
|
||||
this.model = model as NotebookModel;
|
||||
this._register(this.model.kernelChanged((changedArgs: azdata.nb.IKernelChangedArgs) => {
|
||||
this.updateKernel(changedArgs.newValue);
|
||||
}));
|
||||
let kernel = this.model.clientSession && this.model.clientSession.kernel;
|
||||
this.updateKernel(kernel);
|
||||
}
|
||||
|
||||
// Update SelectBox values
|
||||
public updateKernel(kernel: azdata.nb.IKernel) {
|
||||
let kernels: string[] = this.model.standardKernelsDisplayName();
|
||||
if (kernel && kernel.isReady) {
|
||||
let standardKernel = this.model.getStandardKernelFromName(kernel.name);
|
||||
|
||||
if (kernels && standardKernel) {
|
||||
let index = kernels.findIndex((kernel => kernel === standardKernel.displayName));
|
||||
this.setOptions(kernels, index);
|
||||
}
|
||||
} else if (this.model.clientSession.isInErrorState) {
|
||||
let noKernelName = localize('noKernel', "No Kernel");
|
||||
kernels.unshift(noKernelName);
|
||||
this.setOptions(kernels, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public doChangeKernel(displayName: string): void {
|
||||
this.setOptions([msgChanging], 0);
|
||||
this.model.changeKernel(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
export class AttachToDropdown extends SelectBox {
|
||||
private model: NotebookModel;
|
||||
|
||||
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IConnectionDialogService private _connectionDialogService: IConnectionDialogService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService) {
|
||||
super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false } 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.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 =>
|
||||
console.log(err));
|
||||
}
|
||||
model.onValidConnectionSelected(validConnection => {
|
||||
this.handleContextsChanged(!validConnection);
|
||||
});
|
||||
}
|
||||
|
||||
private getKernelDisplayName(): string {
|
||||
let kernelDisplayName: string;
|
||||
if (this.model.clientSession && this.model.clientSession.kernel && this.model.clientSession.kernel.name) {
|
||||
let currentKernelName = this.model.clientSession.kernel.name.toLowerCase();
|
||||
let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name && kernel.name.toLowerCase() === currentKernelName);
|
||||
if (currentKernelSpec) {
|
||||
kernelDisplayName = currentKernelSpec.display_name;
|
||||
}
|
||||
}
|
||||
return kernelDisplayName;
|
||||
}
|
||||
|
||||
// Load "Attach To" dropdown with the values corresponding to Kernel dropdown
|
||||
public async loadAttachToDropdown(model: INotebookModel, currentKernel: string, showSelectConnection?: boolean): Promise<void> {
|
||||
let connProviderIds = this.model.getApplicableConnectionProviderIds(currentKernel);
|
||||
if ((connProviderIds && connProviderIds.length === 0) || currentKernel === noKernel) {
|
||||
this.setOptions([msgLocalHost]);
|
||||
}
|
||||
else {
|
||||
let connections = this.getConnections(model);
|
||||
this.enable();
|
||||
if (showSelectConnection) {
|
||||
connections = this.loadWithSelectConnection(connections);
|
||||
}
|
||||
else {
|
||||
if (connections.length === 1 && connections[0] === msgAddNewConnection) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
this.selectWithOptionName(msgSelectConnection);
|
||||
}
|
||||
else {
|
||||
if (!connections.includes(msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
}
|
||||
this.setOptions(connections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadWithSelectConnection(connections: string[]): string[] {
|
||||
if (connections && connections.length > 0) {
|
||||
if (!connections.includes(msgSelectConnection)) {
|
||||
connections.unshift(msgSelectConnection);
|
||||
}
|
||||
this.selectWithOptionName(msgSelectConnection);
|
||||
if (!connections.includes(msgAddNewConnection)) {
|
||||
connections.push(msgAddNewConnection);
|
||||
}
|
||||
this.setOptions(connections);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
//Get connections from context
|
||||
public getConnections(model: INotebookModel): string[] {
|
||||
let otherConnections: ConnectionProfile[] = [];
|
||||
model.contexts.otherConnections.forEach((conn) => { otherConnections.push(conn); });
|
||||
// If current connection connects to master, select the option in the dropdown that doesn't specify a database
|
||||
if (!model.contexts.defaultConnection.databaseName) {
|
||||
this.selectWithOptionName(model.contexts.defaultConnection.serverName);
|
||||
} else {
|
||||
if (model.contexts.defaultConnection) {
|
||||
this.selectWithOptionName(model.contexts.defaultConnection.title ? model.contexts.defaultConnection.title : model.contexts.defaultConnection.serverName);
|
||||
} else {
|
||||
this.select(0);
|
||||
}
|
||||
}
|
||||
otherConnections = this.setConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections);
|
||||
let connections = otherConnections.map((context) => context.title ? context.title : context.serverName);
|
||||
return connections;
|
||||
}
|
||||
|
||||
private setConnectionsList(defaultConnection: ConnectionProfile, otherConnections: ConnectionProfile[]) {
|
||||
if (defaultConnection.serverName !== msgSelectConnection) {
|
||||
otherConnections = otherConnections.filter(conn => conn.id !== defaultConnection.id);
|
||||
otherConnections.unshift(defaultConnection);
|
||||
if (otherConnections.length > 1) {
|
||||
otherConnections = otherConnections.filter(val => val.serverName !== msgSelectConnection);
|
||||
}
|
||||
}
|
||||
return otherConnections;
|
||||
}
|
||||
|
||||
public getSelectedConnection(selection: string): ConnectionProfile {
|
||||
// Find all connections with the the same server as the selected option
|
||||
let connections = this.model.contexts.otherConnections.filter((c) => selection === c.title);
|
||||
// If only one connection exists with the same server name, use that one
|
||||
if (connections.length === 1) {
|
||||
return connections[0];
|
||||
} else {
|
||||
return this.model.contexts.otherConnections.find((c) => selection === c.title);
|
||||
}
|
||||
}
|
||||
|
||||
public doChangeContext(connection?: ConnectionProfile, hideErrorMessage?: boolean): void {
|
||||
if (this.value === msgAddNewConnection) {
|
||||
this.openConnectionDialog();
|
||||
} else {
|
||||
this.model.changeContext(this.value, connection, hideErrorMessage).then(ok => undefined, err => this._notificationService.error(getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open connection dialog
|
||||
* Enter server details and connect to a server from the dialog
|
||||
* Bind the server value to 'Attach To' drop down
|
||||
* Connected server is displayed at the top of drop down
|
||||
**/
|
||||
public async openConnectionDialog(useProfile: boolean = false): Promise<void> {
|
||||
try {
|
||||
await this._connectionDialogService.openDialogAndWait(this._connectionManagementService, { connectionType: 1, providers: this.model.getApplicableConnectionProviderIds(this.model.clientSession.kernel.name) }, useProfile ? this.model.connectionProfile : undefined).then(connection => {
|
||||
let attachToConnections = this.values;
|
||||
if (!connection) {
|
||||
this.loadAttachToDropdown(this.model, this.getKernelDisplayName());
|
||||
this.doChangeContext(undefined, true);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
else {
|
||||
attachToConnections.unshift(connectedServer);
|
||||
}
|
||||
//To ignore n/a after we have at least one valid connection
|
||||
attachToConnections = attachToConnections.filter(val => val !== msgSelectConnection);
|
||||
|
||||
let index = attachToConnections.findIndex((connection => connection === connectedServer));
|
||||
this.setOptions([]);
|
||||
this.setOptions(attachToConnections);
|
||||
if (!index || index < 0 || index >= attachToConnections.length) {
|
||||
index = 0;
|
||||
}
|
||||
this.select(index);
|
||||
|
||||
this.model.addAttachToConnectionsToBeDisposed(connectionUri);
|
||||
// Call doChangeContext to set the newly chosen connection in the model
|
||||
this.doChangeContext(connectionProfile);
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
const actions: INotificationActions = { primary: [] };
|
||||
this._notificationService.notify({ severity: Severity.Error, message: getErrorMessage(error), actions });
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/sql/workbench/parts/notebook/notebookConstants.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export namespace nbversion {
|
||||
/**
|
||||
* The major version of the notebook format.
|
||||
*/
|
||||
export const MAJOR_VERSION: number = 4;
|
||||
|
||||
/**
|
||||
* The minor version of the notebook format.
|
||||
*/
|
||||
export const MINOR_VERSION: number = 2;
|
||||
}
|
||||
107
src/sql/workbench/parts/notebook/notebookEditor.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/services/bootstrap/bootstrapService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
|
||||
import { NotebookModule } from 'sql/workbench/parts/notebook/notebook.module';
|
||||
import { NOTEBOOK_SELECTOR } from 'sql/workbench/parts/notebook/notebook.component';
|
||||
import { INotebookParams } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { $ } from 'sql/base/browser/builder';
|
||||
|
||||
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);
|
||||
|
||||
$(parentElement).clearChildren();
|
||||
|
||||
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(),
|
||||
isTrusted: input.isTrusted,
|
||||
profile: input.connectionProfile
|
||||
};
|
||||
bootstrapAngular(this.instantiationService,
|
||||
NotebookModule,
|
||||
this._notebookContainer,
|
||||
NOTEBOOK_SELECTOR,
|
||||
params,
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
366
src/sql/workbench/parts/notebook/notebookInput.ts
Normal file
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
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/parts/notebook/notebookUtils';
|
||||
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { INotebookModel, IContentManager } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { LocalContentManager } from 'sql/workbench/services/notebook/node/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';
|
||||
|
||||
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
|
||||
|
||||
export class NotebookEditorModel extends EditorModel {
|
||||
private dirty: boolean;
|
||||
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
|
||||
constructor(public readonly notebookUri: URI,
|
||||
private textEditorModel: TextFileEditorModel | UntitledEditorModel,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
@ITextFileService private textFileService: ITextFileService
|
||||
) {
|
||||
super();
|
||||
this._register(this.notebookService.onNotebookEditorAdd(notebook => {
|
||||
if (notebook.id === this.notebookUri.toString()) {
|
||||
// Hook to content change events
|
||||
notebook.modelReady.then(() => {
|
||||
this._register(notebook.model.kernelChanged(e => this.updateModel()));
|
||||
this._register(notebook.model.contentChanged(e => this.updateModel()));
|
||||
}, err => undefined);
|
||||
}
|
||||
}));
|
||||
|
||||
if (this.textEditorModel instanceof UntitledEditorModel) {
|
||||
this._register(this.textEditorModel.onDidChangeDirty(e => this.setDirty(this.textEditorModel.isDirty())));
|
||||
} else {
|
||||
this._register(this.textEditorModel.onDidStateChange(e => this.setDirty(this.textEditorModel.isDirty())));
|
||||
}
|
||||
this.dirty = this.textEditorModel.isDirty();
|
||||
}
|
||||
|
||||
public get contentString(): string {
|
||||
let model = this.textEditorModel.textEditorModel;
|
||||
return model.getValue();
|
||||
}
|
||||
|
||||
get isDirty(): boolean {
|
||||
return 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(): void {
|
||||
let notebookModel = this.getNotebookModel();
|
||||
if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) {
|
||||
let content = JSON.stringify(notebookModel.toJSON(), undefined, ' ');
|
||||
let model = this.textEditorModel.textEditorModel;
|
||||
let endLine = model.getLineCount();
|
||||
let endCol = model.getLineMaxColumn(endLine);
|
||||
|
||||
this.textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(1, 1, endLine, endCol),
|
||||
text: content
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
isModelCreated(): boolean {
|
||||
return this.getNotebookModel() !== undefined;
|
||||
}
|
||||
|
||||
private getNotebookModel(): INotebookModel {
|
||||
let editor = this.notebookService.listNotebookEditors().find(n => n.id === this.notebookUri.toString());
|
||||
if (editor) {
|
||||
return editor.model;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get onDidChangeDirty(): Event<void> {
|
||||
return this._onDidChangeDirty.event;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private _isTrusted: boolean = false;
|
||||
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>;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 = new 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 get isTrusted(): boolean {
|
||||
return this._isTrusted;
|
||||
}
|
||||
|
||||
public set isTrusted(value: boolean) {
|
||||
this._isTrusted = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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._model && this._model.isModelCreated()) {
|
||||
return Promise.resolve(this._model);
|
||||
} else {
|
||||
let textOrUntitledEditorModel: UntitledEditorModel | IEditorModel;
|
||||
if (this.resource.scheme === Schemas.untitled) {
|
||||
textOrUntitledEditorModel = this._untitledEditorModel ? this._untitledEditorModel : await this._textInput.resolve();
|
||||
}
|
||||
else {
|
||||
const textEditorModelReference = await this.textModelService.createModelReference(this.resource);
|
||||
textOrUntitledEditorModel = await textEditorModelReference.object.load();
|
||||
}
|
||||
this._model = this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel);
|
||||
this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire());
|
||||
return this._model;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
}
|
||||
|
||||
async loadContent(): Promise<azdata.nb.INotebookContents> {
|
||||
let notebookEditorModel = await this.notebookInput.resolve();
|
||||
let contentManager = new LocalContentManager();
|
||||
let contents = await contentManager.loadFromContentString(notebookEditorModel.contentString);
|
||||
return contents;
|
||||
}
|
||||
|
||||
}
|
||||
189
src/sql/workbench/parts/notebook/notebookStyles.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, editorBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IDisposable } from 'vscode-xterm';
|
||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
export function registerNotebookThemes(overrideEditorThemeSetting: boolean): 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;
|
||||
|
||||
// Active border
|
||||
const activeBorder = theme.getColor(buttonBackground);
|
||||
if (activeBorder) {
|
||||
collector.addRule(`
|
||||
.notebookEditor .notebook-cell.active {
|
||||
border-color: ${activeBorder};
|
||||
border-width: 1px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// 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.active:hover {
|
||||
border-color: ${activeBorder};
|
||||
}
|
||||
|
||||
.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
|
||||
{
|
||||
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;
|
||||
}
|
||||
`);
|
||||
|
||||
// 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;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Styling for all links in notebooks
|
||||
const linkForeground = theme.getColor(textLinkForeground);
|
||||
if (linkForeground) {
|
||||
collector.addRule(`
|
||||
.notebookEditor a:link {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: ${linkForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
125
src/sql/workbench/parts/notebook/notebookUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as path from 'path';
|
||||
import { nb } from 'azdata';
|
||||
import * as os from 'os';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { localize } from 'vs/nls';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IOutputChannel } from 'vs/workbench/contrib/output/common/output';
|
||||
|
||||
|
||||
/**
|
||||
* 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 getErrorMessage(error: Error | string): string {
|
||||
return (error instanceof Error) ? error.message : error;
|
||||
}
|
||||
|
||||
export function getUserHome(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE;
|
||||
}
|
||||
|
||||
export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Promise<void> {
|
||||
let exists = await pfs.dirExists(dirPath);
|
||||
if (!exists) {
|
||||
if (outputChannel) {
|
||||
outputChannel.append(localize('mkdirOutputMsg', '... Creating {0}', dirPath) + os.EOL);
|
||||
}
|
||||
await pfs.mkdirp(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
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 && fileExt.startsWith('.')) {
|
||||
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 => {
|
||||
Object.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;
|
||||
ipAddress: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
99
src/sql/workbench/parts/notebook/outputs/common/mimemodel.ts
Normal file
@@ -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 '../../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,111 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { JSONObject, isPrimitive } from '../../models/jsonext';
|
||||
import { MimeModel } from './mimemodel';
|
||||
import { nbformat } from '../../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,365 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { ReadonlyJSONObject } from '../../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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to initialize a document widget factory.
|
||||
*
|
||||
* This interface is intended to be used by mime renderer extensions
|
||||
* to define a document opener that uses its renderer factory.
|
||||
*/
|
||||
export interface IDocumentWidgetFactoryOptions {
|
||||
/**
|
||||
* The name of the widget to display in dialogs.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The name of the document model type.
|
||||
*/
|
||||
readonly modelName?: string;
|
||||
|
||||
/**
|
||||
* The primary file type of the widget.
|
||||
*/
|
||||
readonly primaryFileType: string;
|
||||
|
||||
/**
|
||||
* The file types the widget can view.
|
||||
*/
|
||||
readonly fileTypes: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The file types for which the factory should be the default.
|
||||
*/
|
||||
readonly defaultFor?: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The file types for which the factory should be the default for rendering,
|
||||
* if that is different than the default factory (which may be for editing)
|
||||
* If undefined, then it will fall back on the default file type.
|
||||
*/
|
||||
readonly defaultRendered?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file type to associate with the renderer.
|
||||
*/
|
||||
export interface IFileType {
|
||||
/**
|
||||
* The name of the file type.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The mime types associated the file type.
|
||||
*/
|
||||
readonly mimeTypes: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The extensions of the file type (e.g. `".txt"`). Can be a compound
|
||||
* extension (e.g. `".table.json`).
|
||||
*/
|
||||
readonly extensions: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* An optional display name for the file type.
|
||||
*/
|
||||
readonly displayName?: string;
|
||||
|
||||
/**
|
||||
* An optional pattern for a file name (e.g. `^Dockerfile$`).
|
||||
*/
|
||||
readonly pattern?: string;
|
||||
|
||||
/**
|
||||
* The icon class name for the file type.
|
||||
*/
|
||||
readonly iconClass?: string;
|
||||
|
||||
/**
|
||||
* The icon label for the file type.
|
||||
*/
|
||||
readonly iconLabel?: string;
|
||||
|
||||
/**
|
||||
* The file format for the file type ('text', 'base64', or 'json').
|
||||
*/
|
||||
readonly fileFormat?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for using a RenderMime.IRenderer for output and read-only documents.
|
||||
*/
|
||||
export interface IExtension {
|
||||
/**
|
||||
* The ID of the extension.
|
||||
*
|
||||
* #### Notes
|
||||
* The convention for extension IDs in JupyterLab is the full NPM package
|
||||
* name followed by a colon and a unique string token, e.g.
|
||||
* `'@jupyterlab/apputils-extension:settings'` or `'foo-extension:bar'`.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* A renderer factory to be registered to render the MIME type.
|
||||
*/
|
||||
readonly rendererFactory: IRendererFactory;
|
||||
|
||||
/**
|
||||
* The rank passed to `RenderMime.addFactory`. If not given,
|
||||
* defaults to the `defaultRank` of the factory.
|
||||
*/
|
||||
readonly rank?: number;
|
||||
|
||||
/**
|
||||
* The timeout after user activity to re-render the data.
|
||||
*/
|
||||
readonly renderTimeout?: number;
|
||||
|
||||
/**
|
||||
* Preferred data type from the model. Defaults to `string`.
|
||||
*/
|
||||
readonly dataType?: 'string' | 'json';
|
||||
|
||||
/**
|
||||
* The options used to open a document with the renderer factory.
|
||||
*/
|
||||
readonly documentWidgetFactoryOptions?:
|
||||
| IDocumentWidgetFactoryOptions
|
||||
| ReadonlyArray<IDocumentWidgetFactoryOptions>;
|
||||
|
||||
/**
|
||||
* The optional file type associated with the extension.
|
||||
*/
|
||||
readonly fileTypes?: ReadonlyArray<IFileType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a module that exports an extension or extensions as
|
||||
* the default value.
|
||||
*/
|
||||
export interface IExtensionModule {
|
||||
/**
|
||||
* The default export.
|
||||
*/
|
||||
readonly default: IExtension | ReadonlyArray<IExtension>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
181
src/sql/workbench/parts/notebook/outputs/common/url.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import { JSONObject } from '../../models/jsonext';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
/**
|
||||
* The namespace for URL-related functions.
|
||||
*/
|
||||
export namespace URLExt {
|
||||
/**
|
||||
* Normalize a url.
|
||||
*/
|
||||
export function normalize(url: string): string {
|
||||
return URI.parse(url).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a sequence of url components and normalizes as in node `path.join`.
|
||||
*
|
||||
* @param parts - The url components.
|
||||
*
|
||||
* @returns the joined url.
|
||||
*/
|
||||
export function join(...parts: string[]): string {
|
||||
parts = parts || [];
|
||||
|
||||
// Isolate the top element.
|
||||
const top = parts[0] || '';
|
||||
|
||||
// Check whether protocol shorthand is being used.
|
||||
const shorthand = top.indexOf('//') === 0;
|
||||
|
||||
// Parse the top element into a header collection.
|
||||
const header = top.match(/(\w+)(:)(\/\/)?/);
|
||||
const protocol = header && header[1];
|
||||
const colon = protocol && header[2];
|
||||
const slashes = colon && header[3];
|
||||
|
||||
// Construct the URL prefix.
|
||||
const prefix = shorthand
|
||||
? '//'
|
||||
: [protocol, colon, slashes].filter(str => str).join('');
|
||||
|
||||
// Construct the URL body omitting the prefix of the top value.
|
||||
const body = [top.indexOf(prefix) === 0 ? top.replace(prefix, '') : top]
|
||||
// Filter out top value if empty.
|
||||
.filter(str => str)
|
||||
// Remove leading slashes in all subsequent URL body elements.
|
||||
.concat(parts.slice(1).map(str => str.replace(/^\//, '')))
|
||||
.join('/')
|
||||
// Replace multiple slashes with one.
|
||||
.replace(/\/+/g, '/');
|
||||
|
||||
return prefix + body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the components of a multi-segment url.
|
||||
*
|
||||
* @param url - The url to encode.
|
||||
*
|
||||
* @returns the encoded url.
|
||||
*
|
||||
* #### Notes
|
||||
* Preserves the `'/'` separators.
|
||||
* Should not include the base url, since all parts are escaped.
|
||||
*/
|
||||
export function encodeParts(url: string): string {
|
||||
return join(...url.split('/').map(encodeURIComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a serialized object string suitable for a query.
|
||||
*
|
||||
* @param object - The source object.
|
||||
*
|
||||
* @returns an encoded url query.
|
||||
*
|
||||
* #### Notes
|
||||
* Modified version of [stackoverflow](http://stackoverflow.com/a/30707423).
|
||||
*/
|
||||
export function objectToQueryString(value: JSONObject): string {
|
||||
const keys = Object.keys(value);
|
||||
|
||||
if (!keys.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
'?' +
|
||||
keys
|
||||
.map(key => {
|
||||
const content = encodeURIComponent(String(value[key]));
|
||||
return key + (content ? '=' + content : '');
|
||||
})
|
||||
.join('&')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a parsed object that represents the values in a query string.
|
||||
*/
|
||||
export function queryStringToObject(
|
||||
value: string
|
||||
): { [key: string]: string } {
|
||||
return value
|
||||
.replace(/^\?/, '')
|
||||
.split('&')
|
||||
.reduce(
|
||||
(acc, val) => {
|
||||
const [key, value] = val.split('=');
|
||||
acc[key] = decodeURIComponent(value || '');
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the url is a local url.
|
||||
*
|
||||
* #### Notes
|
||||
* This function returns `false` for any fully qualified url, including
|
||||
* `data:`, `file:`, and `//` protocol URLs.
|
||||
*/
|
||||
export function isLocal(url: string): boolean {
|
||||
// If if doesn't have a scheme such as file: or http:// it's local
|
||||
return !!URI.parse(url).scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a URL object
|
||||
*/
|
||||
export interface IUrl {
|
||||
/**
|
||||
* The full URL string that was parsed with both the protocol and host
|
||||
* components converted to lower-case.
|
||||
*/
|
||||
href?: string;
|
||||
|
||||
/**
|
||||
* Identifies the URL's lower-cased protocol scheme.
|
||||
*/
|
||||
protocol?: string;
|
||||
|
||||
/**
|
||||
* The full lower-cased host portion of the URL, including the port if
|
||||
* specified.
|
||||
*/
|
||||
host?: string;
|
||||
|
||||
/**
|
||||
* The lower-cased host name portion of the host component without the
|
||||
* port included.
|
||||
*/
|
||||
hostname?: string;
|
||||
|
||||
/**
|
||||
* The numeric port portion of the host component.
|
||||
*/
|
||||
port?: string;
|
||||
|
||||
/**
|
||||
* The entire path section of the URL.
|
||||
*/
|
||||
pathname?: string;
|
||||
|
||||
/**
|
||||
* The "fragment" portion of the URL including the leading ASCII hash
|
||||
* `(#)` character
|
||||
*/
|
||||
hash?: string;
|
||||
|
||||
/**
|
||||
* The search element, including leading question mark (`'?'`), if any,
|
||||
* of the URL.
|
||||
*/
|
||||
search?: string;
|
||||
}
|
||||
}
|
||||
105
src/sql/workbench/parts/notebook/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 './common/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 Markdown.
|
||||
// */
|
||||
// export const markdownRendererFactory: IRenderMime.IRendererFactory = {
|
||||
// safe: true,
|
||||
// mimeTypes: ['text/markdown'],
|
||||
// defaultRank: 60,
|
||||
// createRenderer: options => new widgets.RenderedMarkdown(options)
|
||||
// };
|
||||
|
||||
/**
|
||||
* A mime renderer factory for svg.
|
||||
*/
|
||||
export const svgRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['image/svg+xml'],
|
||||
defaultRank: 80,
|
||||
createRenderer: options => new widgets.RenderedSVG(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A mime renderer factory for plain and jupyter console text data.
|
||||
*/
|
||||
export const textRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'text/plain',
|
||||
'application/vnd.jupyter.stdout',
|
||||
'application/vnd.jupyter.stderr'
|
||||
],
|
||||
defaultRank: 120,
|
||||
createRenderer: options => new widgets.RenderedText(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A placeholder factory for deprecated rendered JavaScript.
|
||||
*/
|
||||
export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['text/javascript', 'application/javascript'],
|
||||
defaultRank: 110,
|
||||
createRenderer: options => new widgets.RenderedJavaScript(options)
|
||||
};
|
||||
|
||||
export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
defaultRank: 40,
|
||||
createRenderer: options => new widgets.RenderedDataResource(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* The standard factories provided by the rendermime package.
|
||||
*/
|
||||
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
|
||||
htmlRendererFactory,
|
||||
// markdownRendererFactory,
|
||||
// latexRendererFactory,
|
||||
svgRendererFactory,
|
||||
imageRendererFactory,
|
||||
javaScriptRendererFactory,
|
||||
textRendererFactory,
|
||||
dataResourceRendererFactory
|
||||
];
|
||||
352
src/sql/workbench/parts/notebook/outputs/registry.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { IRenderMime } from './common/renderMimeInterfaces';
|
||||
import { MimeModel } from './common/mimemodel';
|
||||
import { ReadonlyJSONObject } from '../models/jsonext';
|
||||
import { defaultSanitizer } from './sanitizer';
|
||||
|
||||
/**
|
||||
* An object which manages mime renderer factories.
|
||||
*
|
||||
* This object is used to render mime models using registered mime
|
||||
* renderers, selecting the preferred mime renderer to render the
|
||||
* model into a widget.
|
||||
*
|
||||
* #### Notes
|
||||
* This class is not intended to be subclassed.
|
||||
*/
|
||||
export class RenderMimeRegistry {
|
||||
/**
|
||||
* Construct a new rendermime.
|
||||
*
|
||||
* @param options - The options for initializing the instance.
|
||||
*/
|
||||
constructor(options: RenderMimeRegistry.IOptions = {}) {
|
||||
// Parse the options.
|
||||
this.resolver = options.resolver || null;
|
||||
this.linkHandler = options.linkHandler || null;
|
||||
this.latexTypesetter = options.latexTypesetter || null;
|
||||
this.sanitizer = options.sanitizer || defaultSanitizer;
|
||||
|
||||
// Add the initial factories.
|
||||
if (options.initialFactories) {
|
||||
for (let factory of options.initialFactories) {
|
||||
this.addFactory(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizer used by the rendermime instance.
|
||||
*/
|
||||
readonly sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The object used to resolve relative urls for the rendermime instance.
|
||||
*/
|
||||
readonly resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* The object used to handle path opening links.
|
||||
*/
|
||||
readonly linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the rendermime.
|
||||
*/
|
||||
readonly latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
|
||||
/**
|
||||
* The ordered list of mimeTypes.
|
||||
*/
|
||||
get mimeTypes(): ReadonlyArray<string> {
|
||||
return this._types || (this._types = Private.sortedTypes(this._ranks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the preferred mime type for a mime bundle.
|
||||
*
|
||||
* @param bundle - The bundle of mime data.
|
||||
*
|
||||
* @param safe - How to consider safe/unsafe factories. If 'ensure',
|
||||
* it will only consider safe factories. If 'any', any factory will be
|
||||
* considered. If 'prefer', unsafe factories will be considered, but
|
||||
* only after the safe options have been exhausted.
|
||||
*
|
||||
* @returns The preferred mime type from the available factories,
|
||||
* or `undefined` if the mime type cannot be rendered.
|
||||
*/
|
||||
preferredMimeType(
|
||||
bundle: ReadonlyJSONObject,
|
||||
safe: 'ensure' | 'prefer' | 'any' = 'ensure'
|
||||
): string | undefined {
|
||||
// Try to find a safe factory first, if preferred.
|
||||
if (safe === 'ensure' || safe === 'prefer') {
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle && this._factories[mt].safe) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (safe !== 'ensure') {
|
||||
// Otherwise, search for the best factory among all factories.
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, no matching mime type exists.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a renderer for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns A new renderer for the given mime type.
|
||||
*
|
||||
* @throws An error if no factory exists for the mime type.
|
||||
*/
|
||||
createRenderer(mimeType: string): IRenderMime.IRenderer {
|
||||
// Throw an error if no factory exists for the mime type.
|
||||
if (!(mimeType in this._factories)) {
|
||||
throw new Error(`No factory for mime type: '${mimeType}'`);
|
||||
}
|
||||
|
||||
// Invoke the best factory for the given mime type.
|
||||
return this._factories[mimeType].createRenderer({
|
||||
mimeType,
|
||||
resolver: this.resolver,
|
||||
sanitizer: this.sanitizer,
|
||||
linkHandler: this.linkHandler,
|
||||
latexTypesetter: this.latexTypesetter
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mime model. This is a convenience method.
|
||||
*
|
||||
* @options - The options used to create the model.
|
||||
*
|
||||
* @returns A new mime model.
|
||||
*/
|
||||
createModel(options: MimeModel.IOptions = {}): MimeModel {
|
||||
return new MimeModel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of this rendermime instance.
|
||||
*
|
||||
* @param options - The options for configuring the clone.
|
||||
*
|
||||
* @returns A new independent clone of the rendermime.
|
||||
*/
|
||||
clone(options: RenderMimeRegistry.ICloneOptions = {}): RenderMimeRegistry {
|
||||
// Create the clone.
|
||||
let clone = new RenderMimeRegistry({
|
||||
resolver: options.resolver || this.resolver || undefined,
|
||||
sanitizer: options.sanitizer || this.sanitizer || undefined,
|
||||
linkHandler: options.linkHandler || this.linkHandler || undefined,
|
||||
latexTypesetter: options.latexTypesetter || this.latexTypesetter
|
||||
});
|
||||
|
||||
// Clone the internal state.
|
||||
clone._factories = { ...this._factories };
|
||||
clone._ranks = { ...this._ranks };
|
||||
clone._id = this._id;
|
||||
|
||||
// Return the cloned object.
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the renderer factory registered for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The factory for the mime type, or `undefined`.
|
||||
*/
|
||||
getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
|
||||
return this._factories[mimeType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a renderer factory to the rendermime.
|
||||
*
|
||||
* @param factory - The renderer factory of interest.
|
||||
*
|
||||
* @param rank - The rank of the renderer. A lower rank indicates
|
||||
* a higher priority for rendering. If not given, the rank will
|
||||
* defer to the `defaultRank` of the factory. If no `defaultRank`
|
||||
* is given, it will default to 100.
|
||||
*
|
||||
* #### Notes
|
||||
* The renderer will replace an existing renderer for the given
|
||||
* mimeType.
|
||||
*/
|
||||
addFactory(factory: IRenderMime.IRendererFactory, rank?: number): void {
|
||||
if (rank === undefined) {
|
||||
rank = factory.defaultRank;
|
||||
if (rank === undefined) {
|
||||
rank = 100;
|
||||
}
|
||||
}
|
||||
for (let mt of factory.mimeTypes) {
|
||||
this._factories[mt] = factory;
|
||||
this._ranks[mt] = { rank, id: this._id++ };
|
||||
}
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*/
|
||||
removeMimeType(mimeType: string): void {
|
||||
delete this._factories[mimeType];
|
||||
delete this._ranks[mimeType];
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rank for a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The rank of the mime type or undefined.
|
||||
*/
|
||||
getRank(mimeType: string): number | undefined {
|
||||
let rank = this._ranks[mimeType];
|
||||
return rank && rank.rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rank of a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @param rank - The new rank to assign.
|
||||
*
|
||||
* #### Notes
|
||||
* This is a no-op if the mime type is not registered.
|
||||
*/
|
||||
setRank(mimeType: string, rank: number): void {
|
||||
if (!this._ranks[mimeType]) {
|
||||
return;
|
||||
}
|
||||
let id = this._id++;
|
||||
this._ranks[mimeType] = { rank, id };
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
private _id = 0;
|
||||
private _ranks: Private.RankMap = {};
|
||||
private _types: string[] | null = null;
|
||||
private _factories: Private.FactoryMap = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for `RenderMimeRegistry` class statics.
|
||||
*/
|
||||
export namespace RenderMimeRegistry {
|
||||
/**
|
||||
* The options used to initialize a rendermime instance.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Initial factories to add to the rendermime instance.
|
||||
*/
|
||||
initialFactories?: ReadonlyArray<IRenderMime.IRendererFactory>;
|
||||
|
||||
/**
|
||||
* The sanitizer used to sanitize untrusted html inputs.
|
||||
*
|
||||
* If not given, a default sanitizer will be used.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The initial resolver object.
|
||||
*
|
||||
* The default is `null`.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* An optional path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* An optional LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to clone a rendermime instance.
|
||||
*/
|
||||
export interface ICloneOptions {
|
||||
/**
|
||||
* The new sanitizer used to sanitize untrusted html inputs.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The new resolver object.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* The new path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* The new LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the module implementation details.
|
||||
*/
|
||||
namespace Private {
|
||||
/**
|
||||
* A type alias for a mime rank and tie-breaking id.
|
||||
*/
|
||||
export type RankPair = { readonly id: number; readonly rank: number };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> rank pair.
|
||||
*/
|
||||
export type RankMap = { [key: string]: RankPair };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> ordered factories.
|
||||
*/
|
||||
export type FactoryMap = { [key: string]: IRenderMime.IRendererFactory };
|
||||
|
||||
/**
|
||||
* Get the mime types in the map, ordered by rank.
|
||||
*/
|
||||
export function sortedTypes(map: RankMap): string[] {
|
||||
return Object.keys(map).sort((a, b) => {
|
||||
let p1 = map[a];
|
||||
let p2 = map[b];
|
||||
if (p1.rank !== p2.rank) {
|
||||
return p1.rank - p2.rank;
|
||||
}
|
||||
return p1.id - p2.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
649
src/sql/workbench/parts/notebook/outputs/renderers.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { default as AnsiUp } from 'ansi_up';
|
||||
import { IRenderMime } from './common/renderMimeInterfaces';
|
||||
import { URLExt } from './common/url';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
|
||||
/**
|
||||
* Render HTML into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderHTML(options: renderHTML.IOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let {
|
||||
host,
|
||||
source,
|
||||
trusted,
|
||||
sanitizer,
|
||||
resolver,
|
||||
linkHandler,
|
||||
shouldTypeset,
|
||||
latexTypesetter
|
||||
} = options;
|
||||
|
||||
let originalSource = source;
|
||||
|
||||
// Bail early if the source is empty.
|
||||
if (!source) {
|
||||
host.textContent = '';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Sanitize the source if it is not trusted. This removes all
|
||||
// `<script>` tags as well as other potentially harmful HTML.
|
||||
if (!trusted) {
|
||||
originalSource = `${source}`;
|
||||
source = sanitizer.sanitize(source);
|
||||
}
|
||||
|
||||
// Set the inner HTML of the host.
|
||||
host.innerHTML = source;
|
||||
|
||||
if (host.getElementsByTagName('script').length > 0) {
|
||||
// If output it trusted, eval any script tags contained in the HTML.
|
||||
// This is not done automatically by the browser when script tags are
|
||||
// created by setting `innerHTML`.
|
||||
if (trusted) {
|
||||
Private.evalInnerHTMLScriptTags(host);
|
||||
} else {
|
||||
const container = document.createElement('div');
|
||||
const warning = document.createElement('pre');
|
||||
warning.textContent =
|
||||
'This HTML output contains inline scripts. Are you sure that you want to run arbitrary Javascript within your Notebook session?';
|
||||
const runButton = document.createElement('button');
|
||||
runButton.textContent = 'Run';
|
||||
runButton.onclick = event => {
|
||||
host.innerHTML = originalSource;
|
||||
Private.evalInnerHTMLScriptTags(host);
|
||||
host.removeChild(host.firstChild);
|
||||
};
|
||||
container.appendChild(warning);
|
||||
container.appendChild(runButton);
|
||||
host.insertBefore(container, host.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default behavior of nodes.
|
||||
Private.handleDefaults(host, resolver);
|
||||
|
||||
// Patch the urls if a resolver is available.
|
||||
let promise: Promise<void>;
|
||||
if (resolver) {
|
||||
promise = Private.handleUrls(host, resolver, linkHandler);
|
||||
} else {
|
||||
promise = Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Return the final rendered promise.
|
||||
return promise.then(() => {
|
||||
if (shouldTypeset && latexTypesetter) {
|
||||
latexTypesetter.typeset(host);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderHTML` function statics.
|
||||
*/
|
||||
export namespace renderHTML {
|
||||
/**
|
||||
* The options for the `renderHTML` function.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* The host node for the rendered HTML.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The HTML source to render.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the source is trusted.
|
||||
*/
|
||||
trusted: boolean;
|
||||
|
||||
/**
|
||||
* The html sanitizer for untrusted source.
|
||||
*/
|
||||
sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* An optional url resolver.
|
||||
*/
|
||||
resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* An optional link handler.
|
||||
*/
|
||||
linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* Whether the node should be typeset.
|
||||
*/
|
||||
shouldTypeset: boolean;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the application.
|
||||
*/
|
||||
latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an image into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderImage(
|
||||
options: renderImage.IRenderOptions
|
||||
): Promise<void> {
|
||||
// Unpack the options.
|
||||
let {
|
||||
host,
|
||||
mimeType,
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
needsBackground,
|
||||
unconfined
|
||||
} = options;
|
||||
|
||||
// Clear the content in the host.
|
||||
host.textContent = '';
|
||||
|
||||
// Create the image element.
|
||||
let img = document.createElement('img');
|
||||
|
||||
// Set the source of the image.
|
||||
img.src = `data:${mimeType};base64,${source}`;
|
||||
|
||||
// Set the size of the image if provided.
|
||||
if (typeof height === 'number') {
|
||||
img.height = height;
|
||||
}
|
||||
if (typeof width === 'number') {
|
||||
img.width = width;
|
||||
}
|
||||
|
||||
if (needsBackground === 'light') {
|
||||
img.classList.add('jp-needs-light-background');
|
||||
} else if (needsBackground === 'dark') {
|
||||
img.classList.add('jp-needs-dark-background');
|
||||
}
|
||||
|
||||
if (unconfined === true) {
|
||||
img.classList.add('jp-mod-unconfined');
|
||||
}
|
||||
|
||||
// Add the image to the host.
|
||||
host.appendChild(img);
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderImage` function statics.
|
||||
*/
|
||||
export namespace renderImage {
|
||||
/**
|
||||
* The options for the `renderImage` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The image node to update with the content.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The mime type for the image.
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* The base64 encoded source for the image.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* The optional width for the image.
|
||||
*/
|
||||
width?: number;
|
||||
|
||||
/**
|
||||
* The optional height for the image.
|
||||
*/
|
||||
height?: number;
|
||||
|
||||
/**
|
||||
* Whether an image requires a background for legibility.
|
||||
*/
|
||||
needsBackground?: string;
|
||||
|
||||
/**
|
||||
* Whether the image should be unconfined.
|
||||
*/
|
||||
unconfined?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render LaTeX into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderLatex(
|
||||
options: renderLatex.IRenderOptions
|
||||
): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source, shouldTypeset, latexTypesetter } = options;
|
||||
|
||||
// Set the source on the node.
|
||||
host.textContent = source;
|
||||
|
||||
// Typeset the node if needed.
|
||||
if (shouldTypeset && latexTypesetter) {
|
||||
latexTypesetter.typeset(host);
|
||||
}
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderLatex` function statics.
|
||||
*/
|
||||
export namespace renderLatex {
|
||||
/**
|
||||
* The options for the `renderLatex` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered LaTeX.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The LaTeX source to render.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the node should be typeset.
|
||||
*/
|
||||
shouldTypeset: boolean;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the application.
|
||||
*/
|
||||
latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderDataResource` function statics.
|
||||
*/
|
||||
export namespace renderDataResource {
|
||||
/**
|
||||
* The options for the `renderDataResource` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered LaTeX.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The DataResource source to render.
|
||||
*/
|
||||
source: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render SVG into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source, trusted, unconfined } = options;
|
||||
|
||||
// Clear the content if there is no source.
|
||||
if (!source) {
|
||||
host.textContent = '';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Display a message if the source is not trusted.
|
||||
if (!trusted) {
|
||||
host.textContent =
|
||||
'Cannot display an untrusted SVG. Maybe you need to run the cell?';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Render in img so that user can save it easily
|
||||
const img = new Image();
|
||||
img.src = `data:image/svg+xml,${encodeURIComponent(source)}`;
|
||||
host.appendChild(img);
|
||||
|
||||
if (unconfined === true) {
|
||||
host.classList.add('jp-mod-unconfined');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderSVG` function statics.
|
||||
*/
|
||||
export namespace renderSVG {
|
||||
/**
|
||||
* The options for the `renderSVG` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the rendered SVG.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The SVG source.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Whether the source is trusted.
|
||||
*/
|
||||
trusted: boolean;
|
||||
|
||||
/**
|
||||
* Whether the svg should be unconfined.
|
||||
*/
|
||||
unconfined?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderText(options: renderText.IRenderOptions): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source } = options;
|
||||
|
||||
const ansiUp = new AnsiUp();
|
||||
ansiUp.escape_for_html = true;
|
||||
ansiUp.use_classes = true;
|
||||
|
||||
// Create the HTML content.
|
||||
let content = ansiUp.ansi_to_html(source);
|
||||
|
||||
// Set the inner HTML for the host node.
|
||||
host.innerHTML = `<pre>${content}</pre>`;
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the `renderText` function statics.
|
||||
*/
|
||||
export namespace renderText {
|
||||
/**
|
||||
* The options for the `renderText` function.
|
||||
*/
|
||||
export interface IRenderOptions {
|
||||
/**
|
||||
* The host node for the text content.
|
||||
*/
|
||||
host: HTMLElement;
|
||||
|
||||
/**
|
||||
* The source text to render.
|
||||
*/
|
||||
source: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for module implementation details.
|
||||
*/
|
||||
namespace Private {
|
||||
/**
|
||||
* Eval the script tags contained in a host populated by `innerHTML`.
|
||||
*
|
||||
* When script tags are created via `innerHTML`, the browser does not
|
||||
* evaluate them when they are added to the page. This function works
|
||||
* around that by creating new equivalent script nodes manually, and
|
||||
* replacing the originals.
|
||||
*/
|
||||
export function evalInnerHTMLScriptTags(host: HTMLElement): void {
|
||||
// Create a snapshot of the current script nodes.
|
||||
let scripts = host.getElementsByTagName('script');
|
||||
|
||||
// Loop over each script node.
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
let script = scripts.item(i);
|
||||
// Skip any scripts which no longer have a parent.
|
||||
if (!script.parentNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new script node which will be clone.
|
||||
let clone = document.createElement('script');
|
||||
|
||||
// Copy the attributes into the clone.
|
||||
let attrs = script.attributes;
|
||||
for (let i = 0, n = attrs.length; i < n; ++i) {
|
||||
let { name, value } = attrs[i];
|
||||
clone.setAttribute(name, value);
|
||||
}
|
||||
|
||||
// Copy the text content into the clone.
|
||||
clone.textContent = script.textContent;
|
||||
|
||||
// Replace the old script in the parent.
|
||||
script.parentNode.replaceChild(clone, script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the default behavior of nodes.
|
||||
*/
|
||||
export function handleDefaults(
|
||||
node: HTMLElement,
|
||||
resolver?: IRenderMime.IResolver
|
||||
): void {
|
||||
// Handle anchor elements.
|
||||
let anchors = node.getElementsByTagName('a');
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
let path = anchors[i].href || '';
|
||||
const isLocal =
|
||||
resolver && resolver.isLocal
|
||||
? resolver.isLocal(path)
|
||||
: URLExt.isLocal(path);
|
||||
if (isLocal) {
|
||||
anchors[i].target = '_self';
|
||||
} else {
|
||||
anchors[i].target = '_blank';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle image elements.
|
||||
let imgs = node.getElementsByTagName('img');
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].alt) {
|
||||
imgs[i].alt = 'Image';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the relative urls in element `src` and `href` attributes.
|
||||
*
|
||||
* @param node - The head html element.
|
||||
*
|
||||
* @param resolver - A url resolver.
|
||||
*
|
||||
* @param linkHandler - An optional link handler for nodes.
|
||||
*
|
||||
* @returns a promise fulfilled when the relative urls have been resolved.
|
||||
*/
|
||||
export function handleUrls(
|
||||
node: HTMLElement,
|
||||
resolver: IRenderMime.IResolver,
|
||||
linkHandler: IRenderMime.ILinkHandler | null
|
||||
): Promise<void> {
|
||||
// Set up an array to collect promises.
|
||||
let promises: Promise<void>[] = [];
|
||||
|
||||
// Handle HTML Elements with src attributes.
|
||||
let nodes = node.querySelectorAll('*[src]');
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
|
||||
}
|
||||
|
||||
// Handle anchor elements.
|
||||
let anchors = node.getElementsByTagName('a');
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
promises.push(handleAnchor(anchors[i], resolver, linkHandler));
|
||||
}
|
||||
|
||||
// Handle link elements.
|
||||
let links = node.getElementsByTagName('link');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
promises.push(handleAttr(links[i], 'href', resolver));
|
||||
}
|
||||
|
||||
// Wait on all promises.
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply ids to headers.
|
||||
*/
|
||||
export function headerAnchors(node: HTMLElement): void {
|
||||
let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
for (let headerType of headerNames) {
|
||||
let headers = node.getElementsByTagName(headerType);
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
let header = headers[i];
|
||||
header.id = encodeURIComponent(header.innerHTML.replace(/ /g, '-'));
|
||||
let anchor = document.createElement('a');
|
||||
anchor.target = '_self';
|
||||
anchor.textContent = '¶';
|
||||
anchor.href = '#' + header.id;
|
||||
anchor.classList.add('jp-InternalAnchorLink');
|
||||
header.appendChild(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a node with a `src` or `href` attribute.
|
||||
*/
|
||||
function handleAttr(
|
||||
node: HTMLElement,
|
||||
name: 'src' | 'href',
|
||||
resolver: IRenderMime.IResolver
|
||||
): Promise<void> {
|
||||
let source = node.getAttribute(name) || '';
|
||||
const isLocal = resolver.isLocal
|
||||
? resolver.isLocal(source)
|
||||
: URLExt.isLocal(source);
|
||||
if (!source || !isLocal) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
node.setAttribute(name, '');
|
||||
return resolver
|
||||
.resolveUrl(source)
|
||||
.then(path => {
|
||||
return resolver.getDownloadUrl(path);
|
||||
})
|
||||
.then(url => {
|
||||
// Check protocol again in case it changed:
|
||||
if (URI.parse(url).scheme !== 'data:') {
|
||||
// Bust caching for local src attrs.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
||||
url += (/\?/.test(url) ? '&' : '?') + new Date().getTime();
|
||||
}
|
||||
node.setAttribute(name, url);
|
||||
})
|
||||
.catch(err => {
|
||||
// If there was an error getting the url,
|
||||
// just make it an empty link.
|
||||
node.setAttribute(name, '');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an anchor node.
|
||||
*/
|
||||
function handleAnchor(
|
||||
anchor: HTMLAnchorElement,
|
||||
resolver: IRenderMime.IResolver,
|
||||
linkHandler: IRenderMime.ILinkHandler | null
|
||||
): Promise<void> {
|
||||
// Get the link path without the location prepended.
|
||||
// (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
|
||||
let href = anchor.getAttribute('href') || '';
|
||||
const isLocal = resolver.isLocal
|
||||
? resolver.isLocal(href)
|
||||
: URLExt.isLocal(href);
|
||||
// Bail if it is not a file-like url.
|
||||
if (!href || !isLocal) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// Remove the hash until we can handle it.
|
||||
let hash = anchor.hash;
|
||||
if (hash) {
|
||||
// Handle internal link in the file.
|
||||
if (hash === href) {
|
||||
anchor.target = '_self';
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// For external links, remove the hash until we have hash handling.
|
||||
href = href.replace(hash, '');
|
||||
}
|
||||
// Get the appropriate file path.
|
||||
return resolver
|
||||
.resolveUrl(href)
|
||||
.then(path => {
|
||||
// Handle the click override.
|
||||
if (linkHandler) {
|
||||
linkHandler.handleLink(anchor, path, hash);
|
||||
}
|
||||
// Get the appropriate file download path.
|
||||
return resolver.getDownloadUrl(path);
|
||||
})
|
||||
.then(url => {
|
||||
// Set the visible anchor.
|
||||
anchor.href = url + hash;
|
||||
})
|
||||
.catch(err => {
|
||||
// If there was an error getting the url,
|
||||
// just make it an empty link.
|
||||
anchor.href = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
1053
src/sql/workbench/parts/notebook/outputs/sanitizer.ts
Normal file
138
src/sql/workbench/parts/notebook/outputs/tableRenderers.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { textFormatter } from 'sql/parts/grid/services/sharedServices';
|
||||
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||
import { escape } from 'sql/base/common/strings';
|
||||
import { IDataResource } from 'sql/workbench/services/notebook/sql/sqlSessionManager';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
|
||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
|
||||
|
||||
/**
|
||||
* Render DataResource as a grid into a host node.
|
||||
*
|
||||
* @params options - The options for rendering.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
export function renderDataResource(
|
||||
options: renderDataResource.IRenderOptions
|
||||
): Promise<void> {
|
||||
// Unpack the options.
|
||||
let { host, source } = options;
|
||||
let sourceObject: IDataResource = JSON.parse(source);
|
||||
|
||||
// Before doing anything, avoid re-rendering the table multiple
|
||||
// times (as can be the case when going untrusted -> trusted)
|
||||
while (host.firstChild) {
|
||||
host.removeChild(host.firstChild);
|
||||
}
|
||||
|
||||
// Now create the table container
|
||||
let tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'notebook-cellTable';
|
||||
|
||||
const ROW_HEIGHT = 29;
|
||||
const BOTTOM_PADDING_AND_SCROLLBAR = 14;
|
||||
let tableResultsData = new TableDataView();
|
||||
let columns: string[] = sourceObject.schema.fields.map(val => val.name);
|
||||
// Table object requires passed in columns to be of datatype Slick.Column
|
||||
let columnsTransformed = transformColumns(columns);
|
||||
|
||||
// In order to show row numbers, we need to put the row number column
|
||||
// ahead of all of the other columns, and register the plugin below
|
||||
let rowNumberColumn = new RowNumberColumn({ numberOfRows: source.length });
|
||||
columnsTransformed.unshift(rowNumberColumn.getColumnDefinition());
|
||||
|
||||
let transformedData = transformData(sourceObject.data, columnsTransformed);
|
||||
tableResultsData.push(transformedData);
|
||||
|
||||
let detailTable = new Table(tableContainer, {
|
||||
dataProvider: tableResultsData, columns: columnsTransformed
|
||||
}, {
|
||||
rowHeight: ROW_HEIGHT,
|
||||
forceFitColumns: false,
|
||||
defaultColumnWidth: 120
|
||||
});
|
||||
detailTable.registerPlugin(rowNumberColumn);
|
||||
detailTable.registerPlugin(new MouseWheelSupport());
|
||||
detailTable.registerPlugin(new AutoColumnSize({ autoSizeOnRender: true }));
|
||||
detailTable.registerPlugin(new AdditionalKeyBindings());
|
||||
let numRows = detailTable.grid.getDataLength();
|
||||
// Need to include column headers and scrollbar, so that's why 1 needs to be added
|
||||
let rowsHeight = (numRows + 1) * ROW_HEIGHT + BOTTOM_PADDING_AND_SCROLLBAR;
|
||||
// if no rows are in the grid, set height to 100% of the container's height
|
||||
if (numRows === 0) {
|
||||
tableContainer.style.height = '100%';
|
||||
} else {
|
||||
// Set the height dynamically if the grid's height is < 500px high; otherwise, set height to 500px
|
||||
tableContainer.style.height = rowsHeight >= 500 ? '500px' : rowsHeight.toString() + 'px';
|
||||
}
|
||||
|
||||
attachTableStyler(detailTable, options.themeService);
|
||||
host.appendChild(tableContainer);
|
||||
detailTable.resizeCanvas();
|
||||
|
||||
// Return the rendered promise.
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// SlickGrid requires columns and data to be in a very specific format; this code was adapted from tableInsight.component.ts
|
||||
function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
let dataWithSchema = {};
|
||||
Object.keys(row).forEach((val, index) => {
|
||||
let displayValue = String(Object.values(row)[index]);
|
||||
// Since the columns[0] represents the row number, start at 1
|
||||
dataWithSchema[columns[index + 1].field] = {
|
||||
displayValue: displayValue,
|
||||
ariaLabel: escape(displayValue),
|
||||
isNull: false
|
||||
};
|
||||
});
|
||||
return dataWithSchema;
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map((col, index) => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
id: col,
|
||||
field: index.toString(),
|
||||
formatter: textFormatter
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Theme service used to react to theme change events
|
||||
*/
|
||||
themeService?: IThemeService;
|
||||
}
|
||||
}
|
||||
380
src/sql/workbench/parts/notebook/outputs/widgets.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as renderers from './renderers';
|
||||
import { IRenderMime } from './common/renderMimeInterfaces';
|
||||
import { ReadonlyJSONObject } from '../models/jsonext';
|
||||
import * as tableRenderers from 'sql/workbench/parts/notebook/outputs/tableRenderers';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
/**
|
||||
* A common base class for mime renderers.
|
||||
*/
|
||||
export abstract class RenderedCommon implements IRenderMime.IRenderer {
|
||||
private _node: HTMLElement;
|
||||
private cachedClasses: string[] = [];
|
||||
/**
|
||||
* Construct a new rendered common widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
this.mimeType = options.mimeType;
|
||||
this.sanitizer = options.sanitizer;
|
||||
this.resolver = options.resolver;
|
||||
this.linkHandler = options.linkHandler;
|
||||
this.latexTypesetter = options.latexTypesetter;
|
||||
}
|
||||
|
||||
public get node(): HTMLElement {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
public set node(value: HTMLElement) {
|
||||
this._node = value;
|
||||
value.dataset['mimeType'] = this.mimeType;
|
||||
this._node.classList.add(...this.cachedClasses);
|
||||
this.cachedClasses = [];
|
||||
}
|
||||
|
||||
toggleClass(className: string, enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.addClass(className);
|
||||
} else {
|
||||
this.removeClass(className);
|
||||
}
|
||||
}
|
||||
|
||||
addClass(className: string): void {
|
||||
if (!this._node) {
|
||||
this.cachedClasses.push(className);
|
||||
} else if (!this._node.classList.contains(className)) {
|
||||
this._node.classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
removeClass(className: string): void {
|
||||
if (!this._node) {
|
||||
this.cachedClasses = this.cachedClasses.filter(c => c !== className);
|
||||
} else if (this._node.classList.contains(className)) {
|
||||
this._node.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The mimetype being rendered.
|
||||
*/
|
||||
readonly mimeType: string;
|
||||
|
||||
/**
|
||||
* The sanitizer used to sanitize untrusted html inputs.
|
||||
*/
|
||||
readonly sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The resolver object.
|
||||
*/
|
||||
readonly resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* The link handler.
|
||||
*/
|
||||
readonly linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The latexTypesetter.
|
||||
*/
|
||||
readonly latexTypesetter: IRenderMime.ILatexTypesetter;
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
// TODO compare model against old model for early bail?
|
||||
|
||||
// Toggle the trusted class on the widget.
|
||||
this.toggleClass('jp-mod-trusted', model.trusted);
|
||||
|
||||
// Render the actual content.
|
||||
return this.render(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
abstract render(model: IRenderMime.IMimeModel): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A common base class for HTML mime renderers.
|
||||
*/
|
||||
export abstract class RenderedHTMLCommon extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered HTML common widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedHTMLCommon');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mime renderer for displaying HTML and math.
|
||||
*/
|
||||
export class RenderedHTML extends RenderedHTMLCommon {
|
||||
/**
|
||||
* Construct a new rendered HTML widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedHTML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderHTML({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType]),
|
||||
trusted: model.trusted,
|
||||
resolver: this.resolver,
|
||||
sanitizer: this.sanitizer,
|
||||
linkHandler: this.linkHandler,
|
||||
shouldTypeset: true, //this.isAttached,
|
||||
latexTypesetter: this.latexTypesetter
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A mime renderer for displaying LaTeX output.
|
||||
// */
|
||||
// export class RenderedLatex extends RenderedCommon {
|
||||
// /**
|
||||
// * Construct a new rendered LaTeX widget.
|
||||
// *
|
||||
// * @param options - The options for initializing the widget.
|
||||
// */
|
||||
// constructor(options: IRenderMime.IRendererOptions) {
|
||||
// super(options);
|
||||
// this.addClass('jp-RenderedLatex');
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Render a mime model.
|
||||
// *
|
||||
// * @param model - The mime model to render.
|
||||
// *
|
||||
// * @returns A promise which resolves when rendering is complete.
|
||||
// */
|
||||
// render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
// return renderers.renderLatex({
|
||||
// host: this.node,
|
||||
// source: String(model.data[this.mimeType]),
|
||||
// shouldTypeset: this.isAttached,
|
||||
// latexTypesetter: this.latexTypesetter
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* A mime renderer for displaying images.
|
||||
*/
|
||||
export class RenderedImage extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered image widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedImage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
|
||||
return renderers.renderImage({
|
||||
host: this.node,
|
||||
mimeType: this.mimeType,
|
||||
source: String(model.data[this.mimeType]),
|
||||
width: metadata && (metadata.width as number | undefined),
|
||||
height: metadata && (metadata.height as number | undefined),
|
||||
needsBackground: model.metadata['needs_background'] as string | undefined,
|
||||
unconfined: metadata && (metadata.unconfined as boolean | undefined)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying SVG content.
|
||||
*/
|
||||
export class RenderedSVG extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered SVG widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedSVG');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
|
||||
return renderers.renderSVG({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType]),
|
||||
trusted: model.trusted,
|
||||
unconfined: metadata && (metadata.unconfined as boolean | undefined)
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * A message handler invoked on an `'after-attach'` message.
|
||||
// */
|
||||
// onAfterAttach(msg: Message): void {
|
||||
// if (this.latexTypesetter) {
|
||||
// this.latexTypesetter.typeset(this.node);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying plain text and console text.
|
||||
*/
|
||||
export class RenderedText extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered text widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedText');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderText({
|
||||
host: this.node,
|
||||
source: String(model.data[this.mimeType])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying deprecated JavaScript output.
|
||||
*/
|
||||
export class RenderedJavaScript extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered text widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedJavaScript');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return renderers.renderText({
|
||||
host: this.node,
|
||||
source: 'JavaScript output is disabled in Notebooks'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget for displaying Data Resource schemas and data.
|
||||
*/
|
||||
export class RenderedDataResource extends RenderedCommon {
|
||||
/**
|
||||
* Construct a new rendered data resource widget.
|
||||
*
|
||||
* @param options - The options for initializing the widget.
|
||||
*/
|
||||
constructor(options: IRenderMime.IRendererOptions) {
|
||||
super(options);
|
||||
this.addClass('jp-RenderedDataResource');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*/
|
||||
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||
return tableRenderers.renderDataResource({
|
||||
host: this.node,
|
||||
source: JSON.stringify(model.data[this.mimeType]),
|
||||
themeService: model.themeService
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,12 @@ import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
|
||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
|
||||
import { ModelFactory } from 'sql/workbench/parts/notebook/models/modelFactory';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { NotebookInput } from 'sql/parts/notebook/notebookInput';
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { ICellModel, INotebookModel, ILanguageMagic } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { ICellModel, INotebookModel, ILanguageMagic } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
|
||||
export const SERVICE_ID = 'notebookService';
|
||||
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER,
|
||||
DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER, OVERRIDE_EDITOR_THEMING_SETTING
|
||||
} from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
|
||||
import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
|
||||
import { standardRendererFactories } from 'sql/workbench/parts/notebook/outputs/factories';
|
||||
import { Extensions, INotebookProviderRegistry, NotebookProviderRegistration } from 'sql/workbench/services/notebook/common/notebookRegistry';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Memento } from 'vs/workbench/common/memento';
|
||||
@@ -28,11 +28,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { NotebookEditorVisibleContext } from 'sql/workbench/services/notebook/common/notebookContext';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { NotebookEditor } from 'sql/parts/notebook/notebookEditor';
|
||||
import { NotebookEditor } from 'sql/workbench/parts/notebook/notebookEditor';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { registerNotebookThemes } from 'sql/parts/notebook/notebookStyles';
|
||||
import { registerNotebookThemes } from 'sql/workbench/parts/notebook/notebookStyles';
|
||||
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
|
||||
import { ILanguageMagic, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { ILanguageMagic, notebookConstants } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { SqlNotebookProvider } from 'sql/workbench/services/notebook/sql/sqlNotebookProvider';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { FutureInternal } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
|
||||
export const noKernel: string = localize('noKernel', 'No Kernel');
|
||||
|
||||
@@ -14,10 +14,10 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { JSONObject } from 'sql/parts/notebook/models/jsonext';
|
||||
import { OutputTypes } from 'sql/parts/notebook/models/contracts';
|
||||
import { nbversion } from 'sql/parts/notebook/notebookConstants';
|
||||
import { nbformat } from 'sql/parts/notebook/models/nbformat';
|
||||
import { JSONObject } from 'sql/workbench/parts/notebook/models/jsonext';
|
||||
import { OutputTypes } from 'sql/workbench/parts/notebook/models/contracts';
|
||||
import { nbversion } from 'sql/workbench/parts/notebook/notebookConstants';
|
||||
import { nbformat } from 'sql/workbench/parts/notebook/models/nbformat';
|
||||
|
||||
type MimeBundle = { [key: string]: string | string[] | undefined };
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as os from 'os';
|
||||
import { nb, QueryExecuteSubsetResult, IDbColumn, BatchSummary, IResultMessage, ResultSetSummary } from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { FutureInternal, ILanguageMagic, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { FutureInternal, ILanguageMagic, notebookConstants } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import QueryRunner, { EventType } from 'sql/platform/query/common/queryRunner';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -21,7 +21,7 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { escape } from 'sql/base/common/strings';
|
||||
import { elapsedTimeLabel } from 'sql/parts/query/common/localizedConstants';
|
||||
import * as notebookUtils from 'sql/parts/notebook/notebookUtils';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
|
||||
|
||||