Merge changes from the Notebook feature branch.

These will be preserved as they have important history.
This commit is contained in:
Kevin Cunnane
2018-11-14 11:33:59 -08:00
95 changed files with 10178 additions and 28 deletions

View File

@@ -3,15 +3,22 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
import URI from 'vs/base/common/uri';
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import URI from 'vs/base/common/uri';
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
const fs = require('fs');
@@ -28,7 +35,7 @@ export const sqlModeId = 'sql';
* to that type.
* @param input The input to check for conversion
* @param options Editor options for controlling the conversion
* @param instantiationService The instatianation service to use to create the new input types
* @param instantiationService The instantiation service to use to create the new input types
*/
export function convertEditorInput(input: EditorInput, options: IQueryEditorOptions, instantiationService: IInstantiationService): EditorInput {
let denyQueryEditor = options && options.denyQueryEditor;
@@ -48,8 +55,25 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
let queryPlanInput: QueryPlanInput = instantiationService.createInstance(QueryPlanInput, queryPlanXml, 'aaa', undefined);
return queryPlanInput;
}
}
//Notebook
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
uri = getNotebookEditorUri(input);
if(uri && notebookValidator.isNotebookEnabled()){
//TODO: We need to pass in notebook data either through notebook input or notebook service
let fileName: string = 'untitled';
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
if (input) {
fileName = input.getName();
providerId = getProviderForFileName(fileName);
}
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
notebookInputModel.providerId = providerId;
//TO DO: Second parameter has to be the content.
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
return notebookInput;
}
}
return input;
}
@@ -129,6 +153,47 @@ function getQueryPlanEditorUri(input: EditorInput): URI {
return undefined;
}
/**
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
* @param input The EditorInput to get the URI of.
*/
function getNotebookEditorUri(input: EditorInput): URI {
if (!input || !input.getName()) {
return undefined;
}
// If this editor is not already of type notebook input
if (!(input instanceof NotebookInput)) {
let uri: URI = getSupportedInputResource(input);
if (uri) {
if (hasFileExtension(getNotebookFileExtensions(), input, false)) {
return uri;
}
}
}
return undefined;
}
function getNotebookFileExtensions() {
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
return notebookRegistry.getSupportedFileExtensions();
}
function getProviderForFileName(fileName: string) {
let fileExt = path.extname(fileName);
if (fileExt && fileExt.startsWith('.')) {
fileExt = fileExt.slice(1,fileExt.length);
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
return notebookRegistry.getProviderForFileType(fileExt);
}
return DEFAULT_NOTEBOOK_PROVIDER;
}
/**
* Checks whether the given EditorInput is set to either undefined or sql mode
* @param input The EditorInput to check the mode of

View File

@@ -5,7 +5,7 @@
import 'vs/css!./loadingComponent';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ViewChild, ElementRef
Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ElementRef
} from '@angular/core';
import * as sqlops from 'sqlops';
@@ -31,9 +31,6 @@ export default class LoadingComponent extends ComponentBase implements IComponen
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild('spinnerElement', { read: ElementRef }) private _spinnerElement: ElementRef;
@ViewChild('childElement', { read: ElementRef }) private _childElement: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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!./loadingComponent';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef
} from '@angular/core';
import * as nls from 'vs/nls';
@Component({
selector: 'loading-spinner',
template: `
<div class="modelview-loadingComponent-container" *ngIf="loading">
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
</div>
`
})
export default class LoadingSpinner {
private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading');
@Input() loading: boolean;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
}
}

View File

@@ -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: row">
<div #toolbar class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 40px; min-height: 40px; padding-top: 10px; orientation: portrait">
</div>
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
</div>
</div>

View 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.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle';
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
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 URI from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
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 { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { RunCellAction } from 'sql/parts/notebook/cellViews/codeActions';
export const CODE_SELECTOR: string = 'code-component';
@Component({
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./code.component.html'))
})
export class CodeComponent extends AngularDisposable implements OnInit {
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
@Input() cellModel: ICellModel;
@Output() public onContentChanged = new EventEmitter<void>();
protected _actionBar: Taskbar;
private readonly _minimumHeight = 30;
private _editor: QueryTextEditor;
private _editorInput: UntitledEditorInput;
private _editorModel: ITextModel;
private _uri: string;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IModelService) private _modelService: IModelService,
@Inject(IModeService) private _modeService: IModeService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IContextViewService) private contextViewService: IContextViewService
) {
super();
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.initActionBar();
}
ngOnChanges() {
this.updateLanguageMode();
this.updateModel();
}
ngAfterContentInit(): void {
this.createEditor();
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
this.layout();
}));
}
private createEditor(): 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);
let uri = this.createUri();
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, '', '');
this._editor.setInput(this._editorInput, undefined);
this._editorInput.resolve().then(model => {
this._editorModel = model.textEditorModel;
this._modelService.updateModel(this._editorModel, this.cellModel.source);
});
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.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 runCellAction = this._instantiationService.createInstance(RunCellAction);
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
this._actionBar.context = this;
this._actionBar.setContent([
{ action: runCellAction }
]);
}
private createUri(): URI {
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
// Use this to set the internal (immutable) and public (shared with extension) uri properties
this.cellModel.cellUri = uri;
return uri;
}
/// Editor Functions
private updateModel() {
if (this._editorModel) {
this._modelService.updateModel(this._editorModel, this.cellModel.source);
}
}
private updateLanguageMode() {
if (this._editorModel && this._editor) {
this._modeService.getOrCreateMode(this.cellModel.language).then((modeValue) => {
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();
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* 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;
border-right-style: solid;
}
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 .carbon-taskbar .icon {
background-size: 20px;
width: 40px;
}
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
{
padding-left: 10px
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { localize } from 'vs/nls';
export class RunCellAction extends Action {
public static ID = 'jobaction.notebookRunCell';
public static LABEL = 'Run cell';
constructor(
) {
super(RunCellAction.ID, '', 'toolbarIconRun');
this.tooltip = localize('runCell','Run cell');
}
public run(context: any): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
resolve(true);
} catch (e) {
reject(e);
}
});
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-code" style="flex: 0 0 auto;">
<code-component [cellModel]="cellModel"></code-component>
</div>
<div class="notebook-output" style="flex: 0 0 auto;">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel">
</output-area-component>
</div>
</div>

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* 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!./codeCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
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 {
@Input() cellModel: ICellModel;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
) {
super();
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
}
// Todo: implement layout
public layout() {
}
private updateTheme(theme: IColorTheme): void {
}
}

View 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.
*--------------------------------------------------------------------------------------------*/
code-cell-component {
display: block;
}
code-cell-component .notebook-output {
border-top-width: 1px;
border-top-style: solid;
}

View 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/common/lifecycle';
export abstract class CellView extends AngularDisposable implements OnDestroy {
constructor() {
super();
}
public abstract layout(): void;
}

View File

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

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { AngularDisposable } from 'sql/base/common/lifecycle';
import { nb } from 'sqlops';
import { INotebookService } from 'sql/services/notebook/notebookService';
import { MimeModel } from 'sql/parts/notebook/outputs/common/mimemodel';
import * as outputProcessor from '../outputs/common/outputProcessor';
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
import 'vs/css!sql/parts/notebook/outputs/style/index';
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() trustedMode: boolean;
private readonly _minimumHeight = 30;
registry: RenderMimeRegistry;
constructor(
@Inject(INotebookService) private _notebookService: INotebookService
) {
super();
this.registry = _notebookService.getMimeRegistry();
}
ngOnInit() {
let node = this.outputElement.nativeElement;
let output = this.cellOutput;
let options = outputProcessor.getBundleOptions({ value: output, trusted: this.trustedMode });
// TODO handle safe/unsafe mapping
this.createRenderedMimetype(options, node);
}
public layout(): void {
}
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 });
}
}
}

View File

@@ -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 class="notebook-output" style="flex: 0 0 auto;">
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" >
</output-component>
</div>
</div>

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { AngularDisposable } from 'sql/base/common/lifecycle';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
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 {
@Input() cellModel: ICellModel;
private readonly _minimumHeight = 30;
constructor(
@Inject(IModeService) private _modeService: IModeService
) {
super();
}
ngOnInit(): void {
}
}

View 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.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-text" style="flex: 0 0 auto;">
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()"></code-component>
</div>
<div #preview class="notebook-preview" style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
</div>
</div>

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* 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 } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
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 { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
import { localize } from 'vs/nls';
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 {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@Input() cellModel: ICellModel;
private _content: string;
private isEditMode: boolean;
private _sanitizer: ISanitizer;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(ICommandService) private _commandService: ICommandService
) {
super();
this.isEditMode = false;
}
ngOnChanges() {
this.updatePreview();
}
//Gets sanitizer from ISanitizer interface
private get sanitizer(): ISanitizer {
if (this._sanitizer) {
return this._sanitizer;
}
return this._sanitizer = defaultSanitizer;
}
/**
* 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) {
if (!this.cellModel.source && !this.isEditMode) {
(<HTMLElement>this.output.nativeElement).innerHTML = 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;
}
ngOnInit() {
this.updatePreview();
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
}
// 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();
}
public handleContentChanged(): void {
this.updatePreview();
}
public toggleEditMode(): void {
this.isEditMode = !this.isEditMode;
this.updatePreview();
this._changeRef.detectChanges();
}
}

View 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 {
border-top-width: 1px;
border-top-style: solid;
user-select: initial;
}

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

View 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

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title> code_cell</title><path d="M3.65,4.65l.7.7L1.71,8l2.64,2.65-.7.7L.29,8ZM9.48,2h1l-4,12h-1Zm2.87,9.35-.7-.7L14.29,8,11.65,5.35l.7-.7L15.71,8Z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>text_cell</title><path d="M5.53,8.62H2L1.25,11H0L3.16,1.52H4.4L6.88,8.93l-.75,1.5Zm-.4-1.18-1.35-4-1.35,4Zm9.08,6.28.29.67a2,2,0,0,0,.32.5,1.16,1.16,0,0,0,.46.31,2.06,2.06,0,0,0,.72.11v.41H11.85v-.41h.35a1.31,1.31,0,0,0,.36,0,.58.58,0,0,0,.28-.15.47.47,0,0,0,.11-.34,1.51,1.51,0,0,0-.06-.36c0-.15-.09-.32-.15-.5s-.12-.36-.19-.55l-.2-.53-.17-.43-.1-.24H7.92l-.11.25-.19.43-.22.52q-.12.28-.21.54c-.06.18-.12.34-.16.49a1.53,1.53,0,0,0-.06.37.42.42,0,0,0,.12.33.64.64,0,0,0,.28.15,1.66,1.66,0,0,0,.36,0h.34v.41H4.58v-.41a3.25,3.25,0,0,0,.7-.19,1.1,1.1,0,0,0,.41-.3A1.86,1.86,0,0,0,6,14.34l.31-.73L9.89,5.07h.76ZM11.52,11,9.88,7.46,8.32,11Z"/></svg>

After

Width:  |  Height:  |  Size: 744 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>cell_output</title><path d="M10.91,9.46v4H0V2.52H10.91v4h-1v-3H1v8.93H9.92v-3Zm3-2L11.8,5.35l.7-.7L15.83,8,12.5,11.3l-.7-.7,2.13-2.13H5v-1Z"/></svg>

After

Width:  |  Height:  |  Size: 248 B

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

View File

@@ -0,0 +1,321 @@
/*---------------------------------------------------------------------------------------------
* 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 { Event, Emitter } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { nb } from 'sqlops';
import { ICellModelOptions, IModelFactory } from './modelInterfaces';
import * as notebookUtils from '../notebookUtils';
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
let modelId = 0;
export class CellModel implements ICellModel {
private static LanguageMapping: Map<string, string>;
private _cellType: nb.CellType;
private _source: string;
private _language: string;
private _future: nb.IFuture;
private _outputs: nb.ICellOutput[] = [];
private _isEditMode: boolean;
private _onOutputsChanged = new Emitter<ReadonlyArray<nb.ICellOutput>>();
private _onCellModeChanged = new Emitter<boolean>();
public id: string;
private _isTrusted: boolean;
private _active: boolean;
private _cellUri: URI;
constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) {
this.id = `${modelId++}`;
CellModel.CreateLanguageMappings();
// Do nothing for now
if (cellData) {
this.fromJSON(cellData);
} else {
this._cellType = CellTypes.Code;
this._source = '';
}
this._isEditMode = this._cellType !== CellTypes.Markdown;
this.setDefaultLanguage();
if (_options && _options.isTrusted) {
this._isTrusted = true;
} else {
this._isTrusted = false;
}
}
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(): nb.IFuture {
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;
}
public get cellUri(): URI {
return this._cellUri;
}
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 {
return this._language;
}
public set language(newLanguage: string) {
this._language = newLanguage;
}
/**
* Sets the future which will be used to update the output
* area for this cell
*/
setFuture(future: nb.IFuture): 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) });
}
private 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(): ReadonlyArray<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;
}
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) {
this._outputs.push(output);
this.fireOutputsChanged();
}
}
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
let transient = (msg.content.transient || {});
return transient['display_id'] as string;
}
public toJSON(): nb.ICell {
let cellJson: Partial<nb.ICell> = {
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 = 1; // TODO: keep track of actual execution count
}
return cellJson as nb.ICell;
}
public fromJSON(cell: nb.ICell): void {
if (!cell) {
return;
}
this._cellType = cell.cell_type;
this._source = Array.isArray(cell.source) ? cell.source.join('') : cell.source;
this._language = (cell.metadata && cell.metadata.language) ? cell.metadata.language : 'python';
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 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 static CreateLanguageMappings(): void {
if (CellModel.LanguageMapping) {
return;
}
CellModel.LanguageMapping = new Map<string, string>();
CellModel.LanguageMapping['pyspark'] = 'python';
CellModel.LanguageMapping['pyspark3'] = 'python';
CellModel.LanguageMapping['python'] = 'python';
CellModel.LanguageMapping['scala'] = 'scala';
}
private get languageInfo(): nb.ILanguageInfo {
if (this._options && this._options.notebook && this._options.notebook.languageInfo) {
return this._options.notebook.languageInfo;
}
return undefined;
}
private setDefaultLanguage(): void {
this._language = 'python';
// 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;
if (languageInfo) {
if (languageInfo.name) {
// check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python')
if (CellModel.LanguageMapping[languageInfo.name]) {
this._language = CellModel.LanguageMapping[languageInfo.name];
} else {
this._language = languageInfo.name;
}
} else if (languageInfo.mimetype) {
this._language = languageInfo.mimetype;
}
}
let mimeTypePrefix = 'x-';
if (this._language.includes(mimeTypePrefix)) {
this._language = this._language.replace(mimeTypePrefix, '');
}
}
}

