No browser from common (#7178)

* no browser from common

* clean up some imports
This commit is contained in:
Anthony Dresser
2019-09-12 14:52:42 -07:00
committed by GitHub
parent a67e62b2d0
commit 823d136a00
134 changed files with 269 additions and 274 deletions

View File

@@ -0,0 +1,652 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb, ServerInfo } from 'azdata';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import * as notebookUtils from 'sql/workbench/parts/notebook/browser/models/notebookUtils';
import { CellTypes, CellType, NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { NotebookModel } from 'sql/workbench/parts/notebook/browser/models/notebookModel';
import { ICellModel, notebookConstants, IOutputChangedEvent, FutureInternal, CellExecutionState, ICellModelOptions } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Schemas } from 'vs/base/common/network';
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { getErrorMessage } from 'vs/base/common/errors';
import { generateUuid } from 'vs/base/common/uuid';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
let modelId = 0;
export class CellModel implements ICellModel {
private _cellType: nb.CellType;
private _source: string | string[];
private _language: string;
private _cellGuid: string;
private _future: FutureInternal;
private _outputs: nb.ICellOutput[] = [];
private _isEditMode: boolean;
private _onOutputsChanged = new Emitter<IOutputChangedEvent>();
private _onCellModeChanged = new Emitter<boolean>();
private _onExecutionStateChanged = new Emitter<CellExecutionState>();
private _isTrusted: boolean;
private _active: boolean;
private _hover: boolean;
private _executionCount: number | undefined;
private _cellUri: URI;
public id: string;
private _connectionManagementService: IConnectionManagementService;
private _stdInHandler: nb.MessageHandler<nb.IStdinMessage>;
private _onCellLoaded = new Emitter<string>();
private _loaded: boolean;
private _stdInVisible: boolean;
private _metadata: { language?: string, cellGuid?: string; };
private _modelContentChangedEvent: IModelContentChangedEvent;
constructor(cellData: nb.ICellContents,
private _options: ICellModelOptions,
@optional(INotebookService) private _notebookService?: INotebookService
) {
this.id = `${modelId++}`;
if (cellData) {
// Read in contents if available
this.fromJSON(cellData);
} else {
this._cellType = CellTypes.Code;
this._source = '';
}
this._isEditMode = this._cellType !== CellTypes.Markdown;
this._stdInVisible = false;
if (_options && _options.isTrusted) {
this._isTrusted = true;
} else {
this._isTrusted = false;
}
// if the fromJson() method was already called and _cellGuid was previously set, don't generate another UUID unnecessarily
this._cellGuid = this._cellGuid || generateUuid();
this.createUri();
}
public equals(other: ICellModel) {
return other && other.id === this.id;
}
public get onOutputsChanged(): Event<IOutputChangedEvent> {
return this._onOutputsChanged.event;
}
public get onCellModeChanged(): Event<boolean> {
return this._onCellModeChanged.event;
}
public get isEditMode(): boolean {
return this._isEditMode;
}
public get future(): FutureInternal {
return this._future;
}
public set isEditMode(isEditMode: boolean) {
this._isEditMode = isEditMode;
this._onCellModeChanged.fire(this._isEditMode);
// Note: this does not require a notebook update as it does not change overall state
}
public get trustedMode(): boolean {
return this._isTrusted;
}
public set trustedMode(isTrusted: boolean) {
if (this._isTrusted !== isTrusted) {
this._isTrusted = isTrusted;
let outputEvent: IOutputChangedEvent = {
outputs: this._outputs,
shouldScroll: false
};
this._onOutputsChanged.fire(outputEvent);
}
}
public get active(): boolean {
return this._active;
}
public set active(value: boolean) {
this._active = value;
this.fireExecutionStateChanged();
}
public get hover(): boolean {
return this._hover;
}
public set hover(value: boolean) {
this._hover = value;
this.fireExecutionStateChanged();
}
public get executionCount(): number | undefined {
return this._executionCount;
}
public set executionCount(value: number | undefined) {
this._executionCount = value;
this.fireExecutionStateChanged();
}
public get cellUri(): URI {
return this._cellUri;
}
public get notebookModel(): NotebookModel {
return <NotebookModel>this.options.notebook;
}
public set cellUri(value: URI) {
this._cellUri = value;
}
public get options(): ICellModelOptions {
return this._options;
}
public get cellType(): CellType {
return this._cellType;
}
public get source(): string | string[] {
return this._source;
}
public set source(newSource: string | string[]) {
newSource = this.getMultilineSource(newSource);
if (this._source !== newSource) {
this._source = newSource;
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
}
this._modelContentChangedEvent = undefined;
}
public get modelContentChangedEvent(): IModelContentChangedEvent {
return this._modelContentChangedEvent;
}
public set modelContentChangedEvent(e: IModelContentChangedEvent) {
this._modelContentChangedEvent = e;
}
public get language(): string {
if (this._cellType === CellTypes.Markdown) {
return 'markdown';
}
if (this._language) {
return this._language;
}
return this.options.notebook.language;
}
public get cellGuid(): string {
return this._cellGuid;
}
public setOverrideLanguage(newLanguage: string) {
this._language = newLanguage;
}
public get onExecutionStateChange(): Event<CellExecutionState> {
return this._onExecutionStateChanged.event;
}
private fireExecutionStateChanged(): void {
this._onExecutionStateChanged.fire(this.executionState);
}
public get onLoaded(): Event<string> {
return this._onCellLoaded.event;
}
public get loaded(): boolean {
return this._loaded;
}
public set loaded(val: boolean) {
this._loaded = val;
if (val) {
this._onCellLoaded.fire(this._cellType);
}
}
public get stdInVisible(): boolean {
return this._stdInVisible;
}
public set stdInVisible(val: boolean) {
this._stdInVisible = val;
}
private notifyExecutionComplete(): void {
if (this._notebookService) {
this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted, this);
}
}
public get executionState(): CellExecutionState {
let isRunning = !!(this._future && this._future.inProgress);
if (isRunning) {
return CellExecutionState.Running;
} else if (this.active || this.hover) {
return CellExecutionState.Stopped;
}
// TODO save error state and show the error
return CellExecutionState.Hidden;
}
public async runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean> {
try {
if (!this.active && this !== this.notebookModel.activeCell) {
this.notebookModel.updateActiveCell(this);
this.active = true;
}
if (connectionManagementService) {
this._connectionManagementService = connectionManagementService;
}
if (this.cellType !== CellTypes.Code) {
// TODO should change hidden state to false if we add support
// for this property
return false;
}
let kernel = await this.getOrStartKernel(notificationService);
if (!kernel) {
return false;
}
// If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt()
// This matches the same behavior as JupyterLab
if (this.future && this.future.inProgress) {
this.future.inProgress = false;
await kernel.interrupt();
this.sendNotification(notificationService, Severity.Info, localize('runCellCancelled', "Cell execution cancelled"));
} else {
// TODO update source based on editor component contents
if (kernel.requiresConnection && !this.notebookModel.activeConnection) {
let connected = await this.notebookModel.requestConnection();
if (!connected) {
return false;
}
}
let content = this.source;
if ((Array.isArray(content) && content.length > 0) || (!Array.isArray(content) && content)) {
// requestExecute expects a string for the code parameter
content = Array.isArray(content) ? content.join('') : content;
let future = await kernel.requestExecute({
code: content,
stop_on_error: true
}, false);
this.setFuture(future as FutureInternal);
this.fireExecutionStateChanged();
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
let result: nb.IExecuteReplyMsg = <nb.IExecuteReplyMsg><any>await future.done;
if (result && result.content) {
this.executionCount = result.content.execution_count;
if (result.content.status !== 'ok') {
// TODO track error state
return false;
}
}
}
}
} catch (error) {
let message: string;
if (error.message === 'Canceled') {
message = localize('executionCanceled', "Query execution was canceled");
} else {
message = getErrorMessage(error);
}
this.sendNotification(notificationService, Severity.Error, message);
// TODO track error state for the cell
} finally {
this.disposeFuture();
this.fireExecutionStateChanged();
this.notifyExecutionComplete();
}
return true;
}
private async getOrStartKernel(notificationService: INotificationService): Promise<nb.IKernel> {
let model = this.options.notebook;
let clientSession = model && model.clientSession;
if (!clientSession) {
this.sendNotification(notificationService, Severity.Error, localize('notebookNotReady', "The session for this notebook is not yet ready"));
return undefined;
} else if (!clientSession.isReady || clientSession.status === 'dead') {
this.sendNotification(notificationService, Severity.Info, localize('sessionNotReady', "The session for this notebook will start momentarily"));
await clientSession.kernelChangeCompleted;
}
if (!clientSession.kernel) {
let defaultKernel = model && model.defaultKernel && model.defaultKernel.name;
if (!defaultKernel) {
this.sendNotification(notificationService, Severity.Error, localize('noDefaultKernel', "No kernel is available for this notebook"));
return undefined;
}
await clientSession.changeKernel({
name: defaultKernel
});
}
return clientSession.kernel;
}
private sendNotification(notificationService: INotificationService, severity: Severity, message: string): void {
if (notificationService) {
notificationService.notify({ severity: severity, message: message });
}
}
/**
* Sets the future which will be used to update the output
* area for this cell
*/
setFuture(future: FutureInternal): void {
if (this._future === future) {
// Nothing to do
return;
}
// Setting the future indicates the cell is running which enables trusted mode.
// See https://jupyter-notebook.readthedocs.io/en/stable/security.html
this._isTrusted = true;
if (this._future) {
this._future.dispose();
}
this.clearOutputs();
this._future = future;
future.setReplyHandler({ handle: (msg) => this.handleReply(msg) });
future.setIOPubHandler({ handle: (msg) => this.handleIOPub(msg) });
future.setStdInHandler({ handle: (msg) => this.handleSdtIn(msg) });
}
public clearOutputs(): void {
this._outputs = [];
this.fireOutputsChanged();
}
private fireOutputsChanged(shouldScroll: boolean = false): void {
let outputEvent: IOutputChangedEvent = {
outputs: this.outputs,
shouldScroll: !!shouldScroll
};
this._onOutputsChanged.fire(outputEvent);
if (this.outputs.length !== 0) {
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated);
} else {
this.sendChangeToNotebook(NotebookChangeType.CellOutputCleared);
}
}
private sendChangeToNotebook(change: NotebookChangeType): void {
if (this._options && this._options.notebook) {
this._options.notebook.onCellChange(this, change);
}
}
public get outputs(): Array<nb.ICellOutput> {
return this._outputs;
}
private handleReply(msg: nb.IShellMessage): void {
// TODO #931 we should process this. There can be a payload attached which should be added to outputs.
// In all other cases, it is a no-op
let output: nb.ICellOutput = msg.content as nb.ICellOutput;
if (!this._future.inProgress) {
this.disposeFuture();
}
}
private handleIOPub(msg: nb.IIOPubMessage): void {
let msgType = msg.header.msg_type;
let displayId = this.getDisplayId(msg);
let output: nb.ICellOutput;
switch (msgType) {
case 'execute_result':
case 'display_data':
case 'stream':
case 'error':
output = msg.content as nb.ICellOutput;
output.output_type = msgType;
break;
case 'clear_output':
// TODO wait until next message before clearing
// let wait = (msg as KernelMessage.IClearOutputMsg).content.wait;
this.clearOutputs();
break;
case 'update_display_data':
output = msg.content as nb.ICellOutput;
output.output_type = 'display_data';
// TODO #930 handle in-place update of displayed data
// targets = this._displayIdMap.get(displayId);
// if (targets) {
// for (let index of targets) {
// model.set(index, output);
// }
// }
break;
default:
break;
}
// TODO handle in-place update of displayed data
// if (displayId && msgType === 'display_data') {
// targets = this._displayIdMap.get(displayId) || [];
// targets.push(model.length - 1);
// this._displayIdMap.set(displayId, targets);
// }
if (output) {
// deletes transient node in the serialized JSON
delete output['transient'];
this._outputs.push(this.rewriteOutputUrls(output));
// Only scroll on 1st output being added
let shouldScroll = this._outputs.length === 1;
this.fireOutputsChanged(shouldScroll);
}
}
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
const driverLog = '/gateway/default/yarn/container';
const yarnUi = '/gateway/default/yarn/proxy';
const defaultPort = ':30433';
// Only rewrite if this is coming back during execution, not when loading from disk.
// A good approximation is that the model has a future (needed for execution)
if (this.future) {
try {
let result = output as nb.IDisplayResult;
if (result && result.data && result.data['text/html']) {
let model = (this as CellModel).options.notebook as NotebookModel;
if (model.activeConnection) {
let gatewayEndpointInfo = this.getGatewayEndpoint(model.activeConnection);
if (gatewayEndpointInfo) {
let hostAndIp = notebookUtils.getHostAndPortFromEndpoint(gatewayEndpointInfo.endpoint);
let host = gatewayEndpointInfo && hostAndIp.host ? hostAndIp.host : model.activeConnection.serverName;
let port = gatewayEndpointInfo && hostAndIp.port ? ':' + hostAndIp.port : defaultPort;
let html = result.data['text/html'];
// CTP 3.1 and earlier Spark link
html = this.rewriteUrlUsingRegex(/(https?:\/\/master.*\/proxy)(.*)/g, html, host, port, yarnUi);
// CTP 3.2 and later spark link
html = this.rewriteUrlUsingRegex(/(https?:\/\/sparkhead.*\/proxy)(.*)/g, html, host, port, yarnUi);
// Driver link
html = this.rewriteUrlUsingRegex(/(https?:\/\/storage.*\/containerlogs)(.*)/g, html, host, port, driverLog);
(<nb.IDisplayResult>output).data['text/html'] = html;
}
}
}
}
catch (e) { }
}
return output;
}
private rewriteUrlUsingRegex(regex: RegExp, html: string, host: string, port: string, target: string): string {
return html.replace(regex, function (a, b, c) {
let ret = '';
if (b !== '') {
ret = 'https://' + host + port + target;
}
if (c !== '') {
ret = ret + c;
}
return ret;
});
}
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
let transient = (msg.content.transient || {});
return transient['display_id'] as string;
}
public setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void {
this._stdInHandler = handler;
}
/**
* StdIn requires user interaction, so this is deferred to upstream UI
* components. If one is registered the cell will call and wait on it, if not
* it will immediately return to unblock error handling
*/
private handleSdtIn(msg: nb.IStdinMessage): void | Thenable<void> {
let handler = async () => {
if (!this._stdInHandler) {
// No-op
return;
}
try {
await this._stdInHandler.handle(msg);
} catch (err) {
if (this.future) {
// TODO should we error out in this case somehow? E.g. send Ctrl+C?
this.future.sendInputReply({ value: '' });
}
}
};
return handler();
}
public toJSON(): nb.ICellContents {
let cellJson: Partial<nb.ICellContents> = {
cell_type: this._cellType,
source: this._source,
metadata: this._metadata || {}
};
cellJson.metadata.azdata_cell_guid = this._cellGuid;
if (this._cellType === CellTypes.Code) {
cellJson.metadata.language = this._language;
cellJson.outputs = this._outputs;
cellJson.execution_count = this.executionCount ? this.executionCount : 0;
}
return cellJson as nb.ICellContents;
}
public fromJSON(cell: nb.ICellContents): void {
if (!cell) {
return;
}
this._cellType = cell.cell_type;
this.executionCount = cell.execution_count;
this._source = this.getMultilineSource(cell.source);
this._metadata = cell.metadata;
this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid();
this.setLanguageFromContents(cell);
if (cell.outputs) {
for (let output of cell.outputs) {
// For now, we're assuming it's OK to save these as-is with no modification
this.addOutput(output);
}
}
}
private setLanguageFromContents(cell: nb.ICellContents): void {
if (cell.cell_type === CellTypes.Markdown) {
this._language = 'markdown';
} else if (cell.metadata && cell.metadata.language) {
this._language = cell.metadata.language;
}
// else skip, we set default language anyhow
}
private addOutput(output: nb.ICellOutput) {
this._normalize(output);
this._outputs.push(output);
}
/**
* Normalize an output.
*/
private _normalize(value: nb.ICellOutput): void {
if (notebookUtils.isStream(value)) {
if (Array.isArray(value.text)) {
value.text = (value.text as string[]).join('\n');
}
}
}
private createUri(): void {
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` });
// Use this to set the internal (immutable) and public (shared with extension) uri properties
this.cellUri = uri;
}
// Get Knox endpoint from IConnectionProfile
// TODO: this will be refactored out into the notebooks extension as a contribution point
private getGatewayEndpoint(activeConnection: IConnectionProfile): notebookUtils.IEndpoint {
let endpoint;
if (this._connectionManagementService && activeConnection && activeConnection.providerName.toLowerCase() === notebookConstants.SQL_CONNECTION_PROVIDER.toLowerCase()) {
let serverInfo: ServerInfo = this._connectionManagementService.getServerInfo(activeConnection.id);
if (serverInfo) {
let endpoints: notebookUtils.IEndpoint[] = notebookUtils.getClusterEndpoints(serverInfo);
if (endpoints && endpoints.length > 0) {
endpoint = endpoints.find(ep => ep.serviceName.toLowerCase() === notebookUtils.hadoopEndpointNameGateway);
}
}
}
return endpoint;
}
private getMultilineSource(source: string | string[]): string | string[] {
if (typeof source === 'string') {
let sourceMultiline = source.split('\n');
// If source is one line (i.e. no '\n'), return it immediately
if (sourceMultiline.length === 1) {
return [source];
} else if (sourceMultiline.length === 0) {
return [];
}
// Otherwise, add back all of the newlines here
// Note: for Windows machines that require '/r/n',
// splitting on '\n' and putting back the '\n' will still
// retain the '\r', so that isn't lost in the process
// Note: the last line will not include a newline at the end
for (let i = 0; i < sourceMultiline.length - 1; i++) {
sourceMultiline[i] += '\n';
}
return sourceMultiline;
}
return source;
}
// Dispose and set current future to undefined
private disposeFuture() {
if (this._future) {
this._future.dispose();
}
this._future = undefined;
}
}

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICellMagicMapper } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
const defaultKernel = '*';
export class CellMagicMapper implements ICellMagicMapper {
private kernelToMagicMap = new Map<string, ILanguageMagic[]>();
constructor(languageMagics: ILanguageMagic[]) {
if (languageMagics) {
for (let magic of languageMagics) {
if (!magic.kernels || magic.kernels.length === 0) {
this.addKernelMapping(defaultKernel, magic);
}
if (magic.kernels) {
for (let kernel of magic.kernels) {
this.addKernelMapping(kernel.toLowerCase(), magic);
}
}
}
}
}
private addKernelMapping(kernelId: string, magic: ILanguageMagic): void {
let magics = this.kernelToMagicMap.get(kernelId) || [];
magics.push(magic);
this.kernelToMagicMap.set(kernelId, magics);
}
private findMagicForKernel(searchText: string, kernelId: string): ILanguageMagic | undefined {
if (kernelId === undefined || !searchText) {
return undefined;
}
searchText = searchText.toLowerCase();
let kernelMagics = this.kernelToMagicMap.get(kernelId) || [];
if (kernelMagics) {
return kernelMagics.find(m => m.magic.toLowerCase() === searchText);
}
return undefined;
}
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic {
let languageMagic = this.findMagicForKernel(magic, kernelId.toLowerCase());
if (!languageMagic) {
languageMagic = this.findMagicForKernel(magic, defaultKernel);
}
return languageMagic;
}
}

View File

@@ -0,0 +1,367 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx
import { nb } from 'azdata';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { getErrorMessage } from 'vs/base/common/errors';
import { IClientSession, IKernelPreference, IClientSessionOptions } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { Deferred } from 'sql/base/common/promise';
import { INotebookManager } from 'sql/workbench/services/notebook/browser/notebookService';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
type KernelChangeHandler = (kernel: nb.IKernelChangedArgs) => Promise<void>;
/**
* Implementation of a client session. This is a model over session operations,
* which may come from the session manager or a specific session.
*/
export class ClientSession implements IClientSession {
//#region private fields with public accessors
private _terminatedEmitter = new Emitter<void>();
private _kernelChangedEmitter = new Emitter<nb.IKernelChangedArgs>();
private _statusChangedEmitter = new Emitter<nb.ISession>();
private _iopubMessageEmitter = new Emitter<nb.IMessage>();
private _unhandledMessageEmitter = new Emitter<nb.IMessage>();
private _propertyChangedEmitter = new Emitter<'path' | 'name' | 'type'>();
private _notebookUri: URI;
private _type: string;
private _name: string;
private _isReady: boolean;
private _ready: Deferred<void>;
private _kernelChangeCompleted: Deferred<void>;
private _kernelPreference: IKernelPreference;
private _kernelDisplayName: string;
private _errorMessage: string;
private _cachedKernelSpec: nb.IKernelSpec;
private _kernelChangeHandlers: KernelChangeHandler[] = [];
private _defaultKernel: nb.IKernelSpec;
//#endregion
private _serverLoadFinished: Promise<void>;
private _session: nb.ISession;
private isServerStarted: boolean;
private notebookManager: INotebookManager;
private _kernelConfigActions: ((kernelName: string) => Promise<any>)[] = [];
constructor(private options: IClientSessionOptions) {
this._notebookUri = options.notebookUri;
this.notebookManager = options.notebookManager;
this._isReady = false;
this._ready = new Deferred<void>();
this._kernelChangeCompleted = new Deferred<void>();
this._defaultKernel = options.kernelSpec;
}
public async initialize(): Promise<void> {
try {
this._serverLoadFinished = this.startServer();
await this._serverLoadFinished;
await this.initializeSession();
await this.updateCachedKernelSpec();
} catch (err) {
this._errorMessage = getErrorMessage(err) || localize('clientSession.unknownError', "An error occurred while starting the notebook session");
}
// Always resolving for now. It's up to callers to check for error case
this._isReady = true;
this._ready.resolve();
if (!this.isInErrorState && this._session && this._session.kernel) {
await this.notifyKernelChanged(undefined, this._session.kernel);
}
}
private async startServer(): Promise<void> {
let serverManager = this.notebookManager.serverManager;
if (serverManager && !serverManager.isStarted) {
await serverManager.startServer();
if (!serverManager.isStarted) {
throw new Error(localize('ServerNotStarted', "Server did not start for unknown reason"));
}
this.isServerStarted = serverManager.isStarted;
} else {
this.isServerStarted = true;
}
}
private async initializeSession(): Promise<void> {
await this._serverLoadFinished;
if (this.isServerStarted) {
if (!this.notebookManager.sessionManager.isReady) {
await this.notebookManager.sessionManager.ready;
}
if (this._defaultKernel) {
await this.startSessionInstance(this._defaultKernel.name);
}
}
}
private async startSessionInstance(kernelName: string): Promise<void> {
let session: nb.ISession;
try {
// TODO #3164 should use URI instead of path for startNew
session = await this.notebookManager.sessionManager.startNew({
path: this.notebookUri.fsPath,
kernelName: kernelName
// TODO add kernel name if saved in the document
});
session.defaultKernelLoaded = true;
} catch (err) {
// TODO move registration
if (err && err.response && err.response.status === 501) {
this.options.notificationService.warn(localize('kernelRequiresConnection', "Kernel {0} was not found. The default kernel will be used instead.", kernelName));
session = await this.notebookManager.sessionManager.startNew({
path: this.notebookUri.fsPath,
kernelName: undefined
});
session.defaultKernelLoaded = false;
} else {
throw err;
}
}
this._session = session;
await this.runKernelConfigActions(kernelName);
this._statusChangedEmitter.fire(session);
}
private async runKernelConfigActions(kernelName: string): Promise<void> {
for (let startAction of this._kernelConfigActions) {
await startAction(kernelName);
}
}
public dispose(): void {
// No-op for now
}
/**
* Indicates the server has finished loading. It may have failed to load in
* which case the view will be in an error state.
*/
public get serverLoadFinished(): Promise<void> {
return this._serverLoadFinished;
}
//#region IClientSession Properties
public get terminated(): Event<void> {
return this._terminatedEmitter.event;
}
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
return this._kernelChangedEmitter.event;
}
public onKernelChanging(changeHandler: (kernel: nb.IKernelChangedArgs) => Promise<void>): void {
if (changeHandler) {
this._kernelChangeHandlers.push(changeHandler);
}
}
public get statusChanged(): Event<nb.ISession> {
return this._statusChangedEmitter.event;
}
public get iopubMessage(): Event<nb.IMessage> {
return this._iopubMessageEmitter.event;
}
public get unhandledMessage(): Event<nb.IMessage> {
return this._unhandledMessageEmitter.event;
}
public get propertyChanged(): Event<'path' | 'name' | 'type'> {
return this._propertyChangedEmitter.event;
}
public get kernel(): nb.IKernel | null {
return this._session ? this._session.kernel : undefined;
}
public get notebookUri(): URI {
return this._notebookUri;
}
public get name(): string {
return this._name;
}
public get type(): string {
return this._type;
}
public get status(): nb.KernelStatus {
if (!this.isReady) {
return 'starting';
}
return this._session ? this._session.status : 'dead';
}
public get isReady(): boolean {
return this._isReady;
}
public get ready(): Promise<void> {
return this._ready.promise;
}
public get kernelChangeCompleted(): Promise<void> {
return this._kernelChangeCompleted.promise;
}
public get kernelPreference(): IKernelPreference {
return this._kernelPreference;
}
public set kernelPreference(value: IKernelPreference) {
this._kernelPreference = value;
}
public get kernelDisplayName(): string {
return this._kernelDisplayName;
}
public get errorMessage(): string {
return this._errorMessage;
}
public get isInErrorState(): boolean {
return !!this._errorMessage;
}
public get cachedKernelSpec(): nb.IKernelSpec {
return this._cachedKernelSpec;
}
//#endregion
//#region Not Yet Implemented
/**
* Change the current kernel associated with the document.
*/
async changeKernel(options: nb.IKernelSpec, oldValue?: nb.IKernel): Promise<nb.IKernel> {
this._kernelChangeCompleted = new Deferred<void>();
this._isReady = false;
let oldKernel = oldValue ? oldValue : this.kernel;
let newKernel = this.kernel;
let kernel = await this.doChangeKernel(options);
try {
await kernel.ready;
} catch (error) {
// Cleanup some state before re-throwing
this._isReady = kernel.isReady;
this._kernelChangeCompleted.resolve();
throw error;
}
newKernel = this._session ? kernel : this._session.kernel;
this._isReady = kernel.isReady;
await this.updateCachedKernelSpec();
// Send resolution events to listeners
await this.notifyKernelChanged(oldKernel, newKernel);
return kernel;
}
private async notifyKernelChanged(oldKernel: nb.IKernel, newKernel: nb.IKernel): Promise<void> {
let changeArgs: nb.IKernelChangedArgs = {
oldValue: oldKernel,
newValue: newKernel
};
let changePromises = this._kernelChangeHandlers.map(handler => handler(changeArgs));
await Promise.all(changePromises);
// Wait on connection configuration to complete before resolving full kernel change
this._kernelChangeCompleted.resolve();
this._kernelChangedEmitter.fire(changeArgs);
}
private async updateCachedKernelSpec(): Promise<void> {
this._cachedKernelSpec = undefined;
let kernel = this.kernel;
if (kernel) {
await kernel.ready;
if (kernel.isReady) {
this._cachedKernelSpec = await kernel.getSpec();
}
}
}
/**
* Helper method to either call ChangeKernel on current session, or start a new session
*/
private async doChangeKernel(options: nb.IKernelSpec): Promise<nb.IKernel> {
let kernel: nb.IKernel;
if (this._session) {
kernel = await this._session.changeKernel(options);
await this.runKernelConfigActions(kernel.name);
} else {
kernel = await this.startSessionInstance(options.name).then(() => this.kernel);
}
return kernel;
}
public async configureKernel(options: nb.IKernelSpec): Promise<void> {
if (this._session) {
await this._session.configureKernel(options);
}
}
public async updateConnection(connection: IConnectionProfile): Promise<void> {
if (!this.kernel) {
// TODO is there any case where skipping causes errors? So far it seems like it gets called twice
return;
}
if (connection.id !== '-1') {
await this._session.configureConnection(connection);
}
}
/**
* Kill the kernel and shutdown the session.
*
* @returns A promise that resolves when the session is shut down.
*/
public async shutdown(): Promise<void> {
// Always try to shut down session
if (this._session && this._session.id && this.notebookManager && this.notebookManager.sessionManager) {
await this.notebookManager.sessionManager.shutdown(this._session.id);
}
}
/**
* Select a kernel for the session.
*/
selectKernel(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Restart the session.
*
* @returns A promise that resolves with whether the kernel has restarted.
*
* #### Notes
* If there is a running kernel, present a dialog.
* If there is no kernel, we start a kernel with the last run
* kernel name and resolves with `true`. If no kernel has been started,
* this is a no-op, and resolves with `false`.
*/
restart(): Promise<boolean> {
throw new Error('Not implemented');
}
/**
* Change the session path.
*
* @param path - The new session path.
*
* @returns A promise that resolves when the session has renamed.
*
* #### Notes
* This uses the Jupyter REST API, and the response is validated.
* The promise is fulfilled on a valid response and rejected otherwise.
*/
setPath(path: string): Promise<void> {
throw new Error('Not implemented');
}
/**
* Change the session name.
*/
setName(name: string): Promise<void> {
throw new Error('Not implemented');
}
/**
* Change the session type.
*/
setType(type: string): Promise<void> {
throw new Error('Not implemented');
}
//#endregion
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { CellModel } from 'sql/workbench/parts/notebook/browser/models/cell';
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { ClientSession } from 'sql/workbench/parts/notebook/browser/models/clientSession';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class ModelFactory implements IModelFactory {
constructor(private instantiationService: IInstantiationService) {
}
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
return this.instantiationService.createInstance(CellModel, cell, options);
}
public createClientSession(options: IClientSessionOptions): IClientSession {
return new ClientSession(options);
}
}

View File

@@ -0,0 +1,561 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx
import { nb } from 'azdata';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { CellType, NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { INotebookManager, ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IStandardKernelWithProvider } from 'sql/workbench/parts/notebook/browser/models/notebookUtils';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { localize } from 'vs/nls';
import { NotebookModel } from 'sql/workbench/parts/notebook/browser/models/notebookModel';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
export interface IClientSessionOptions {
notebookUri: URI;
notebookManager: INotebookManager;
notificationService: INotificationService;
kernelSpec: nb.IKernelSpec;
}
/**
* The interface of client session object.
*
* The client session represents the link between
* a path and its kernel for the duration of the lifetime
* of the session object. The session can have no current
* kernel, and can start a new kernel at any time.
*/
export interface IClientSession extends IDisposable {
/**
* A signal emitted when the session is shut down.
*/
readonly terminated: Event<void>;
/**
* A signal emitted when the kernel changes.
*/
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
/**
* A signal emitted when the kernel status changes.
*/
readonly statusChanged: Event<nb.ISession>;
/**
* A signal emitted for a kernel messages.
*/
readonly iopubMessage: Event<nb.IMessage>;
/**
* A signal emitted for an unhandled kernel message.
*/
readonly unhandledMessage: Event<nb.IMessage>;
/**
* A signal emitted when a session property changes.
*/
readonly propertyChanged: Event<'path' | 'name' | 'type'>;
/**
* The current kernel associated with the document.
*/
readonly kernel: nb.IKernel | null;
/**
* The current path associated with the client session.
*/
readonly notebookUri: URI;
/**
* The current name associated with the client session.
*/
readonly name: string;
/**
* The type of the client session.
*/
readonly type: string;
/**
* The current status of the client session.
*/
readonly status: nb.KernelStatus;
/**
* Whether the session is ready.
*/
readonly isReady: boolean;
/**
* Whether the session is in an unusable state
*/
readonly isInErrorState: boolean;
/**
* The error information, if this session is in an error state
*/
readonly errorMessage: string;
/**
* A promise that is fulfilled when the session is ready.
*/
readonly ready: Promise<void>;
/**
* A promise that is fulfilled when the session completes a kernel change.
*/
readonly kernelChangeCompleted: Promise<void>;
/**
* The kernel preference.
*/
kernelPreference: IKernelPreference;
/**
* The display name of the kernel.
*/
readonly kernelDisplayName: string;
readonly cachedKernelSpec: nb.IKernelSpec;
/**
* Initializes the ClientSession, by starting the server and
* connecting to the SessionManager.
* This will optionally start a session if the kernel preferences
* indicate this is desired
*/
initialize(): Promise<void>;
/**
* Change the current kernel associated with the document.
*/
changeKernel(
options: nb.IKernelSpec,
oldKernel?: nb.IKernel
): Promise<nb.IKernel>;
/**
* Configure the current kernel associated with the document.
*/
configureKernel(
options: nb.IKernelSpec
): Promise<void>;
/**
* Kill the kernel and shutdown the session.
*
* @returns A promise that resolves when the session is shut down.
*/
shutdown(): Promise<void>;
/**
* Select a kernel for the session.
*/
selectKernel(): Promise<void>;
/**
* Restart the session.
*
* @returns A promise that resolves with whether the kernel has restarted.
*
* #### Notes
* If there is a running kernel, present a dialog.
* If there is no kernel, we start a kernel with the last run
* kernel name and resolves with `true`. If no kernel has been started,
* this is a no-op, and resolves with `false`.
*/
restart(): Promise<boolean>;
/**
* Change the session path.
*
* @param path - The new session path.
*
* @returns A promise that resolves when the session has renamed.
*
* #### Notes
* This uses the Jupyter REST API, and the response is validated.
* The promise is fulfilled on a valid response and rejected otherwise.
*/
setPath(path: string): Promise<void>;
/**
* Change the session name.
*/
setName(name: string): Promise<void>;
/**
* Change the session type.
*/
setType(type: string): Promise<void>;
/**
* Updates the connection
*/
updateConnection(connection: IConnectionProfile): Promise<void>;
/**
* Supports registering a handler to run during kernel change and implement any calls needed to configure
* the kernel before actions such as run should be allowed
*/
onKernelChanging(changeHandler: ((kernel: nb.IKernelChangedArgs) => Promise<void>)): void;
}
export interface IDefaultConnection {
defaultConnection: ConnectionProfile;
otherConnections: ConnectionProfile[];
}
/**
* A kernel preference.
*/
export interface IKernelPreference {
/**
* The name of the kernel.
*/
readonly name?: string;
/**
* The preferred kernel language.
*/
readonly language?: string;
/**
* The id of an existing kernel.
*/
readonly id?: string;
/**
* Whether to prefer starting a kernel.
*/
readonly shouldStart?: boolean;
/**
* Whether a kernel can be started.
*/
readonly canStart?: boolean;
/**
* Whether to auto-start the default kernel if no matching kernel is found.
*/
readonly autoStartDefault?: boolean;
}
export interface INotebookModel {
/**
* Cell List for this model
*/
readonly cells: ReadonlyArray<ICellModel>;
/**
* The active cell for this model. May be undefined
*/
readonly activeCell: ICellModel;
/**
* Client Session in the notebook, used for sending requests to the notebook service
*/
readonly clientSession: IClientSession;
/**
* LanguageInfo saved in the notebook
*/
readonly languageInfo: nb.ILanguageInfo;
/**
* Current default language for the notebook
*/
readonly language: string;
/**
* All notebook managers applicable for a given notebook
*/
readonly notebookManagers: INotebookManager[];
/**
* Event fired on first initialization of the kernel and
* on subsequent change events
*/
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
/**
* Fired on notifications that notebook components should be re-laid out.
*/
readonly layoutChanged: Event<void>;
/**
* Event fired on first initialization of the kernels and
* on subsequent change events
*/
readonly kernelsChanged: Event<nb.IKernelSpec>;
/**
* Default kernel
*/
defaultKernel?: nb.IKernelSpec;
/**
* Event fired on first initialization of the contexts and
* on subsequent change events
*/
readonly contextsChanged: Event<void>;
/**
* Event fired on when switching kernel and should show loading context
*/
readonly contextsLoading: Event<void>;
/**
* The specs for available kernels, or undefined if these have
* not been loaded yet
*/
readonly specs: nb.IAllKernels | undefined;
/**
* The specs for available contexts, or undefined if these have
* not been loaded yet
*/
readonly contexts: IDefaultConnection | undefined;
/**
* Event fired on first initialization of the cells and
* on subsequent change events
*/
readonly contentChanged: Event<NotebookContentChange>;
/**
* Event fired on notebook provider change
*/
readonly onProviderIdChange: Event<string>;
/**
* Event fired on active cell change
*/
readonly onActiveCellChanged: Event<ICellModel>;
/**
* The trusted mode of the Notebook
*/
trustedMode: boolean;
/**
* Current notebook provider id
*/
providerId: string;
/**
* Change the current kernel from the Kernel dropdown
* @param displayName kernel name (as displayed in Kernel dropdown)
*/
changeKernel(displayName: string): void;
/**
* Change the current context (if applicable)
*/
changeContext(host: string, connection?: IConnectionProfile, hideErrorMessage?: boolean): Promise<void>;
/**
* Find a cell's index given its model
*/
findCellIndex(cellModel: ICellModel): number;
/**
* Adds a cell to the index of the model
*/
addCell(cellType: CellType, index?: number): void;
/**
* Deletes a cell
*/
deleteCell(cellModel: ICellModel): void;
/**
* Serialize notebook cell content to JSON
*/
toJSON(type?: NotebookChangeType): nb.INotebookContents;
/**
* Notifies the notebook of a change in the cell
*/
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
/**
* Push edit operations, basically editing the model. This is the preferred way of
* editing the model. Long-term, this will ensure edit operations can be added to the undo stack
* @param edits The edit operations to perform
*/
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
getApplicableConnectionProviderIds(kernelName: string): string[];
/**
* Get the standardKernelWithProvider by name
* @param name The kernel name
*/
getStandardKernelFromName(name: string): IStandardKernelWithProvider;
/** Event fired once we get call back from ConfigureConnection method in sqlops extension */
readonly onValidConnectionSelected: Event<boolean>;
serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void;
standardKernels: IStandardKernelWithProvider[];
/**
* Updates the model's view of an active cell to the new active cell
* @param cell New active cell
*/
updateActiveCell(cell: ICellModel);
}
export interface NotebookContentChange {
/**
* The type of change that occurred
*/
changeType: NotebookChangeType;
/**
* Optional cells that were changed
*/
cells?: ICellModel | ICellModel[];
/**
* Optional index of the change, indicating the cell at which an insert or
* delete occurred
*/
cellIndex?: number;
/**
* Optional value indicating if the notebook is in a dirty or clean state after this change
*/
isDirty?: boolean;
/**
* Text content changed event for cell edits
*/
modelContentChangedEvent?: IModelContentChangedEvent;
}
export interface ICellModelOptions {
notebook: INotebookModel;
isTrusted: boolean;
}
export enum CellExecutionState {
Hidden = 0,
Stopped = 1,
Running = 2,
Error = 3
}
export interface IOutputChangedEvent {
outputs: ReadonlyArray<nb.ICellOutput>;
shouldScroll: boolean;
}
export interface ICellModel {
cellUri: URI;
id: string;
readonly language: string;
readonly cellGuid: string;
source: string | string[];
cellType: CellType;
trustedMode: boolean;
active: boolean;
hover: boolean;
executionCount: number | undefined;
readonly future: FutureInternal;
readonly outputs: ReadonlyArray<nb.ICellOutput>;
readonly onOutputsChanged: Event<IOutputChangedEvent>;
readonly onExecutionStateChange: Event<CellExecutionState>;
readonly executionState: CellExecutionState;
readonly notebookModel: NotebookModel;
setFuture(future: FutureInternal): void;
setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void;
runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean>;
setOverrideLanguage(language: string);
equals(cellModel: ICellModel): boolean;
toJSON(): nb.ICellContents;
loaded: boolean;
stdInVisible: boolean;
readonly onLoaded: Event<string>;
modelContentChangedEvent: IModelContentChangedEvent;
}
export interface FutureInternal extends nb.IFuture {
inProgress: boolean;
}
export interface IModelFactory {
createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel;
createClientSession(options: IClientSessionOptions): IClientSession;
}
export interface IContentManager {
/**
* This is a specialized method intended to load for a default context - just the current Notebook's URI
*/
loadContent(): Promise<nb.INotebookContents>;
}
export interface INotebookModelOptions {
/**
* Path to the local or remote notebook
*/
notebookUri: URI;
/**
* Factory for creating cells and client sessions
*/
factory: IModelFactory;
contentManager: IContentManager;
notebookManagers: INotebookManager[];
providerId: string;
defaultKernel: nb.IKernelSpec;
cellMagicMapper: ICellMagicMapper;
layoutChanged: Event<void>;
notificationService: INotificationService;
connectionService: IConnectionManagementService;
capabilitiesService: ICapabilitiesService;
editorLoadedTimestamp?: number;
}
export interface ICellMagicMapper {
/**
* Tries to find a language mapping for an identified cell magic
* @param magic a string defining magic. For example for %%sql the magic text is sql
* @param kernelId the name of the current kernel to use when looking up magics
*/
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic | undefined;
}
export namespace notebookConstants {
export const SQL = 'SQL';
export const SQL_CONNECTION_PROVIDER = mssqlProviderName;
export const sqlKernel: string = localize('sqlKernel', "SQL");
export const sqlKernelSpec: nb.IKernelSpec = ({
name: sqlKernel,
language: 'sql',
display_name: sqlKernel
});
}
export interface INotebookContentsEditable {
cells: nb.ICellContents[];
metadata: nb.INotebookMetadata;
nbformat: number;
nbformat_minor: number;
}

View File

@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { localize } from 'vs/nls';
import { IDefaultConnection, notebookConstants } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
export class NotebookContexts {
private static get DefaultContext(): IDefaultConnection {
let defaultConnection: ConnectionProfile = <any>{
providerName: mssqlProviderName,
id: '-1',
serverName: localize('selectConnection', "Select Connection")
};
return {
// default context if no other contexts are applicable
defaultConnection: defaultConnection,
otherConnections: [defaultConnection]
};
}
private static get LocalContext(): IDefaultConnection {
let localConnection: ConnectionProfile = <any>{
providerName: mssqlProviderName,
id: '-1',
serverName: localize('localhost', "localhost")
};
return {
// default context if no other contexts are applicable
defaultConnection: localConnection,
otherConnections: [localConnection]
};
}
/**
* Get all of the applicable contexts for a given kernel
* @param connectionService connection management service
* @param connProviderIds array of connection provider ids applicable for a kernel
* @param kernelChangedArgs kernel changed args (both old and new kernel info)
* @param profile current connection profile
*/
public static async getContextsForKernel(connectionService: IConnectionManagementService, connProviderIds: string[], kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise<IDefaultConnection> {
let connections: IDefaultConnection = this.DefaultContext;
if (!profile) {
if (!kernelChangedArgs || !kernelChangedArgs.newValue ||
(kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) {
// nothing to do, kernels are the same or new kernel is undefined
return connections;
}
}
if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name && connProviderIds.length < 1) {
return connections;
} else {
connections = await this.getActiveContexts(connectionService, connProviderIds, profile);
}
return connections;
}
/**
* Get all active contexts and sort them
* @param apiWrapper ApiWrapper
* @param profile current connection profile
*/
public static async getActiveContexts(connectionService: IConnectionManagementService, connProviderIds: string[], profile: IConnectionProfile): Promise<IDefaultConnection> {
let defaultConnection: ConnectionProfile = NotebookContexts.DefaultContext.defaultConnection;
let activeConnections: ConnectionProfile[] = await connectionService.getActiveConnections();
if (activeConnections && activeConnections.length > 0) {
activeConnections = activeConnections.filter(conn => conn.id !== '-1');
}
// If no connection provider ids exist for a given kernel, the attach to should show localhost
if (connProviderIds.length === 0) {
return NotebookContexts.LocalContext;
}
// If no active connections exist, show "Select connection" as the default value
if (activeConnections.length === 0) {
return NotebookContexts.DefaultContext;
}
// Filter active connections by their provider ids to match kernel's supported connection providers
else if (activeConnections.length > 0) {
let connections = activeConnections.filter(connection => {
return connProviderIds.includes(connection.providerName);
});
if (connections && connections.length > 0) {
defaultConnection = connections[0];
if (profile && profile.options) {
if (connections.find(connection => connection.serverName === profile.serverName)) {
defaultConnection = connections.find(connection => connection.serverName === profile.serverName);
}
}
} else if (connections.length === 0) {
return NotebookContexts.DefaultContext;
}
activeConnections = [];
connections.forEach(connection => activeConnections.push(connection));
}
if (defaultConnection === NotebookContexts.DefaultContext.defaultConnection) {
let newConnection = <ConnectionProfile><any>{
providerName: 'SQL',
id: '-2',
serverName: localize('addConnection', "Add New Connection")
};
activeConnections.push(newConnection);
}
return {
otherConnections: activeConnections,
defaultConnection: defaultConnection
};
}
/**
*
* @param specs kernel specs (comes from session manager)
* @param displayName kernel info loaded from
*/
public static getDefaultKernel(specs: nb.IAllKernels, displayName: string): nb.IKernelSpec {
let defaultKernel: nb.IKernelSpec;
if (specs) {
// find the saved kernel (if it exists)
if (displayName) {
defaultKernel = specs.kernels.find((kernel) => kernel.display_name === displayName);
}
// if no saved kernel exists, use the default KernelSpec
if (!defaultKernel) {
defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
}
if (defaultKernel) {
return defaultKernel;
}
}
// If no default kernel specified (should never happen), default to SQL
if (!defaultKernel) {
defaultKernel = notebookConstants.sqlKernelSpec;
}
return defaultKernel;
}
}

View File

@@ -10,11 +10,11 @@ import { URI } from 'vs/base/common/uri';
import * as resources from 'vs/base/common/resources';
import * as azdata from 'azdata';
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/common/notebookService';
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/parts/notebook/browser/models/notebookUtils';
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { Schemas } from 'vs/base/common/network';
@@ -26,7 +26,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
import { IDisposable } from 'vs/base/common/lifecycle';
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { Deferred } from 'sql/base/common/promise';
import { NotebookTextFileModel } from 'sql/workbench/parts/notebook/common/models/notebookTextFileModel';
import { NotebookTextFileModel } from 'sql/workbench/parts/notebook/browser/models/notebookTextFileModel';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,258 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range, IRange } from 'vs/editor/common/core/range';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { FindMatch } from 'vs/editor/common/model';
import { NotebookContentChange, INotebookModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
export class NotebookTextFileModel {
// save active cell's line/column in editor model for the beginning of the source property
private _sourceBeginRange: Range;
// save active cell's line/column in editor model for the beginning of the output property
private _outputBeginRange: Range;
// save active cell guid
private _activeCellGuid: string;
constructor(private _eol: string) {
}
public get activeCellGuid(): string {
return this._activeCellGuid;
}
public set activeCellGuid(guid: string) {
if (this._activeCellGuid !== guid) {
this._sourceBeginRange = undefined;
this._outputBeginRange = undefined;
this._activeCellGuid = guid;
}
}
public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
// convert the range to leverage offsets in the json
if (contentChange && contentChange.modelContentChangedEvent && areRangePropertiesPopulated(cellGuidRange)) {
contentChange.modelContentChangedEvent.changes.forEach(change => {
let convertedRange: IRange = {
startLineNumber: change.range.startLineNumber + cellGuidRange.startLineNumber - 1,
endLineNumber: change.range.endLineNumber + cellGuidRange.startLineNumber - 1,
startColumn: change.range.startColumn + cellGuidRange.startColumn,
endColumn: change.range.endColumn + cellGuidRange.startColumn
};
// Need to subtract one because we're going from 1-based to 0-based
let startSpaces: string = ' '.repeat(cellGuidRange.startColumn - 1);
// The text here transforms a string from 'This is a string\n this is another string' to:
// This is a string
// this is another string
textEditorModel.textEditorModel.applyEdits([{
range: new Range(convertedRange.startLineNumber, convertedRange.startColumn, convertedRange.endLineNumber, convertedRange.endColumn),
text: change.text.split(this._eol).join('\\n\",'.concat(this._eol).concat(startSpaces).concat('\"'))
}]);
});
} else {
return false;
}
return true;
}
public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) {
let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' ');
if (contentChange.cells[0].outputs.length > 1) {
newOutput = ', '.concat(newOutput);
} else {
newOutput = '\n'.concat(newOutput).concat('\n');
}
let range = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
if (range) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn),
text: newOutput
}]);
}
} else {
return false;
}
return true;
}
public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
if (executionCountMatch && executionCountMatch.range) {
// Execution count can be between 0 and n characters long
let beginExecutionCountColumn = executionCountMatch.range.endColumn;
let endExecutionCountColumn = beginExecutionCountColumn + 1;
let lineContent = textEditorModel.textEditorModel.getLineContent(executionCountMatch.range.endLineNumber);
while (lineContent[endExecutionCountColumn - 1]) {
endExecutionCountColumn++;
}
if (contentChange.cells[0].executionCount) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(executionCountMatch.range.startLineNumber, beginExecutionCountColumn, executionCountMatch.range.endLineNumber, endExecutionCountColumn),
text: contentChange.cells[0].executionCount.toString()
}]);
} else {
// This is a special case when cells are canceled; there will be no execution count included
return true;
}
} else {
return false;
}
return true;
}
public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) {
return false;
}
if (!this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid)) {
this.updateOutputBeginRange(textEditorModel, contentChange.cells[0].cellGuid);
}
let outputEndRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
let outputStartRange = this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
if (outputStartRange && outputEndRange) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(outputStartRange.startLineNumber, outputStartRange.endColumn, outputEndRange.endLineNumber, outputEndRange.endColumn),
text: ''
}]);
return true;
}
return false;
}
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: TextFileEditorModel | UntitledEditorModel) {
let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' ');
let model = textEditorModel.textEditorModel;
let endLine = model.getLineCount();
let endCol = model.getLineMaxColumn(endLine);
textEditorModel.textEditorModel.applyEdits([{
range: new Range(1, 1, endLine, endCol),
text: content
}]);
}
// Find the beginning of a cell's source in the text editor model
private updateSourceBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
if (!cellGuid) {
return;
}
this._sourceBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let sourceBefore = textEditorModel.textEditorModel.findPreviousMatch('"source": [', { lineNumber: cellGuidMatches[0].range.startLineNumber, column: cellGuidMatches[0].range.startColumn }, false, true, undefined, true);
if (!sourceBefore || !sourceBefore.range) {
return;
}
let firstQuoteOfSource = textEditorModel.textEditorModel.findNextMatch('"', { lineNumber: sourceBefore.range.startLineNumber, column: sourceBefore.range.endColumn }, false, true, undefined, true);
this._sourceBeginRange = firstQuoteOfSource.range;
} else {
return;
}
}
// Find the beginning of a cell's outputs in the text editor model
private updateOutputBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
if (!cellGuid) {
return undefined;
}
this._outputBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let outputsBegin = textEditorModel.textEditorModel.findNextMatch('"outputs": [', { lineNumber: cellGuidMatches[0].range.endLineNumber, column: cellGuidMatches[0].range.endColumn }, false, true, undefined, true);
if (!outputsBegin || !outputsBegin.range) {
return undefined;
}
this._outputBeginRange = outputsBegin.range;
} else {
return undefined;
}
}
// Find the end of a cell's outputs in the text editor model
// This will be used as a starting point for any future outputs
private getEndOfOutputs(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
let outputsBegin;
if (this._activeCellGuid === cellGuid) {
outputsBegin = this._outputBeginRange;
}
if (!outputsBegin || !textEditorModel.textEditorModel.getLineContent(outputsBegin.startLineNumber).trim().includes('output')) {
this.updateOutputBeginRange(textEditorModel, cellGuid);
outputsBegin = this._outputBeginRange;
if (!outputsBegin) {
return undefined;
}
}
let outputsEnd = textEditorModel.textEditorModel.matchBracket({ column: outputsBegin.endColumn - 1, lineNumber: outputsBegin.endLineNumber });
if (!outputsEnd || outputsEnd.length < 2) {
return undefined;
}
// single line output [i.e. no outputs exist for a cell]
if (outputsBegin.endLineNumber === outputsEnd[1].startLineNumber) {
// Adding 1 to startColumn to replace text starting one character after '['
return {
startColumn: outputsEnd[0].startColumn + 1,
startLineNumber: outputsEnd[0].startLineNumber,
endColumn: outputsEnd[0].endColumn,
endLineNumber: outputsEnd[0].endLineNumber
};
} else {
// Last 2 lines in multi-line output will look like the following:
// " }"
// " ],"
if (textEditorModel.textEditorModel.getLineContent(outputsEnd[1].endLineNumber - 1).trim() === '}') {
return {
startColumn: textEditorModel.textEditorModel.getLineFirstNonWhitespaceColumn(outputsEnd[1].endLineNumber - 1) + 1,
startLineNumber: outputsEnd[1].endLineNumber - 1,
endColumn: outputsEnd[1].endColumn - 1,
endLineNumber: outputsEnd[1].endLineNumber
};
}
return undefined;
}
}
// Determine what text needs to be replaced when execution counts are updated
private getExecutionCountRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid);
if (endOutputRange && endOutputRange.endLineNumber) {
return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true);
}
return undefined;
}
// Find a cell's location, given its cellGuid
// If it doesn't exist (e.g. it's not the active cell), attempt to find it
private getCellNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
if (this._activeCellGuid !== guid || !this._sourceBeginRange) {
this.updateSourceBeginRange(textEditorModel, guid);
}
return this._sourceBeginRange;
}
private getOutputNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
if (this._activeCellGuid !== guid) {
this.updateOutputBeginRange(textEditorModel, guid);
}
return this._outputBeginRange;
}
}
function areRangePropertiesPopulated(range: Range) {
return range && range.startLineNumber && range.startColumn && range.endLineNumber && range.endColumn;
}
function findOrSetCellGuidMatch(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): FindMatch[] {
if (!textEditorModel || !cellGuid) {
return undefined;
}
return textEditorModel.textEditorModel.findMatches(cellGuid, false, false, true, undefined, true);
}

View File

@@ -0,0 +1,183 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { nb, ServerInfo } from 'azdata';
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICellModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { URI } from 'vs/base/common/uri';
export const clusterEndpointsProperty = 'clusterEndpoints';
export const hadoopEndpointNameGateway = 'gateway';
/**
* Test whether an output is from a stream.
*/
export function isStream(output: nb.ICellOutput): output is nb.IStreamResult {
return output.output_type === 'stream';
}
export function getProvidersForFileName(fileName: string, notebookService: INotebookService): string[] {
let fileExt = path.extname(fileName);
let providers: string[];
// First try to get provider for actual file type
if (fileExt && fileExt.startsWith('.')) {
fileExt = fileExt.slice(1, fileExt.length);
providers = notebookService.getProvidersForFileType(fileExt);
}
// Fallback to provider for default file type (assume this is a global handler)
if (!providers) {
providers = notebookService.getProvidersForFileType(DEFAULT_NOTEBOOK_FILETYPE);
}
// Finally if all else fails, use the built-in handler
if (!providers) {
providers = [DEFAULT_NOTEBOOK_PROVIDER];
}
return providers;
}
export function getStandardKernelsForProvider(providerId: string, notebookService: INotebookService): IStandardKernelWithProvider[] {
if (!providerId || !notebookService) {
return [];
}
let standardKernels = notebookService.getStandardKernelsForProvider(providerId);
standardKernels.forEach(kernel => {
Object.assign(<IStandardKernelWithProvider>kernel, {
name: kernel.name,
connectionProviderIds: kernel.connectionProviderIds,
notebookProvider: providerId
});
});
return <IStandardKernelWithProvider[]>(standardKernels);
}
// In the Attach To dropdown, show the database name (if it exists) using the current connection
// Example: myFakeServer (myDatabase)
export function formatServerNameWithDatabaseNameForAttachTo(connectionProfile: ConnectionProfile): string {
if (connectionProfile && connectionProfile.serverName) {
return !connectionProfile.databaseName ? connectionProfile.serverName : connectionProfile.serverName + ' (' + connectionProfile.databaseName + ')';
}
return '';
}
// Extract server name from format used in Attach To: serverName (databaseName)
export function getServerFromFormattedAttachToName(name: string): string {
return name.substring(0, name.lastIndexOf(' (')) ? name.substring(0, name.lastIndexOf(' (')) : name;
}
// Extract database name from format used in Attach To: serverName (databaseName)
export function getDatabaseFromFormattedAttachToName(name: string): string {
return name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) ?
name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) : '';
}
export interface IStandardKernelWithProvider {
readonly name: string;
readonly displayName: string;
readonly connectionProviderIds: string[];
readonly notebookProvider: string;
}
export interface IEndpoint {
serviceName: string;
description: string;
endpoint: string;
protocol: string;
}
export function tryMatchCellMagic(input: string): string {
if (!input) {
return input;
}
let firstLine = input.trimLeft();
let magicRegex = /^%%(\w+)/g;
let match = magicRegex.exec(firstLine);
let magicName = match && match[1];
return magicName;
}
export async function asyncForEach(array: any, callback: any): Promise<any> {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
/**
* Only replace vscode-resource with file when in the same (or a sub) directory
* This matches Jupyter Notebook viewer behavior
*/
export function convertVscodeResourceToFileInSubDirectories(htmlContent: string, cellModel: ICellModel): string {
let htmlContentCopy = htmlContent;
while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
// If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
if (!path.relative(path.dirname(cellModel.notebookModel.notebookUri.fsPath), filePath).includes('..')) {
// ok to change from vscode-resource: to file:
htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
}
htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
}
return htmlContent;
}
export function useInProcMarkdown(configurationService: IConfigurationService): boolean {
return configurationService.getValue('notebook.useInProcMarkdown');
}
export function getClusterEndpoints(serverInfo: ServerInfo): IEndpoint[] | undefined {
let endpoints: RawEndpoint[] = serverInfo.options[clusterEndpointsProperty];
if (!endpoints || endpoints.length === 0) { return []; }
return endpoints.map(e => {
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
let updatedEndpoint: IEndpoint = {
serviceName: e.serviceName,
description: e.description,
endpoint: endpoint,
protocol: e.protocol
};
return updatedEndpoint;
});
}
export type HostAndIp = { host: string, port: string };
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
let authority = URI.parse(endpoint).authority;
let hostAndPortRegex = /^(.*)([,:](\d+))/g;
let match = hostAndPortRegex.exec(authority);
if (match) {
return {
host: match[1],
port: match[3]
};
}
return {
host: authority,
port: undefined
};
}
interface RawEndpoint {
serviceName: string;
description?: string;
endpoint?: string;
protocol?: string;
ipAddress?: string;
port?: number;
}
export interface IEndpoint {
serviceName: string;
description: string;
endpoint: string;
protocol: string;
}