View File

@@ -0,0 +1,361 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { IClientSession, IKernelPreference, IClientSessionOptions } from './modelInterfaces';
import { Deferred } from 'sql/base/common/promise';
import * as notebookUtils from '../notebookUtils';
import * as sparkUtils from '../spark/sparkUtils';
import { INotebookManager } from 'sql/services/notebook/notebookService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
/**
* 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;
//#endregion
private _serverLoadFinished: Promise<void>;
private _session: nb.ISession;
private isServerStarted: boolean;
private notebookManager: INotebookManager;
private _connection: NotebookConnection;
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>();
}
public async initialize(connection?: NotebookConnection): Promise<void> {
try {
this._kernelConfigActions.push((kernelName: string) => { return this.runTasksBeforeSessionStart(kernelName); });
this._connection = connection;
this._serverLoadFinished = this.startServer();
await this._serverLoadFinished;
await this.initializeSession();
} catch (err) {
this._errorMessage = notebookUtils.getErrorMessage(err);
}
// Always resolving for now. It's up to callers to check for error case
this._isReady = true;
this._ready.resolve();
this._kernelChangeCompleted.resolve();
}
private async startServer(): Promise<void> {
let serverManager = this.notebookManager.serverManager;
if (serverManager && !serverManager.isStarted) {
await serverManager.startServer();
if (!serverManager.isStarted) {
throw new Error(nls.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._kernelPreference && this._kernelPreference.shouldStart) {
await this.startSessionInstance(this._kernelPreference.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(nls.localize('sparkKernelRequiresConnection', '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 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;
}
//#endregion
//#region Not Yet Implemented
/**
* Change the current kernel associated with the document.
*/
async changeKernel(options: nb.IKernelSpec): Promise<nb.IKernel> {
this._kernelChangeCompleted = new Deferred<void>();
this._isReady = false;
let oldKernel = 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;
// Send resolution events to listeners
this._kernelChangeCompleted.resolve();
this._kernelChangedEmitter.fire({
oldValue: oldKernel,
newValue: newKernel
});
return kernel;
}
/**
* 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 runTasksBeforeSessionStart(kernelName: string): Promise<void> {
// TODO we should move all Spark-related code to SparkMagicContext
if (this._session && this._connection && this.isSparkKernel(kernelName)) {
// TODO may need to reenable a way to get the credential
// await this._connection.getCredential();
// %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options,
// such as user/profile/host name/auth type
let server = URI.parse(sparkUtils.getLivyUrl(this._connection.host, this._connection.knoxport)).toString();
let doNotCallChangeEndpointParams =
`%_do_not_call_change_endpoint --username=${this._connection.user} --password=${this._connection.password} --server=${server} --auth=Basic_Access`;
let future = this._session.kernel.requestExecute({
code: doNotCallChangeEndpointParams
}, true);
await future.done;
}
}
public async updateConnection(connection: NotebookConnection): Promise<void> {
if (!this.kernel) {
// TODO is there any case where skipping causes errors? Do far it seems like it gets called twice
return;
}
this._connection = (connection.connectionProfile.id !== '-1') ? connection : this._connection;
// if kernel is not set, don't run kernel config actions
// this should only occur when a cell is cancelled, which interrupts the kernel
if (this.kernel && this.kernel.name) {
await this.runKernelConfigActions(this.kernel.name);
}
}
isSparkKernel(kernelName: string): any {
return kernelName && kernelName.toLowerCase().indexOf('spark') > -1;
}
/**
* Kill the kernel and shutdown the session.
*
* @returns A promise that resolves when the session is shut down.
*/
public async shutdown(): Promise<void> {
// Always try to shut down session
if (this._session && this._session.id) {
this.notebookManager.sessionManager.shutdown(this._session.id);
}
let serverManager = this.notebookManager.serverManager;
if (serverManager) {
await serverManager.stopServer();
}
}
/**
* 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
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* 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
}

View 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 'sqlops';
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.ICell, options: ICellModelOptions): ICellModel {
return new CellModel(this, cell, options);
}
public createClientSession(options: IClientSessionOptions): IClientSession {
return new ClientSession(options);
}
}

View File

@@ -0,0 +1,377 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
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/parts/notebook/models/contracts';
import { INotebookManager } from 'sql/services/notebook/notebookService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
export interface IClientSessionOptions {
notebookUri: URI;
notebookManager: INotebookManager;
notificationService: INotificationService;
}
/**
* 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;
/**
* 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(connection?: NotebookConnection): Promise<void>;
/**
* Change the current kernel associated with the document.
*/
changeKernel(
options: nb.IKernelSpec
): Promise<nb.IKernel>;
/**
* 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: NotebookConnection): void;
}
export interface IDefaultConnection {
defaultConnection: IConnectionProfile;
otherConnections: IConnectionProfile[];
}
/**
* 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>;
/**
* Client Session in the notebook, used for sending requests to the notebook service
*/
readonly clientSession: IClientSession;
/**
* LanguageInfo saved in the query book
*/
readonly languageInfo: nb.ILanguageInfo;
/**
* The notebook service used to call backend APIs
*/
readonly notebookManager: INotebookManager;
/**
* Event fired on first initialization of the kernel and
* on subsequent change events
*/
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
/**
* 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>;
/**
* 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;
/**
* The trusted mode of the NoteBook
*/
trustedMode: boolean;
/**
* 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): void;
/**
* Adds a cell to the end of the model
*/
addCell(cellType: CellType): void;
/**
* Deletes a cell
*/
deleteCell(cellModel: ICellModel): void;
/**
* Save the model to its backing content manager.
* Serializes the model and then calls through to save it
*/
saveModel(): Promise<boolean>;
/**
* Notifies the notebook of a change in the cell
*/
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
}
export interface ICellModelOptions {
notebook: INotebookModel;
isTrusted: boolean;
}
export interface ICellModel {
cellUri: URI;
id: string;
language: string;
source: string;
cellType: CellType;
trustedMode: boolean;
active: boolean;
readonly outputs: ReadonlyArray<nb.ICellOutput>;
equals(cellModel: ICellModel): boolean;
toJSON(): nb.ICell;
}
export interface IModelFactory {
createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel;
createClientSession(options: IClientSessionOptions): IClientSession;
}
export interface INotebookModelOptions {
/**
* Path to the local or remote notebook
*/
notebookUri: URI;
/**
* Factory for creating cells and client sessions
*/
factory: IModelFactory;
notebookManager: INotebookManager;
notificationService: INotificationService;
connectionService: IConnectionManagementService;
}
// TODO would like to move most of these constants to an extension
export namespace notebookConstants {
export const hadoopKnoxProviderName = 'HADOOP_KNOX';
export const python3 = 'python3';
export const python3DisplayName = 'Python 3';
export const defaultSparkKernel = 'pyspark3kernel';
}

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* 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/parts/connection/common/interfaces';
export namespace constants {
export const hostPropName = 'host';
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.options[constants.hostPropName];
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;
}
}

View File

@@ -0,0 +1,479 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { CellModel } from './cell';
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants } from './modelInterfaces';
import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { nbversion } from '../notebookConstants';
import * as notebookUtils from '../notebookUtils';
import { INotebookManager } from 'sql/services/notebook/notebookService';
import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
import { INotification, Severity } from 'vs/platform/notification/common/notification';
import { Schemas } from 'vs/base/common/network';
/*
* 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 interface NotebookContentChange {
/**
* What was the 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 class NotebookModel extends Disposable implements INotebookModel {
private _contextsChangedEmitter = new Emitter<void>();
private _contentChangedEmitter = new Emitter<NotebookContentChange>();
private _kernelsChangedEmitter = new Emitter<nb.IKernelSpec>();
private _inErrorState: boolean = false;
private _clientSession: IClientSession;
private _sessionLoadFinished: Promise<void>;
private _onClientSessionReady = new Emitter<IClientSession>();
private _activeContexts: IDefaultConnection;
private _trustedMode: boolean;
private _cells: ICellModel[];
private _defaultLanguageInfo: nb.ILanguageInfo;
private onErrorEmitter = new Emitter<INotification>();
private _savedKernelInfo: nb.IKernelInfo;
private readonly _nbformat: number = nbversion.MAJOR_VERSION;
private readonly _nbformatMinor: number = nbversion.MINOR_VERSION;
private _hadoopConnection: NotebookConnection;
private _defaultKernel: nb.IKernelSpec;
constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) {
super();
if (!notebookOptions || !notebookOptions.notebookUri || !notebookOptions.notebookManager) {
throw new Error('path or notebook service not defined');
}
if (startSessionImmediately) {
this.backgroundStartSession();
}
this._trustedMode = false;
}
public get notebookManager(): INotebookManager {
return this.notebookOptions.notebookManager;
}
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._clientSession;
}
/**
* 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._clientSession;
}
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
return this.clientSession.kernelChanged;
}
public get kernelsChanged(): Event<nb.IKernelSpec> {
return this._kernelsChangedEmitter.event;
}
public get defaultKernel(): nb.IKernelSpec {
return this._defaultKernel;
}
public get contextsChanged(): Event<void> {
return this._contextsChangedEmitter.event;
}
public get cells(): ICellModel[] {
return this._cells;
}
public get contexts(): IDefaultConnection {
return this._activeContexts;
}
public get specs(): nb.IAllKernels | undefined {
return this.notebookManager.sessionManager.specs;
}
public get inErrorState(): boolean {
return this._inErrorState;
}
public get onError(): Event<INotification> {
return this.onErrorEmitter.event;
}
public get trustedMode(): boolean {
return this._trustedMode;
}
public set trustedMode(isTrusted: boolean) {
this._trustedMode = isTrusted;
if (this._cells) {
this._cells.forEach(c => {
c.trustedMode = this._trustedMode;
});
}
}
/**
* 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 async requestModelLoad(isTrusted: boolean = false): Promise<void> {
try {
this._trustedMode = isTrusted;
let contents = null;
if(this.notebookOptions.notebookUri.scheme !== Schemas.untitled) {
contents = await this.notebookManager.contentManager.getNotebookContents(this.notebookOptions.notebookUri);
}
let factory = this.notebookOptions.factory;
// if cells already exist, create them with language info (if it is saved)
this._cells = undefined;
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 }));
}
}
if (!this._cells) {
this._cells = [this.createCell(CellTypes.Code)];
}
} catch (error) {
this._inErrorState = true;
throw error;
}
}
public addCell(cellType: CellType): void {
if (this.inErrorState || !this._cells) {
return;
}
let cell = this.createCell(cellType);
this._cells.push(cell);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsAdded,
cells: [cell]
});
}
private createCell(cellType: CellType): ICellModel {
let singleCell: nb.ICell = {
cell_type: cellType,
source: '',
metadata: {},
execution_count: 1
};
return this.notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true });
}
deleteCell(cellModel: CellModel): 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.'));
}
}
private notifyError(error: string): void {
this.onErrorEmitter.fire({ message: error, severity: Severity.Error });
}
public backgroundStartSession(): void {
this._clientSession = this.notebookOptions.factory.createClientSession({
notebookUri: this.notebookOptions.notebookUri,
notebookManager: this.notebookManager,
notificationService: this.notebookOptions.notificationService
});
let id: string = this.connectionProfile ? this.connectionProfile.id : undefined;
this._hadoopConnection = this.connectionProfile ? new NotebookConnection(this.connectionProfile) : undefined;
this._clientSession.initialize(this._hadoopConnection);
this._sessionLoadFinished = this._clientSession.ready.then(async () => {
if (this._clientSession.isInErrorState) {
this.setErrorState(this._clientSession.errorMessage);
} else {
this._onClientSessionReady.fire(this._clientSession);
// Once session is loaded, can use the session manager to retrieve useful info
this.loadKernelInfo();
await this.loadActiveContexts(undefined);
}
});
}
public get languageInfo(): nb.ILanguageInfo {
return this._defaultLanguageInfo;
}
private updateLanguageInfo(info: nb.ILanguageInfo) {
if (info) {
this._defaultLanguageInfo = info;
}
}
public changeKernel(displayName: string): void {
let spec = this.getSpecNameFromDisplayName(displayName);
this.doChangeKernel(spec);
}
private doChangeKernel(kernelSpec: nb.IKernelSpec): void {
this._clientSession.changeKernel(kernelSpec)
.then((kernel) => {
kernel.ready.then(() => {
if (kernel.info) {
this.updateLanguageInfo(kernel.info.language_info);
}
}, err => undefined);
return this.updateKernelInfo(kernel);
}).catch((err) => {
this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err)));
// TODO should revert kernels dropdown
});
}
public changeContext(host: string): void {
try {
let newConnection: IConnectionProfile = this._activeContexts.otherConnections.find((connection) => connection.options['host'] === host);
if (!newConnection && this._activeContexts.defaultConnection.options['host'] === host) {
newConnection = this._activeContexts.defaultConnection;
}
if (newConnection) {
SparkMagicContexts.configureContext(newConnection, this.notebookOptions);
this._hadoopConnection = new NotebookConnection(newConnection);
this._clientSession.updateConnection(this._hadoopConnection);
}
} catch (err) {
let msg = notebookUtils.getErrorMessage(err);
this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg));
}
}
private loadKernelInfo(): void {
this.clientSession.kernelChanged(async (e) => {
await this.loadActiveContexts(e);
});
try {
let sessionManager = this.notebookManager.sessionManager;
if (sessionManager) {
let defaultKernel = SparkMagicContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo, this.notebookOptions.notificationService);
this._defaultKernel = defaultKernel;
this._clientSession.statusChanged(async (session) => {
if (session && session.defaultKernelLoaded === true) {
this._kernelsChangedEmitter.fire(defaultKernel);
} else if (session && !session.defaultKernelLoaded) {
this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName });
}
});
this.doChangeKernel(defaultKernel);
}
} catch (err) {
let msg = notebookUtils.getErrorMessage(err);
this.notifyError(localize('loadKernelFailed', 'Loading kernel info failed: {0}', msg));
}
}
// Get default language if saved in notebook file
// Otherwise, default to python
private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo {
return notebook!.metadata!.language_info || {
name: 'python',
version: '',
mimetype: 'x-python'
};
}
// Get default kernel info if saved in notebook file
private getSavedKernelInfo(notebook: nb.INotebook): nb.IKernelInfo {
return notebook!.metadata!.kernelspec;
}
private getSpecNameFromDisplayName(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 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.handleClosed();
}
public async handleClosed(): Promise<void> {
try {
if (this._clientSession) {
await this._clientSession.shutdown();
this._clientSession = undefined;
}
} catch (err) {
this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err));
}
}
private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise<void> {
this._activeContexts = await SparkMagicContexts.getContextsForKernel(this.notebookOptions.connectionService, kernelChangedArgs, this.connectionProfile);
this._contextsChangedEmitter.fire();
let defaultHadoopConnection = new NotebookConnection(this.contexts.defaultConnection);
this.changeContext(defaultHadoopConnection.host);
}
/**
* 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;
}
public async saveModel(): Promise<boolean> {
let notebook = this.toJSON();
if (!notebook) {
return false;
}
await this.notebookManager.contentManager.save(this.notebookOptions.notebookUri, notebook);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.DirtyStateChanged,
isDirty: false
});
return true;
}
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
};
} catch (err) {
// Don't worry about this for now. Just use saved values
}
}
}
/**
* Serialize the model to JSON.
*/
toJSON(): nb.INotebook {
let cells: nb.ICell[] = 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: CellModel, 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);
}
}

View File

@@ -0,0 +1,194 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import { nb } from 'sqlops';
import * as json from 'vs/base/common/json';
import * as pfs from 'vs/base/node/pfs';
import { localize } from 'vs/nls';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IDefaultConnection, notebookConstants, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces';
import * as notebookUtils from '../notebookUtils';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
export class SparkMagicContexts {
public static get DefaultContext(): IDefaultConnection {
// TODO NOTEBOOK REFACTOR fix default connection handling
let defaultConnection: IConnectionProfile = <any> {
providerName: notebookConstants.hadoopKnoxProviderName,
id: '-1',
options:
{
host: localize('selectConnection', 'Select Connection')
}
};
return {
// default context if no other contexts are applicable
defaultConnection: defaultConnection,
otherConnections: [defaultConnection]
};
}
/**
* Get all of the applicable contexts for a given kernel
* @param apiWrapper ApiWrapper
* @param kernelChangedArgs kernel changed args (both old and new kernel info)
* @param profile current connection profile
*/
public static async getContextsForKernel(connectionService: IConnectionManagementService, 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) {
switch (kernelChangedArgs.newValue.name) {
case (notebookConstants.python3):
// python3 case, use this.DefaultContext for the only connection
break;
//TO DO: Handle server connections based on kernel type. Right now, we call the same method for all kernel types.
default:
connections = await this.getActiveContexts(connectionService, profile);
}
} else {
connections = await this.getActiveContexts(connectionService, profile);
}
return connections;
}
/**
* Get all active contexts and sort them
* @param apiWrapper ApiWrapper
* @param profile current connection profile
*/
public static async getActiveContexts(connectionService: IConnectionManagementService, profile: IConnectionProfile): Promise<IDefaultConnection> {
let defaultConnection: IConnectionProfile = SparkMagicContexts.DefaultContext.defaultConnection;
let activeConnections: IConnectionProfile[] = await connectionService.getActiveConnections();
// If no connections exist, only show 'n/a'
if (activeConnections.length === 0) {
return SparkMagicContexts.DefaultContext;
}
activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName);
// If launched from the right click or server dashboard, connection profile data exists, so use that as default
if (profile && profile.options) {
let profileConnection = activeConnections.filter(conn => conn.options['host'] === profile.options['host']);
if (profileConnection) {
defaultConnection = profileConnection[0];
}
} else {
if (activeConnections.length > 0) {
defaultConnection = activeConnections[0];
} else {
// TODO NOTEBOOK REFACTOR change this so it's no longer incompatible with IConnectionProfile
defaultConnection = <IConnectionProfile> <any>{
providerName: notebookConstants.hadoopKnoxProviderName,
id: '-1',
options:
{
host: localize('addConnection', 'Add new connection')
}
};
activeConnections.push(defaultConnection);
}
}
return {
otherConnections: activeConnections,
defaultConnection: defaultConnection
};
}
public static async configureContext(connection: IConnectionProfile, options: INotebookModelOptions): Promise<object> {
let sparkmagicConfDir = path.join(notebookUtils.getUserHome(), '.sparkmagic');
// TODO NOTEBOOK REFACTOR re-enable this or move to extension. Requires config files to be available in order to work
// await notebookUtils.mkDir(sparkmagicConfDir);
// let hadoopConnection = new Connection({ options: connection.options }, undefined, connection.connectionId);
// await hadoopConnection.getCredential();
// // Default to localhost in config file.
// let creds: ICredentials = {
// 'url': 'http://localhost:8088'
// };
// let configPath = notebookUtils.getTemplatePath(options.extensionContext.extensionPath, path.join('jupyter_config', 'sparkmagic_config.json'));
// let fileBuffer: Buffer = await pfs.readFile(configPath);
// let fileContents: string = fileBuffer.toString();
// let config: ISparkMagicConfig = json.parse(fileContents);
// SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir);
// let configFilePath = path.join(sparkmagicConfDir, 'config.json');
// await pfs.writeFile(configFilePath, JSON.stringify(config));
return {'SPARKMAGIC_CONF_DIR': sparkmagicConfDir};
}
/**
*
* @param specs kernel specs (comes from session manager)
* @param connectionInfo connection profile
* @param savedKernelInfo kernel info loaded from
*/
public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec {
let defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
let profile = connectionInfo as IConnectionProfile;
if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
// set default kernel to default spark kernel if profile exists
// otherwise, set default to kernel info loaded from existing file
defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo;
} else {
// Handle kernels
if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) {
notificationService.warn(localize('sparkKernelRequiresConnection', 'Cannot use kernel {0} as no connection is active. The default kernel of {1} will be used instead.', savedKernelInfo.display_name, defaultKernel.display_name));
}
}
// If no default kernel specified (should never happen), default to python3
if (!defaultKernel) {
defaultKernel = {
name: notebookConstants.python3,
display_name: notebookConstants.python3DisplayName
};
}
return defaultKernel;
}
private static updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void {
config.kernel_python_credentials = creds;
config.kernel_scala_credentials = creds;
config.kernel_r_credentials = creds;
config.logging_config.handlers.magicsHandler.home_path = homePath;
}
}
interface ICredentials {
'url': string;
}
interface ISparkMagicConfig {
kernel_python_credentials: ICredentials;
kernel_scala_credentials: ICredentials;
kernel_r_credentials: ICredentials;
logging_config: {
handlers: {
magicsHandler: {
home_path: string;
}
}
};
}
export interface IKernelJupyterID {
id: string;
jupyterId: string;
}

View 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.
*--------------------------------------------------------------------------------------------*/
-->
<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; height: 40px">
</div>
<div class="scrollable" style="flex: 1 1 auto; position: relative">
<loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell">
</code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell">
</text-cell-component>
</div>
</div>
</div>

View File

@@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './notebookStyles';
import { nb } from 'sqlops';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
import URI from 'vs/base/common/uri';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle';
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
import { ICellModel, INotebookModel, IModelFactory, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
import { NotebookModel, ErrorInfo, MessageLevel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
import * as notebookUtils from './notebookUtils';
import { Deferred } from 'sql/base/common/promise';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { KernelsDropdown, AttachToDropdown, AddCellAction } from 'sql/parts/notebook/notebookActions';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
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 {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
private _model: NotebookModel;
private _isInErrorState: boolean = false;
private _errorMessage: string;
protected _actionBar: Taskbar;
private _activeCell: ICellModel;
protected isLoading: boolean;
private notebookManager: INotebookManager;
private _modelReadyDeferred = new Deferred<NotebookModel>();
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
@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
) {
super();
this.profile = this.notebookParams!.profile;
this.isLoading = true;
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.initActionBar();
this.doLoad();
}
public get modelRegistered(): Promise<NotebookModel> {
return this._modelRegisteredDeferred.promise;
}
protected get cells(): ReadonlyArray<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) {
if (cell !== this._activeCell) {
if (this._activeCell) {
this._activeCell.active = false;
}
this._activeCell = cell;
this._activeCell.active = true;
this._changeRef.detectChanges();
}
}
//Add cell based on cell type
public addCell(cellType: CellType)
{
this._model.addCell(cellType);
}
public onKeyDown(event) {
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
let nextIndex = (this.findCellIndex(this._activeCell) + 1) % this.cells.length;
this.selectCell(this.cells[nextIndex]);
break;
case 'ArrowUp':
case 'ArrowLeft':
let index = this.findCellIndex(this._activeCell);
if (index === 0) {
index = this.cells.length;
}
this.selectCell(this.cells[--index]);
break;
default:
break;
}
}
private async doLoad(): Promise<void> {
try {
await this.loadModel();
this.setLoading(false);
this._modelReadyDeferred.resolve(this._model);
} catch (error) {
this.setViewInErrorState(localize('displayFailed', 'Could not display contents: {0}', error));
this.setLoading(false);
this._modelReadyDeferred.reject(error);
}
}
private setLoading(isLoading: boolean): void {
this.isLoading = isLoading;
this._changeRef.detectChanges();
}
private async loadModel(): Promise<void> {
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri);
let model = new NotebookModel({
factory: this.modelFactory,
notebookUri: this.notebookParams.notebookUri,
connectionService: this.connectionManagementService,
notificationService: this.notificationService,
notebookManager: this.notebookManager
}, false, this.profile);
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
await model.requestModelLoad(this.notebookParams.isTrusted);
model.contentChanged((change) => this.handleContentChanged(change));
this._model = model;
this._register(model);
this._modelRegisteredDeferred.resolve(this._model);
model.backgroundStartSession();
this._changeRef.detectChanges();
}
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.setDirty(true);
this._changeRef.detectChanges();
}
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 kernelInfoText = document.createElement('div');
kernelInfoText.className = 'notebook-info-label';
kernelInfoText.innerText = 'Kernel: ';
let kernelsDropdown = new KernelsDropdown(this.contextViewService, this.modelRegistered);
let kernelsDropdownTemplateContainer = document.createElement('div');
kernelsDropdownTemplateContainer.className = 'notebook-toolbar-dropdown';
kernelsDropdown.render(kernelsDropdownTemplateContainer);
attachSelectBoxStyler(kernelsDropdown, this.themeService);
let attachToDropdown = new AttachToDropdown(this.contextViewService);
let attachToDropdownTemplateContainer = document.createElement('div');
attachToDropdownTemplateContainer.className = 'notebook-toolbar-dropdown';
attachToDropdown.render(attachToDropdownTemplateContainer);
attachSelectBoxStyler(attachToDropdown, this.themeService);
let attachToInfoText = document.createElement('div');
attachToInfoText.className = 'notebook-info-label';
attachToInfoText.innerText = 'Attach To: ';
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', 'Code'), 'notebook-info-button');
addCodeCellButton.cellType = CellTypes.Code;
let addTextCellButton = new AddCellAction('notebook.AddTextCell',localize('text', 'Text'), 'notebook-info-button');
addTextCellButton.cellType = CellTypes.Markdown;
let taskbar = <HTMLElement>this.toolbar.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
this._actionBar.context = this;
this._actionBar.setContent([
{ element: kernelInfoText },
{ element: kernelsDropdownTemplateContainer },
{ element: attachToInfoText },
{ element: attachToDropdownTemplateContainer },
{ action: addCodeCellButton},
{ action: addTextCellButton}
]);
}
public async save(): Promise<boolean> {
try {
let saved = await this._model.saveModel();
return saved;
} catch (err) {
this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err)));
return false;
}
}
private setDirty(isDirty: boolean): void {
// TODO reenable handling of isDirty
// if (this.editor) {
// this.editor.isDirty = isDirty;
// }
}
}

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { Action } from 'vs/base/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TPromise } from 'vs/base/common/winjs.base';
import { Schemas } from 'vs/base/common/network';
import URI from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { NotebookInput, NotebookInputModel, notebooksEnabledCondition } from 'sql/parts/notebook/notebookInput';
import { NotebookEditor } from 'sql/parts/notebook/notebookEditor';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
let counter = 0;
/**
* todo: Will remove this code.
* This is the entry point to open the new Notebook
*/
export class NewNotebookAction extends Action {
public static ID = 'workbench.action.newnotebook';
public static LABEL = localize('workbench.action.newnotebook.description', 'New Notebook');
constructor(
id: string,
label: string,
@IEditorService private _editorService: IEditorService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
super(id, label);
}
public run(): TPromise<void> {
let title = `Untitled-${counter++}`;
let untitledUri = URI.from({ scheme: Schemas.untitled, path: title });
let model = new NotebookInputModel(untitledUri, undefined, false, undefined);
let input = this._instantiationService.createInstance(NotebookInput, title, model);
return this._editorService.openEditor(input, { pinned: true }).then(() => undefined);
}
}
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
NotebookEditor,
NotebookEditor.ID,
'Notebook'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]);
// Feature flag for built-in Notebooks. Will be removed in the future.
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'notebook',
'title': 'Notebook',
'type': 'object',
'properties': {
'notebook.enabled': {
'type': 'boolean',
'default': false,
'description': localize('notebook.enabledDescription', 'Enable viewing notebook files using built-in notebook editor.')
}
}
});
// this is the entry point to open the new Notebook
CommandsRegistry.registerCommand(NewNotebookAction.ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService).createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run();
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: NewNotebookAction.ID,
title:NewNotebookAction.LABEL,
},
when: notebooksEnabledCondition
});

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
.notebookEditor .notebook-toolbar-dropdown {
width: 150px;
padding-right: 10px;
}
.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-info-button {
display: inline-block;
width: 100%;
padding: 4px;
text-align: center;
cursor: pointer;
padding-left: 15px;
background-size: 11px;
margin-right: 0.3em;
font-size: 13px;
background-image: url("./media/light/add.svg")
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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 { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { ComponentHostDirective } from 'sql/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/parts/notebook/notebook.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component';
import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.component';
import { OutputAreaComponent } from 'sql/parts/notebook/cellViews/outputArea.component';
import { OutputComponent } from 'sql/parts/notebook/cellViews/output.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,
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;
};

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as sqlops from 'sqlops';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { localize } from 'vs/nls';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { INotebookModel } from 'sql/parts/notebook/models/modelInterfaces';
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { NotebookComponent } from 'sql/parts/notebook/notebook.component';
const msgLoading = localize('loading', 'Loading kernels...');
//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): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
context.addCell(this.cellType);
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
export class KernelsDropdown extends SelectBox {
private model: INotebookModel;
constructor(contextViewProvider: IContextViewProvider, modelRegistered: Promise<INotebookModel>
) {
super( [msgLoading], msgLoading, contextViewProvider);
if (modelRegistered) {
modelRegistered
.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;
model.kernelsChanged((defaultKernel) => {
this.updateKernel(defaultKernel);
});
if (model.clientSession) {
model.clientSession.kernelChanged((changedArgs: sqlops.nb.IKernelChangedArgs) => {
if (changedArgs.newValue) {
this.updateKernel(changedArgs.newValue);
}
});
}
}
// Update SelectBox values
private updateKernel(defaultKernel: sqlops.nb.IKernelSpec) {
let specs = this.model.specs;
if (specs && specs.kernels) {
let index = specs.kernels.findIndex((kernel => kernel.name === defaultKernel.name));
this.setOptions(specs.kernels.map(kernel => kernel.display_name), index);
}
}
public doChangeKernel(displayName: string): void {
this.model.changeKernel(displayName);
}
}
export class AttachToDropdown extends SelectBox {
constructor(contextViewProvider: IContextViewProvider
) {
let options: string[] = ['localhost'];
super(options, 'localhost', contextViewProvider);
}
}

View 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;
}

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
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 { $ } from 'vs/base/browser/builder';
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/parts/notebook/notebookInput';
import { NotebookModule } from 'sql/parts/notebook/notebook.module';
import { NOTEBOOK_SELECTOR } from 'sql/parts/notebook/notebook.component';
import { INotebookParams, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
export class NotebookEditor extends BaseEditor {
public static ID: string = 'workbench.editor.notebookEditor';
private _notebookContainer: HTMLElement;
protected _input: NotebookInput;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private instantiationService: IInstantiationService,
) {
super(NotebookEditor.ID, telemetryService, themeService);
}
public get input(): NotebookInput {
return this._input;
}
/**
* 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 {
}
public setInput(input: NotebookInput, options: EditorOptions): TPromise<void> {
if (this.input && this.input.matches(input)) {
return TPromise.as(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);
this.input.container = this._notebookContainer;
return TPromise.wrap<void>(this.bootstrapAngular(input));
} else {
this._notebookContainer = DOM.append(parentElement, this.input.container);
return TPromise.wrap<void>(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,
providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER,
isTrusted: input.isTrusted
};
bootstrapAngular(this.instantiationService,
NotebookModule,
this._notebookContainer,
NOTEBOOK_SELECTOR,
params,
input
);
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* 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 { TPromise } from 'vs/base/common/winjs.base';
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 { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { INotebookService } from 'sql/services/notebook/notebookService';
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
export let notebooksEnabledCondition = ContextKeyExpr.equals('config.notebook.enabled', true);
export class NotebookInputModel extends EditorModel {
private dirty: boolean;
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
private _providerId: string;
constructor(public readonly notebookUri: URI, private readonly handle: number, private _isTrusted: boolean = false, private saveHandler?: ModeViewSaveHandler) {
super();
this.dirty = false;
}
public get providerId(): string {
return this._providerId;
}
public set providerId(value: string) {
this._providerId = value;
}
get isTrusted(): boolean {
return this._isTrusted;
}
get onDidChangeDirty(): Event<void> {
return this._onDidChangeDirty.event;
}
get isDirty(): boolean {
return this.dirty;
}
public setDirty(dirty: boolean): void {
if (this.dirty === dirty) {
return;
}
this.dirty = dirty;
this._onDidChangeDirty.fire();
}
save(): TPromise<boolean> {
if (this.saveHandler) {
return TPromise.wrap(this.saveHandler(this.handle));
}
return TPromise.wrap(true);
}
}
export class NotebookInputValidator {
constructor(@IContextKeyService private readonly _contextKeyService: IContextKeyService) {}
public isNotebookEnabled(): boolean {
return this._contextKeyService.contextMatchesRules(notebooksEnabledCondition);
}
}
export class NotebookInput extends EditorInput {
public static ID: string = 'workbench.editorinputs.notebookInput';
public hasBootstrapped = false;
// Holds the HTML content for the editor when the editor discards this input and loads another
private _parentContainer: HTMLElement;
constructor(private _title: string,
private _model: NotebookInputModel,
@INotebookService private notebookService: INotebookService
) {
super();
this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire());
this.onDispose(() => {
if (this.notebookService) {
this.notebookService.handleNotebookClosed(this.notebookUri);
}
});
}
public get title(): string {
return this._title;
}
public get notebookUri(): URI {
return this._model.notebookUri;
}
public get providerId(): string {
return this._model.providerId;
}
public getTypeId(): string {
return NotebookInput.ID;
}
public resolve(refresh?: boolean): TPromise<IEditorModel> {
return undefined;
}
public getName(): string {
return this._title;
}
public get isTrusted(): boolean {
return this._model.isTrusted;
}
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 {
return this._model.isDirty;
}
/**
* Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result.
*/
confirmSave(): TPromise<ConfirmResult> {
// TODO #2530 support save on close / confirm save. This is significantly more work
// as we need to either integrate with textFileService (seems like this isn't viable)
// or register our own complimentary service that handles the lifecycle operations such
// as close all, auto save etc.
return TPromise.wrap(ConfirmResult.DONT_SAVE);
}
/**
* Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation.
*/
save(): TPromise<boolean> {
return this._model.save();
}
}

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./notebook';
import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { activeContrastBorder, buttonBackground } from 'vs/platform/theme/common/colorRegistry';
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
// Active border
const activeBorder = theme.getColor(buttonBackground);
if (activeBorder) {
collector.addRule(`
.notebookEditor .notebook-cell.active {
border-color: ${activeBorder};
border-width: 2px;
}
`);
}
// Inactive border
const inactiveBorder = theme.getColor(SIDE_BAR_BACKGROUND);
if (inactiveBorder) {
collector.addRule(`
.notebookEditor .notebook-cell {
border-color: ${inactiveBorder};
border-width: 1px;
}
`);
}
// Styling with Outline color (e.g. high contrast theme)
const outline = theme.getColor(activeContrastBorder);
if (outline) {
collector.addRule(`
.notebookEditor .notebook-cell.active {
outline-color: ${outline};
outline-width: 1px;
outline-style: solid;
}
.notebookEditor .notebook-cell:hover:not(.active) {
outline-style: dashed;
}
`);
}
});

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
import * as os from 'os';
import * as pfs from 'vs/base/node/pfs';
import { localize } from 'vs/nls';
import { IOutputChannel } from 'vs/workbench/parts/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);
}
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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'
);
}

View File

@@ -0,0 +1,87 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { IRenderMime } from './renderMimeInterfaces';
import { ReadonlyJSONObject } from './jsonext';
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
}

View 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 'sqlops';
/**
* 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;
}

View File

@@ -0,0 +1,110 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { JSONObject } from './jsonext';
import { MimeModel } from './mimemodel';
import * as JSONExt from './jsonext';
import { nbformat } from './nbformat';
import { nb } from 'sqlops';
/**
* 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.join('\n');
bundle['application/vnd.jupyter.stderr'] =
traceback || `${output.ename}: ${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 (JSONExt.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;
}

View File

@@ -0,0 +1,360 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { ReadonlyJSONObject } from './jsonext';
/**
* 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;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,184 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { JSONObject } from './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;
}
}

View File

@@ -0,0 +1,94 @@
/*-----------------------------------------------------------------------------
| 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)
};
/**
* The standard factories provided by the rendermime package.
*/
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
htmlRendererFactory,
// markdownRendererFactory,
// latexRendererFactory,
svgRendererFactory,
imageRendererFactory,
javaScriptRendererFactory,
textRendererFactory
];

View 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 './common/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;
});
}
}

View File

@@ -0,0 +1,629 @@
/*-----------------------------------------------------------------------------
| 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 JupyterLab 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;
}
}
/**
* 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 = '';
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,467 @@
/*-----------------------------------------------------------------------------
| 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;
color: var(--jp-content-link-color);
}
output-component .jp-RenderedHTMLCommon a:hover {
text-decoration: underline;
color: var(--jp-content-link-color);
}
output-component .jp-RenderedHTMLCommon a:visited {
text-decoration: none;
color: var(--jp-content-link-color);
}
/* 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: fixed;
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: right;
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;
}
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);
}

View File

@@ -0,0 +1,348 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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 './common/jsonext';
/**
* 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'
});
}
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
// TODO: The content of this file should be refactored to an extension
export function getKnoxUrl(host: string, port: string): string {
return `https://${host}:${port}/gateway`;
}
export function getLivyUrl(serverName: string, port: string): string {
return getKnoxUrl(serverName, port) + '/default/livy/v1/';
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
import * as json from 'vs/base/common/json';
import * as pfs from 'vs/base/node/pfs';
import URI from 'vs/base/common/uri';
import ContentManager = nb.ContentManager;
import INotebook = nb.INotebook;
export class LocalContentManager implements ContentManager {
public async getNotebookContents(notebookUri: URI): Promise<INotebook> {
if (!notebookUri) {
return undefined;
}
// TODO validate this is an actual file URI, and error if not
let path = notebookUri.fsPath;
// Note: intentionally letting caller handle exceptions
let notebookFileBuffer = await pfs.readFile(path);
return <INotebook>json.parse(notebookFileBuffer.toString());
}
public async save(notebookUri: URI, notebook: INotebook): Promise<INotebook> {
// Convert to JSON with pretty-print functionality
let contents = JSON.stringify(notebook, undefined, ' ');
let path = notebookUri.fsPath;
await pfs.writeFile(path, contents);
return notebook;
}
}

View File

@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* 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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { localize } from 'vs/nls';
import * as platform from 'vs/platform/registry/common/platform';
export const Extensions = {
NotebookProviderContribution: 'notebook.providers'
};
export interface NotebookProviderDescription {
provider: string;
fileExtensions: string | string[];
}
let notebookProviderType: IJSONSchema = {
type: 'object',
default: { provider: '', fileExtensions: [] },
properties: {
provider: {
description: localize('carbon.extension.contributes.notebook.provider', 'Identifier of the notebook provider.'),
type: 'string'
},
fileExtensions: {
description: localize('carbon.extension.contributes.notebook.fileExtensions', 'What file extensions should be registered to this notebook provider'),
oneOf: [
{ type: 'string' },
{
type: 'array',
items: {
type: 'string'
}
}
]
}
}
};
let notebookContrib: IJSONSchema = {
description: localize('vscode.extension.contributes.notebook.providers', "Contributes notebook providers."),
oneOf: [
notebookProviderType,
{
type: 'array',
items: notebookProviderType
}
]
};
export interface INotebookProviderRegistry {
registerNotebookProvider(provider: NotebookProviderDescription): void;
getSupportedFileExtensions(): string[];
getProviderForFileType(fileType: string): string;
}
class NotebookProviderRegistry implements INotebookProviderRegistry {
private providerIdToProviders = new Map<string, NotebookProviderDescription>();
private fileToProviders = new Map<string, NotebookProviderDescription>();
registerNotebookProvider(provider: NotebookProviderDescription): void {
// Note: this method intentionally overrides default provider for a file type.
// This means that any built-in provider will be overridden by registered extensions
this.providerIdToProviders.set(provider.provider, provider);
if (provider.fileExtensions) {
if (Array.isArray<string>(provider.fileExtensions)) {
for (let fileType of provider.fileExtensions) {
this.addFileProvider(fileType, provider);
}
} else {
this.addFileProvider(provider.fileExtensions, provider);
}
}
}
private addFileProvider(fileType: string, provider: NotebookProviderDescription) {
this.fileToProviders.set(fileType.toUpperCase(), provider);
}
getSupportedFileExtensions(): string[] {
return Array.from(this.fileToProviders.keys());
}
getProviderForFileType(fileType: string): string {
fileType = fileType.toUpperCase();
let provider = this.fileToProviders.get(fileType);
return provider ? provider.provider : undefined;
}
}
const notebookProviderRegistry = new NotebookProviderRegistry();
platform.Registry.add(Extensions.NotebookProviderContribution, notebookProviderRegistry);
ExtensionsRegistry.registerExtensionPoint<NotebookProviderDescription | NotebookProviderDescription[]>(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => {
function handleExtension(contrib: NotebookProviderDescription, extension: IExtensionPointUser<any>) {
notebookProviderRegistry.registerNotebookProvider(contrib);
}
for (let extension of extensions) {
const { value } = extension;
if (Array.isArray<NotebookProviderDescription>(value)) {
for (let command of value) {
handleExtension(command, extension);
}
} else {
handleExtension(value, extension);
}
}
});

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
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 { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
export const SERVICE_ID = 'notebookService';
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin';
export interface INotebookService {
_serviceBrand: any;
/**
* Register a metadata provider
*/
registerProvider(providerId: string, provider: INotebookProvider): void;
/**
* Register a metadata provider
*/
unregisterProvider(providerId: string): void;
/**
* Initializes and returns a Notebook manager that can handle all important calls to open, display, and
* run cells in a notebook.
* @param providerId ID for the provider to be used to instantiate a backend notebook service
* @param uri URI for a notebook that is to be opened. Based on this an existing manager may be used, or
* a new one may need to be created
*/
getOrCreateNotebookManager(providerId: string, uri: URI): Thenable<INotebookManager>;
handleNotebookClosed(uri: URI): void;
shutdown(): void;
getMimeRegistry(): RenderMimeRegistry;
}
export interface INotebookProvider {
readonly providerId: string;
getNotebookManager(notebookUri: URI): Thenable<INotebookManager>;
handleNotebookClosed(notebookUri: URI): void;
}
export interface INotebookManager {
providerId: string;
readonly contentManager: sqlops.nb.ContentManager;
readonly sessionManager: sqlops.nb.SessionManager;
readonly serverManager: sqlops.nb.ServerManager;
}
export interface INotebookParams extends IBootstrapParams {
notebookUri: URI;
providerId: string;
isTrusted: boolean;
profile?: IConnectionProfile;
modelFactory?: ModelFactory;
}

View File

@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* 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 'sqlops';
import { localize } from 'vs/nls';
import URI from 'vs/base/common/uri';
import { Registry } from 'vs/platform/registry/common/platform';
import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
import { SessionManager } from 'sql/services/notebook/sessionManager';
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB';
export class NotebookService implements INotebookService {
_serviceBrand: any;
private _mimeRegistry: RenderMimeRegistry;
private _providers: Map<string, INotebookProvider> = new Map();
private _managers: Map<string, INotebookManager> = new Map();
constructor() {
this.registerDefaultProvider();
}
private registerDefaultProvider() {
let defaultProvider = new BuiltinProvider();
this.registerProvider(defaultProvider.providerId, defaultProvider);
let registry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
registry.registerNotebookProvider({
provider: defaultProvider.providerId,
fileExtensions: DEFAULT_NOTEBOOK_FILETYPE
});
}
registerProvider(providerId: string, provider: INotebookProvider): void {
this._providers.set(providerId, provider);
}
unregisterProvider(providerId: string): void {
this._providers.delete(providerId);
}
public shutdown(): void {
this._managers.forEach(manager => {
if (manager.serverManager) {
// TODO should this thenable be awaited?
manager.serverManager.stopServer();
}
});
}
async getOrCreateNotebookManager(providerId: string, uri: URI): Promise<INotebookManager> {
if (!uri) {
throw new Error(localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager'));
}
let uriString = uri.toString();
let manager = this._managers.get(uriString);
if (!manager) {
manager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri));
if (manager) {
this._managers.set(uriString, manager);
}
}
return manager;
}
handleNotebookClosed(notebookUri: URI): void {
// Remove the manager from the tracked list, and let the notebook provider know that it should update its mappings
let uriString = notebookUri.toString();
let manager = this._managers.get(uriString);
if (manager) {
this._managers.delete(uriString);
let provider = this._providers.get(manager.providerId);
provider.handleNotebookClosed(notebookUri);
}
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Thenable<T> {
// Make sure the provider exists before attempting to retrieve accounts
let provider = this._providers.get(providerId);
if (!provider) {
return Promise.reject(new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then();
}
return op(provider);
}
//Returns an instantiation of RenderMimeRegistry class
getMimeRegistry(): RenderMimeRegistry {
if (!this._mimeRegistry) {
return new RenderMimeRegistry({
initialFactories: standardRendererFactories
});
}
return this._mimeRegistry;
}
}
export class BuiltinProvider implements INotebookProvider {
private manager: BuiltInNotebookManager;
constructor() {
this.manager = new BuiltInNotebookManager();
}
public get providerId(): string {
return DEFAULT_NOTEBOOK_PROVIDER;
}
getNotebookManager(notebookUri: URI): Thenable<INotebookManager> {
return Promise.resolve(this.manager);
}
handleNotebookClosed(notebookUri: URI): void {
// No-op
}
}
export class BuiltInNotebookManager implements INotebookManager {
private _contentManager: nb.ContentManager;
private _sessionManager: nb.SessionManager;
constructor() {
this._contentManager = new LocalContentManager();
this._sessionManager = new SessionManager();
}
public get providerId(): string {
return DEFAULT_NOTEBOOK_PROVIDER;
}
public get contentManager(): nb.ContentManager {
return this._contentManager;
}
public get serverManager(): nb.ServerManager {
return undefined;
}
public get sessionManager(): nb.SessionManager {
return this._sessionManager;
}
}

View File

@@ -0,0 +1,141 @@
'use strict';
import { nb } from 'sqlops';
import { localize } from 'vs/nls';
const noKernel: string = localize('noKernel', 'No Kernel');
let noKernelSpec: nb.IKernelSpec = ({
name: noKernel,
language: 'python',
display_name: noKernel
});
export class SessionManager implements nb.SessionManager {
public get isReady(): boolean {
return true;
}
public get ready(): Thenable<void> {
return Promise.resolve();
}
public get specs(): nb.IAllKernels {
let allKernels: nb.IAllKernels = {
defaultKernel: noKernel,
kernels: [noKernelSpec]
};
return allKernels;
}
startNew(options: nb.ISessionOptions): Thenable<nb.ISession> {
let session = new EmptySession(options);
return Promise.resolve(session);
}
shutdown(id: string): Thenable<void> {
return Promise.resolve();
}
}
class EmptySession implements nb.ISession {
private _kernel: EmptyKernel;
private _defaultKernelLoaded = false;
public set defaultKernelLoaded(value) {
this._defaultKernelLoaded = value;
}
public get defaultKernelLoaded(): boolean {
return this._defaultKernelLoaded;
}
constructor(private options: nb.ISessionOptions) {
this._kernel = new EmptyKernel();
}
public get canChangeKernels(): boolean {
return true;
}
public get id(): string {
return this.options.kernelId || '';
}
public get path(): string {
return this.options.path;
}
public get name(): string {
return this.options.name || '';
}
public get type(): string {
return this.options.type || '';
}
public get status(): nb.KernelStatus {
return 'connected';
}
public get kernel(): nb.IKernel {
return this._kernel;
}
changeKernel(kernelInfo: nb.IKernelSpec): Thenable<nb.IKernel> {
return Promise.resolve(this.kernel);
}
}
class EmptyKernel implements nb.IKernel {
public get id(): string {
return '-1';
}
public get name(): string {
return noKernel;
}
public get supportsIntellisense(): boolean {
return false;
}
public get isReady(): boolean {
return true;
}
public get ready(): Thenable<void> {
return Promise.resolve();
}
public get info(): nb.IInfoReply {
let info: nb.IInfoReply = {
protocol_version: '',
implementation: '',
implementation_version: '',
language_info: {
name: '',
version: '',
},
banner: '',
help_links: [{
text: '',
url: ''
}]
};
return info;
}
getSpec(): Thenable<nb.IKernelSpec> {
return Promise.resolve(noKernelSpec);
}
requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture {
throw new Error('Method not implemented.');
}
requestComplete(content: nb.ICompleteRequest): Thenable<nb.ICompleteReplyMsg> {
let response: Partial<nb.ICompleteReplyMsg> = { };
return Promise.resolve(response as nb.ICompleteReplyMsg);
}
}

View File

@@ -52,8 +52,8 @@ declare module 'sqlops' {
}
export interface TreeComponentView<T> extends vscode.Disposable {
onNodeCheckedChanged: vscode.Event<NodeCheckedEventParameters<T>>;
onDidChangeSelection: vscode.Event<vscode.TreeViewSelectionChangeEvent<T>>;
onNodeCheckedChanged: vscode.Event<NodeCheckedEventParameters<T>>;
onDidChangeSelection: vscode.Event<vscode.TreeViewSelectionChangeEvent<T>>;
}
export class TreeComponentItem extends vscode.TreeItem {
@@ -1365,4 +1365,619 @@ declare module 'sqlops' {
*/
export function openConnectionDialog(providers?: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: IConnectionCompletionOptions): Thenable<connection.Connection>;
}
export namespace nb {
export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable;
export interface NotebookProvider {
readonly providerId: string;
getNotebookManager(notebookUri: vscode.Uri): Thenable<NotebookManager>;
handleNotebookClosed(notebookUri: vscode.Uri): void;
}
export interface NotebookManager {
/**
* Manages reading and writing contents to/from files.
* Files may be local or remote, with this manager giving them a chance to convert and migrate
* from specific notebook file types to and from a standard type for this UI
*/
readonly contentManager: ContentManager;
/**
* A SessionManager that handles starting, stopping and handling notifications around sessions.
* Each notebook has 1 session associated with it, and the session is responsible
* for kernel management
*/
readonly sessionManager: SessionManager;
/**
* (Optional) ServerManager to handle server lifetime management operations.
* Depending on the implementation this may not be needed.
*/
readonly serverManager?: ServerManager;
}
/**
* Defines the contracts needed to manage the lifetime of a notebook server.
*/
export interface ServerManager {
/**
* Indicates if the server is started at the current time
*/
readonly isStarted: boolean;
/**
* Event sent when the server has started. This can be used to query
* the manager for server settings
*/
readonly onServerStarted: vscode.Event<void>;
/**
* Starts the server. Some server types may not support or require this.
* Should no-op if server is already started
*/
startServer(): Thenable<void>;
/**
* Stops the server. Some server types may not support or require this
*/
stopServer(): Thenable<void>;
}
//#region Content APIs
/**
* Handles interacting with file and folder contents
*/
export interface ContentManager {
/* Reads contents from a Uri representing a local or remote notebook and returns a
* JSON object containing the cells and metadata about the notebook
*/
getNotebookContents(notebookUri: vscode.Uri): Thenable<INotebook>;
/**
* Save a file.
*
* @param notebookUri - The desired file path.
*
* @param notebook - notebook to be saved.
*
* @returns A thenable which resolves with the file content model when the
* file is saved.
*/
save(notebookUri: vscode.Uri, notebook: INotebook): Thenable<INotebook>;
}
export interface INotebook {
readonly cells: ICell[];
readonly metadata: INotebookMetadata;
readonly nbformat: number;
readonly nbformat_minor: number;
}
export interface INotebookMetadata {
kernelspec: IKernelInfo;
language_info?: ILanguageInfo;
}
export interface IKernelInfo {
name: string;
language?: string;
display_name?: string;
}
export interface ILanguageInfo {
name: string;
version: string;
mimetype?: string;
codemirror_mode?: string | ICodeMirrorMode;
}
export interface ICodeMirrorMode {
name: string;
version: string;
}
export interface ICell {
cell_type: CellType;
source: string | string[];
metadata: {
language?: string;
};
execution_count: number;
outputs?: ICellOutput[];
}
export type CellType = 'code' | 'markdown' | 'raw';
export interface ICellOutput {
output_type: OutputType;
}
export interface IStreamResult extends ICellOutput {
/**
* Stream output field defining the stream name, for example stdout
*/
name: string;
/**
* Stream output field defining the multiline stream text
*/
text: string | Buffer;
}
export interface IDisplayResult extends ICellOutput {
/**
* Mime bundle expected to contain mime type -> contents mappings.
* This is dynamic and is controlled by kernels, so cannot be more specific
*/
data: {};
/**
* Optional metadata, also a mime bundle
*/
metadata?: {};
}
export interface IExecuteResult extends IDisplayResult {
/**
* Number of times the cell was executed
*/
executionCount: number;
}
export interface IErrorResult extends ICellOutput {
/**
* Exception name
*/
ename: string;
/**
* Exception value
*/
evalue: string;
/**
* Stacktrace equivalent
*/
traceback?: string[];
}
export type OutputType =
| 'execute_result'
| 'display_data'
| 'stream'
| 'error'
| 'update_display_data';
//#endregion
//#region Session APIs
export interface SessionManager {
/**
* Indicates whether the manager is ready.
*/
readonly isReady: boolean;
/**
* A Thenable that is fulfilled when the manager is ready.
*/
readonly ready: Thenable<void>;
readonly specs: IAllKernels | undefined;
startNew(options: ISessionOptions): Thenable<ISession>;
shutdown(id: string): Thenable<void>;
}
export interface ISession {
/**
* Is change of kernels supported for this session?
*/
canChangeKernels: boolean;
/*
* Unique id of the session.
*/
readonly id: string;
/**
* The current path associated with the session.
*/
readonly path: string;
/**
* The current name associated with the session.
*/
readonly name: string;
/**
* The type of the session.
*/
readonly type: string;
/**
* The status indicates if the kernel is healthy, dead, starting, etc.
*/
readonly status: KernelStatus;
/**
* The kernel.
*
* #### Notes
* This is a read-only property, and can be altered by [changeKernel].
*/
readonly kernel: IKernel;
/**
* Tracks whether the default kernel failed to load
* This could be for a reason such as the kernel name not being recognized as a valid kernel;
*/
defaultKernelLoaded?: boolean;
changeKernel(kernelInfo: IKernelSpec): Thenable<IKernel>;
}
export interface ISessionOptions {
/**
* The path (not including name) to the session.
*/
path: string;
/**
* The name of the session.
*/
name?: string;
/**
* The type of the session.
*/
type?: string;
/**
* The type of kernel (e.g. python3).
*/
kernelName?: string;
/**
* The id of an existing kernel.
*/
kernelId?: string;
}
export interface IKernel {
readonly id: string;
readonly name: string;
readonly supportsIntellisense: boolean;
/**
* Test whether the kernel is ready.
*/
readonly isReady: boolean;
/**
* A Thenable that is fulfilled when the kernel is ready.
*/
readonly ready: Thenable<void>;
/**
* The cached kernel info.
*
* #### Notes
* This value will be null until the kernel is ready.
*/
readonly info: IInfoReply | null;
/**
* Gets the full specification for this kernel, which can be serialized to
* a noteobok file
*/
getSpec(): Thenable<IKernelSpec>;
/**
* Send an `execute_request` message.
*
* @param content - The content of the request.
*
* @param disposeOnDone - Whether to dispose of the future when done.
*
* @returns A kernel future.
*
* #### Notes
* See [Messaging in
* Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#execute).
*
* This method returns a kernel future, rather than a Thenable, since execution may
* have many response messages (for example, many iopub display messages).
*
* Future `onReply` is called with the `execute_reply` content when the
* shell reply is received and validated.
*
* **See also:** [[IExecuteReply]]
*/
requestExecute(content: IExecuteRequest, disposeOnDone?: boolean): IFuture;
/**
* Send a `complete_request` message.
*
* @param content - The content of the request.
*
* @returns A Thenable that resolves with the response message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion).
*
* Fulfills with the `complete_reply` content when the shell reply is
* received and validated.
*/
requestComplete(content: ICompleteRequest): Thenable<ICompleteReplyMsg>;
}
export interface IInfoReply {
protocol_version: string;
implementation: string;
implementation_version: string;
language_info: ILanguageInfo;
banner: string;
help_links: {
text: string;
url: string;
}[];
}
/**
* The contents of a requestExecute message sent to the server.
*/
export interface IExecuteRequest extends IExecuteOptions {
code: string;
}
/**
* The options used to configure an execute request.
*/
export interface IExecuteOptions {
/**
* Whether to execute the code as quietly as possible.
* The default is `false`.
*/
silent?: boolean;
/**
* Whether to store history of the execution.
* The default `true` if silent is False.
* It is forced to `false ` if silent is `true`.
*/
store_history?: boolean;
/**
* A mapping of names to expressions to be evaluated in the
* kernel's interactive namespace.
*/
user_expressions?: {};
/**
* Whether to allow stdin requests.
* The default is `true`.
*/
allow_stdin?: boolean;
/**
* Whether to the abort execution queue on an error.
* The default is `false`.
*/
stop_on_error?: boolean;
}
/**
* The content of a `'complete_request'` message.
*
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion).
*
* **See also:** [[ICompleteReply]], [[IKernel.complete]]
*/
export interface ICompleteRequest {
code: string;
cursor_pos: number;
}
export interface ICompletionContent {
matches: string[];
cursor_start: number;
cursor_end: number;
metadata: any;
status: 'ok' | 'error';
}
/**
* A `'complete_reply'` message on the `'stream'` channel.
*
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion).
*
* **See also:** [[ICompleteRequest]], [[IKernel.complete]]
*/
export interface ICompleteReplyMsg extends IShellMessage {
content: ICompletionContent;
}
/**
* The valid Kernel status states.
*/
export type KernelStatus =
| 'unknown'
| 'starting'
| 'reconnecting'
| 'idle'
| 'busy'
| 'restarting'
| 'dead'
| 'connected';
/**
* An arguments object for the kernel changed event.
*/
export interface IKernelChangedArgs {
oldValue: IKernel | null;
newValue: IKernel | null;
}
/// -------- JSON objects, and objects primarily intended not to have methods -----------
export interface IAllKernels {
defaultKernel: string;
kernels: IKernelSpec[];
}
export interface IKernelSpec {
name: string;
language?: string;
display_name?: string;
}
export interface MessageHandler<T extends IMessage> {
handle(message: T): void | Thenable<void>;
}
/**
* A Future interface for responses from the kernel.
*
* When a message is sent to a kernel, a Future is created to handle any
* responses that may come from the kernel.
*/
export interface IFuture extends vscode.Disposable {
/**
* The original outgoing message.
*/
readonly msg: IMessage;
/**
* A Thenable that resolves when the future is done.
*
* #### Notes
* The future is done when there are no more responses expected from the
* kernel.
*
* The `done` Thenable resolves to the reply message if there is one,
* otherwise it resolves to `undefined`.
*/
readonly done: Thenable<IShellMessage | undefined>;
/**
* Set the reply handler for the kernel future.
*
* #### Notes
* If the handler returns a Thenable, all kernel message processing pauses
* until the Thenable is resolved. If there is a reply message, the future
* `done` Thenable also resolves to the reply message after this handler has
* been called.
*/
setReplyHandler(handler: MessageHandler<IShellMessage>): void;
/**
* Sets the stdin handler for the kernel future.
*
* #### Notes
* If the handler returns a Thenable, all kernel message processing pauses
* until the Thenable is resolved.
*/
setStdInHandler(handler: MessageHandler<IStdinMessage>): void;
/**
* Sets the iopub handler for the kernel future.
*
* #### Notes
* If the handler returns a Thenable, all kernel message processing pauses
* until the Thenable is resolved.
*/
setIOPubHandler(handler: MessageHandler<IIOPubMessage>): void;
/**
* Register hook for IOPub messages.
*
* @param hook - The callback invoked for an IOPub message.
*
* #### Notes
* The IOPub hook system allows you to preempt the handlers for IOPub
* messages handled by the future.
*
* The most recently registered hook is run first. A hook can return a
* boolean or a Thenable to a boolean, in which case all kernel message
* processing pauses until the Thenable is fulfilled. If a hook return value
* resolves to false, any later hooks will not run and the function will
* return a Thenable resolving to false. If a hook throws an error, the error
* is logged to the console and the next hook is run. If a hook is
* registered during the hook processing, it will not run until the next
* message. If a hook is removed during the hook processing, it will be
* deactivated immediately.
*/
registerMessageHook(
hook: (msg: IIOPubMessage) => boolean | Thenable<boolean>
): void;
/**
* Remove a hook for IOPub messages.
*
* @param hook - The hook to remove.
*
* #### Notes
* If a hook is removed during the hook processing, it will be deactivated immediately.
*/
removeMessageHook(
hook: (msg: IIOPubMessage) => boolean | Thenable<boolean>
): void;
/**
* Send an `input_reply` message.
*/
sendInputReply(content: IInputReply): void;
}
/**
* The valid channel names.
*/
export type Channel = 'shell' | 'iopub' | 'stdin';
/**
* Kernel message header content.
*
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#general-message-format).
*
* **See also:** [[IMessage]]
*/
export interface IHeader {
username: string;
version: string;
session: string;
msg_id: string;
msg_type: string;
}
/**
* A kernel message
*/
export interface IMessage {
type: Channel;
header: IHeader;
parent_header: IHeader | {};
metadata: {};
content: any;
}
/**
* A kernel message on the `'shell'` channel.
*/
export interface IShellMessage extends IMessage {
channel: 'shell';
}
/**
* A kernel message on the `'iopub'` channel.
*/
export interface IIOPubMessage extends IMessage {
channel: 'iopub';
}
/**
* A kernel message on the `'stdin'` channel.
*/
export interface IStdinMessage extends IMessage {
channel: 'stdin';
}
/**
* The content of an `'input_reply'` message.
*/
export interface IInputReply {
value: string;
}
//#endregion
}
}

View File

@@ -411,3 +411,9 @@ export class SqlThemeIcon {
this.id = id;
}
}
export interface INotebookManagerDetails {
handle: number;
hasContentManager: boolean;
hasServerManager: boolean;
}

View File

@@ -0,0 +1,194 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { TPromise } from 'vs/base/common/winjs.base';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
import { localize } from 'vs/nls';
import { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import URI, { UriComponents } from 'vs/base/common/uri';
import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
export class ExtHostNotebook implements ExtHostNotebookShape {
private static _handlePool: number = 0;
private readonly _proxy: MainThreadNotebookShape;
private _providers = new Map<number, sqlops.nb.NotebookProvider>();
// Notebook URI to manager lookup.
private _managers = new Map<number, NotebookManagerAdapter>();
constructor(private _mainContext: IMainContext) {
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook);
}
//#region APIs called by main thread
async $getNotebookManager(providerHandle: number, notebookUri: UriComponents): Promise<INotebookManagerDetails> {
let uri = URI.revive(notebookUri);
let uriString = uri.toString();
let adapter = this.findManagerForUri(uriString);
if (!adapter) {
adapter = await this._withProvider(providerHandle, (provider) => {
return this.createManager(provider, uri);
});
}
return {
handle: adapter.managerHandle,
hasContentManager: !!adapter.contentManager,
hasServerManager: !!adapter.serverManager
};
}
$handleNotebookClosed(notebookUri: UriComponents): void {
let uri = URI.revive(notebookUri);
let uriString = uri.toString();
let manager = this.findManagerForUri(uriString);
if (manager) {
manager.provider.handleNotebookClosed(uri);
this._managers.delete(manager.managerHandle);
}
}
$doStartServer(managerHandle: number): Thenable<void> {
return this._withServerManager(managerHandle, (serverManager) => serverManager.startServer());
}
$doStopServer(managerHandle: number): Thenable<void> {
return this._withServerManager(managerHandle, (serverManager) => serverManager.stopServer());
}
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebook> {
return this._withContentManager(managerHandle, (contentManager) => contentManager.getNotebookContents(URI.revive(notebookUri)));
}
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook> {
return this._withContentManager(managerHandle, (contentManager) => contentManager.save(URI.revive(notebookUri), notebook));
}
//#endregion
//#region APIs called by extensions
registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable {
if (!provider || !provider.providerId) {
throw new Error(localize('providerRequired', 'A NotebookProvider with valid providerId must be passed to this method'));
}
const handle = this._addNewProvider(provider);
this._proxy.$registerNotebookProvider(provider.providerId, handle);
return this._createDisposable(handle);
}
//#endregion
//#region private methods
private findManagerForUri(uriString: string): NotebookManagerAdapter {
for(let manager of Array.from(this._managers.values())) {
if (manager.uriString === uriString) {
return manager;
}
}
return undefined;
}
private async createManager(provider: sqlops.nb.NotebookProvider, notebookUri: URI): Promise<NotebookManagerAdapter> {
let manager = await provider.getNotebookManager(notebookUri);
let uriString = notebookUri.toString();
let handle = this._nextHandle();
let adapter = new NotebookManagerAdapter(provider, handle, manager, uriString);
this._managers.set(handle, adapter);
return adapter;
}
private _createDisposable(handle: number): Disposable {
return new Disposable(() => {
this._providers.delete(handle);
this._proxy.$unregisterNotebookProvider(handle);
});
}
private _nextHandle(): number {
return ExtHostNotebook._handlePool++;
}
private _withProvider<R>(handle: number, callback: (provider: sqlops.nb.NotebookProvider) => R | PromiseLike<R>): TPromise<R> {
let provider = this._providers.get(handle);
if (provider === undefined) {
return TPromise.wrapError<R>(new Error(localize('errNoProvider', 'no notebook provider found')));
}
return TPromise.wrap(callback(provider));
}
private _withNotebookManager<R>(handle: number, callback: (manager: NotebookManagerAdapter) => R | PromiseLike<R>): TPromise<R> {
let manager = this._managers.get(handle);
if (manager === undefined) {
return TPromise.wrapError<R>(new Error(localize('errNoManager', 'No Manager found')));
}
return TPromise.wrap(callback(manager));
}
private _withServerManager<R>(handle: number, callback: (manager: sqlops.nb.ServerManager) => R | PromiseLike<R>): TPromise<R> {
return this._withNotebookManager(handle, (notebookManager) => {
let serverManager = notebookManager.serverManager;
if (!serverManager) {
return TPromise.wrapError(new Error(localize('noServerManager', 'Notebook Manager for notebook {0} does not have a server manager. Cannot perform operations on it', notebookManager.uriString)));
}
return callback(serverManager);
});
}
private _withContentManager<R>(handle: number, callback: (manager: sqlops.nb.ContentManager) => R | PromiseLike<R>): TPromise<R> {
return this._withNotebookManager(handle, (notebookManager) => {
let contentManager = notebookManager.contentManager;
if (!contentManager) {
return TPromise.wrapError(new Error(localize('noContentManager', 'Notebook Manager for notebook {0} does not have a content manager. Cannot perform operations on it', notebookManager.uriString)));
}
return callback(contentManager);
});
}
private _withSessionManager<R>(handle: number, callback: (manager: sqlops.nb.SessionManager) => R | PromiseLike<R>): TPromise<R> {
return this._withNotebookManager(handle, (notebookManager) => {
let sessionManager = notebookManager.sessionManager;
if (!sessionManager) {
return TPromise.wrapError(new Error(localize('noSessionManager', 'Notebook Manager for notebook {0} does not have a session manager. Cannot perform operations on it', notebookManager.uriString)));
}
return callback(sessionManager);
});
}
private _addNewProvider(adapter: sqlops.nb.NotebookProvider): number {
const handle = this._nextHandle();
this._providers.set(handle, adapter);
return handle;
}
//#endregion
}
class NotebookManagerAdapter implements sqlops.nb.NotebookManager {
constructor(
public readonly provider: sqlops.nb.NotebookProvider,
public readonly managerHandle: number,
private manager: sqlops.nb.NotebookManager,
public readonly uriString: string
) {
}
public get contentManager(): sqlops.nb.ContentManager {
return this.manager.contentManager;
}
public get sessionManager(): sqlops.nb.SessionManager {
return this.manager.sessionManager;
}
public get serverManager(): sqlops.nb.ServerManager {
return this.manager.serverManager;
}
}

View File

@@ -0,0 +1,199 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { SqlExtHostContext, SqlMainContext, ExtHostNotebookShape, MainThreadNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { Event, Emitter } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { INotebookService, INotebookProvider, INotebookManager } from 'sql/services/notebook/notebookService';
import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
@extHostNamedCustomer(SqlMainContext.MainThreadNotebook)
export class MainThreadNotebook extends Disposable implements MainThreadNotebookShape {
private _proxy: ExtHostNotebookShape;
private _providers = new Map<number, NotebookProviderWrapper>();
constructor(
extHostContext: IExtHostContext,
@INotebookService private notebookService: INotebookService
) {
super();
if (extHostContext) {
this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostNotebook);
}
}
//#region Extension host callable methods
public $registerNotebookProvider(providerId: string, handle: number): void {
let notebookProvider = new NotebookProviderWrapper(this._proxy, providerId, handle);
this._providers.set(handle, notebookProvider);
this.notebookService.registerProvider(providerId, notebookProvider);
}
public $unregisterNotebookProvider(handle: number): void {
let registration = this._providers.get(handle);
if (registration) {
this.notebookService.unregisterProvider(registration.providerId);
registration.dispose();
this._providers.delete(handle);
}
}
//#endregion
}
class NotebookProviderWrapper extends Disposable implements INotebookProvider {
private _managers = new Map<string, NotebookManagerWrapper>();
constructor(private _proxy: ExtHostNotebookShape, public readonly providerId, public readonly providerHandle: number) {
super();
}
getNotebookManager(notebookUri: URI): Thenable<INotebookManager> {
// TODO must call through to setup in the extension host
return this.doGetNotebookManager(notebookUri);
}
private async doGetNotebookManager(notebookUri: URI): Promise<INotebookManager> {
let uriString = notebookUri.toString();
let manager = this._managers.get(uriString);
if (!manager) {
manager = new NotebookManagerWrapper(this._proxy, this.providerId, notebookUri);
await manager.initialize(this.providerHandle);
this._managers.set(uriString, manager);
}
return manager;
}
handleNotebookClosed(notebookUri: URI): void {
this._proxy.$handleNotebookClosed(notebookUri);
}
}
class NotebookManagerWrapper implements INotebookManager {
private _sessionManager: sqlops.nb.SessionManager;
private _contentManager: sqlops.nb.ContentManager;
private _serverManager: sqlops.nb.ServerManager;
private managerDetails: INotebookManagerDetails;
constructor(private _proxy: ExtHostNotebookShape,
public readonly providerId,
private notebookUri: URI
) { }
public async initialize(providerHandle: number): Promise<NotebookManagerWrapper> {
this.managerDetails = await this._proxy.$getNotebookManager(providerHandle, this.notebookUri);
let managerHandle = this.managerDetails.handle;
this._contentManager = this.managerDetails.hasContentManager ? new ContentManagerWrapper(managerHandle, this._proxy) : new LocalContentManager();
this._serverManager = this.managerDetails.hasServerManager ? new ServerManagerWrapper(managerHandle, this._proxy) : undefined;
this._sessionManager = new SessionManagerWrapper(managerHandle, this._proxy);
return this;
}
public get sessionManager(): sqlops.nb.SessionManager {
return this._sessionManager;
}
public get contentManager(): sqlops.nb.ContentManager {
return this._contentManager;
}
public get serverManager(): sqlops.nb.ServerManager {
return this._serverManager;
}
public get managerHandle(): number {
return this.managerDetails.handle;
}
}
class ContentManagerWrapper implements sqlops.nb.ContentManager {
constructor(private handle: number, private _proxy: ExtHostNotebookShape) {
}
getNotebookContents(notebookUri: URI): Thenable<sqlops.nb.INotebook> {
return this._proxy.$getNotebookContents(this.handle, notebookUri);
}
save(path: URI, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook> {
return this._proxy.$save(this.handle, path, notebook);
}
}
class ServerManagerWrapper implements sqlops.nb.ServerManager {
private onServerStartedEmitter: Emitter<void>;
private _isStarted: boolean;
constructor(private handle: number, private _proxy: ExtHostNotebookShape) {
this._isStarted = false;
}
get isStarted(): boolean {
return this._isStarted;
}
get onServerStarted(): Event<void> {
return this.onServerStartedEmitter.event;
}
startServer(): Thenable<void> {
return this.doStartServer();
}
private async doStartServer(): Promise<void> {
await this._proxy.$doStartServer(this.handle);
this._isStarted = true;
this.onServerStartedEmitter.fire();
}
stopServer(): Thenable<void> {
return this.doStopServer();
}
private async doStopServer(): Promise<void> {
try {
await this._proxy.$doStopServer(this.handle);
} finally {
// Always consider this a stopping event, even if a failure occurred.
this._isStarted = false;
}
}
}
class SessionManagerWrapper implements sqlops.nb.SessionManager {
constructor(private handle: number, private _proxy: ExtHostNotebookShape) {
}
get isReady(): boolean {
throw new Error('Method not implemented.');
}
get ready(): Thenable<void> {
throw new Error('Method not implemented.');
}
get specs(): sqlops.nb.IAllKernels {
throw new Error('Method not implemented.');
}
startNew(options: sqlops.nb.ISessionOptions): Thenable<sqlops.nb.ISession> {
throw new Error('Method not implemented.');
}
shutdown(id: string): Thenable<void> {
throw new Error('Method not implemented.');
}
}

View File

@@ -37,6 +37,7 @@ import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewD
import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelViewTree';
import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor';
import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement';
import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook';
export interface ISqlExtensionApiFactory {
vsCodeFactory(extension: IExtensionDescription): typeof vscode;
@@ -73,6 +74,7 @@ export function createApiFactory(
const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol));
const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement));
const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol));
const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol));
return {
@@ -402,6 +404,12 @@ export function createApiFactory(
}
};
const nb = {
registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable {
return extHostNotebook.registerNotebookProvider(provider);
}
};
return {
accounts,
connection,
@@ -437,7 +445,8 @@ export function createApiFactory(
CardType: sqlExtHostTypes.CardType,
Orientation: sqlExtHostTypes.Orientation,
SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon,
TreeComponentItem: sqlExtHostTypes.TreeComponentItem
TreeComponentItem: sqlExtHostTypes.TreeComponentItem,
nb: nb
};
}
};

View File

@@ -23,7 +23,8 @@ import 'sql/workbench/api/node/mainThreadDashboardWebview';
import 'sql/workbench/api/node/mainThreadQueryEditor';
import 'sql/workbench/api/node/mainThreadModelView';
import 'sql/workbench/api/node/mainThreadModelViewDialog';
import './mainThreadAccountManagement';
import 'sql/workbench/api/node/mainThreadNotebook';
import 'sql/workbench/api/node/mainThreadAccountManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
export class SqlExtHostContribution implements IWorkbenchContribution {

View File

@@ -21,7 +21,7 @@ import { ITreeComponentItem } from 'sql/workbench/common/views';
import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks';
import {
IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails,
IModelViewWizardDetails, IModelViewWizardPageDetails
IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails
} from 'sql/workbench/api/common/sqlExtHostTypes';
export abstract class ExtHostAccountManagementShape {
@@ -545,6 +545,7 @@ export const SqlMainContext = {
MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'),
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook')
};
export const SqlExtHostContext = {
@@ -563,7 +564,8 @@ export const SqlExtHostContext = {
ExtHostModelViewTreeViews: createExtId<ExtHostModelViewTreeViewsShape>('ExtHostModelViewTreeViews'),
ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'),
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor')
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor'),
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook')
};
export interface MainThreadDashboardShape extends IDisposable {
@@ -703,4 +705,27 @@ export interface ExtHostQueryEditorShape {
export interface MainThreadQueryEditorShape extends IDisposable {
$connect(fileUri: string, connectionId: string): Thenable<void>;
$runQuery(fileUri: string): void;
}
}
export interface ExtHostNotebookShape {
/**
* Looks up a notebook manager for a given notebook URI
* @param {number} providerHandle
* @param {vscode.Uri} notebookUri
* @returns {Thenable<string>} handle of the manager to be used when sending
*/
$getNotebookManager(providerHandle: number, notebookUri: UriComponents): Thenable<INotebookManagerDetails>;
$handleNotebookClosed(notebookUri: UriComponents): void;
$doStartServer(managerHandle: number): Thenable<void>;
$doStopServer(managerHandle: number): Thenable<void>;
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebook>;
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook>;
}
export interface MainThreadNotebookShape extends IDisposable {
$registerNotebookProvider(providerId: string, handle: number): void;
$unregisterNotebookProvider(handle: number): void;
}