move code from parts to contrib (#8319)

This commit is contained in:
Anthony Dresser
2019-11-14 12:23:11 -08:00
committed by GitHub
parent 6438967202
commit 7a2c30e159
619 changed files with 848 additions and 848 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,99 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { IRenderMime } from './renderMimeInterfaces';
import { ReadonlyJSONObject } from '../../common/models/jsonext';
import { IThemeService } from 'vs/platform/theme/common/themeService';
/**
* The default mime model implementation.
*/
export class MimeModel implements IRenderMime.IMimeModel {
/**
* Construct a new mime model.
*/
constructor(options: MimeModel.IOptions = {}) {
this.trusted = !!options.trusted;
this._data = options.data || {};
this._metadata = options.metadata || {};
this._callback = options.callback;
this._themeService = options.themeService;
}
/**
* Whether the model is trusted.
*/
readonly trusted: boolean;
/**
* The data associated with the model.
*/
get data(): ReadonlyJSONObject {
return this._data;
}
/**
* The metadata associated with the model.
*/
get metadata(): ReadonlyJSONObject {
return this._metadata;
}
get themeService(): IThemeService {
return this._themeService;
}
/**
* Set the data associated with the model.
*
* #### Notes
* Depending on the implementation of the mime model,
* this call may or may not have deferred effects,
*/
setData(options: IRenderMime.ISetDataOptions): void {
this._data = options.data || this._data;
this._metadata = options.metadata || this._metadata;
this._callback(options);
}
private _callback: (options: IRenderMime.ISetDataOptions) => void;
private _data: ReadonlyJSONObject;
private _metadata: ReadonlyJSONObject;
private _themeService: IThemeService;
}
/**
* The namespace for MimeModel class statics.
*/
export namespace MimeModel {
/**
* The options used to create a mime model.
*/
export interface IOptions {
/**
* Whether the model is trusted. Defaults to `false`.
*/
trusted?: boolean;
/**
* A callback function for when the data changes.
*/
callback?: (options: IRenderMime.ISetDataOptions) => void;
/**
* The initial mime data.
*/
data?: ReadonlyJSONObject;
/**
* The initial mime metadata.
*/
metadata?: ReadonlyJSONObject;
/**
* Theme service used to react to theme change events
*/
themeService?: IThemeService;
}
}

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/contrib/notebook/browser/models/cell';
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
import { ClientSession } from 'sql/workbench/contrib/notebook/browser/models/clientSession';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class ModelFactory implements IModelFactory {
constructor(private instantiationService: IInstantiationService) {
}
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
return this.instantiationService.createInstance(CellModel, cell, options);
}
public createClientSession(options: IClientSessionOptions): IClientSession {
return new ClientSession(options);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,483 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor';
import { Emitter, Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import * as resources from 'vs/base/common/resources';
import * as azdata from 'azdata';
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/contrib/notebook/browser/models/notebookUtils';
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { Schemas } from 'vs/base/common/network';
import { ITextFileService, ISaveOptions, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IDisposable } from 'vs/base/common/lifecycle';
import { NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
import { Deferred } from 'sql/base/common/promise';
import { NotebookTextFileModel } from 'sql/workbench/contrib/notebook/browser/models/notebookTextFileModel';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
export class NotebookEditorModel extends EditorModel {
private _dirty: boolean;
private _changeEventsHookedUp: boolean = false;
private _notebookTextFileModel: NotebookTextFileModel;
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
private _lastEditFullReplacement: boolean;
constructor(public readonly notebookUri: URI,
private textEditorModel: TextFileEditorModel | UntitledEditorModel | ResourceEditorModel,
@INotebookService private notebookService: INotebookService,
@ITextFileService private textFileService: ITextFileService,
@ITextResourcePropertiesService private textResourcePropertiesService: ITextResourcePropertiesService
) {
super();
let _eol = this.textResourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled }));
this._notebookTextFileModel = new NotebookTextFileModel(_eol);
this._register(this.notebookService.onNotebookEditorAdd(notebook => {
if (notebook.id === this.notebookUri.toString()) {
// Hook to content change events
notebook.modelReady.then((model) => {
if (!this._changeEventsHookedUp) {
this._changeEventsHookedUp = true;
this._register(model.kernelChanged(e => this.updateModel(undefined, NotebookChangeType.KernelChanged)));
this._register(model.contentChanged(e => this.updateModel(e, e.changeType)));
this._register(notebook.model.onActiveCellChanged((cell) => {
if (cell) {
this._notebookTextFileModel.activeCellGuid = cell.cellGuid;
}
}));
}
}, err => undefined);
}
}));
if (this.textEditorModel instanceof UntitledEditorModel) {
this._register(this.textEditorModel.onDidChangeDirty(e => {
let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
this.setDirty(dirty);
}));
} else {
if (this.textEditorModel instanceof TextFileEditorModel) {
this._register(this.textEditorModel.onDidStateChange(change => {
let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
this.setDirty(dirty);
if (change === StateChange.SAVED) {
this.sendNotebookSerializationStateChange();
}
}));
}
}
this._dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
}
public get contentString(): string {
let model = this.textEditorModel.textEditorModel;
return model.getValue();
}
public get lastEditFullReplacement(): boolean {
return this._lastEditFullReplacement;
}
isDirty(): boolean {
return this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty();
}
public setDirty(dirty: boolean): void {
if (this._dirty === dirty) {
return;
}
this._dirty = dirty;
this._onDidChangeDirty.fire();
}
/**
* UntitledEditor uses TextFileService to save data from UntitledEditorInput
* Titled editor uses TextFileEditorModel to save existing notebook
*/
save(options: ISaveOptions): Promise<boolean> {
if (this.textEditorModel instanceof TextFileEditorModel) {
this.textEditorModel.save(options);
return Promise.resolve(true);
}
else {
return this.textFileService.save(this.notebookUri, options);
}
}
public updateModel(contentChange?: NotebookContentChange, type?: NotebookChangeType): void {
this._lastEditFullReplacement = false;
if (contentChange && contentChange.changeType === NotebookChangeType.Saved) {
// We send the saved events out, so ignore. Otherwise we double-count this as a change
// and cause the text to be reapplied
return;
}
if (contentChange && contentChange.changeType === NotebookChangeType.TrustChanged) {
// This is a serializable change (in that we permanently cache trusted state, but
// ironically isn't cached in the JSON contents since trust doesn't persist across machines.
// Request serialization so trusted state is preserved but don't update the model
this.sendNotebookSerializationStateChange();
} else {
let notebookModel = this.getNotebookModel();
let editAppliedSuccessfully = false;
if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) {
if (contentChange && contentChange.cells && contentChange.cells[0]) {
if (type === NotebookChangeType.CellSourceUpdated) {
if (this._notebookTextFileModel.transformAndApplyEditForSourceUpdate(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
} else if (type === NotebookChangeType.CellOutputUpdated) {
if (this._notebookTextFileModel.transformAndApplyEditForOutputUpdate(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
} else if (type === NotebookChangeType.CellOutputCleared) {
if (this._notebookTextFileModel.transformAndApplyEditForClearOutput(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
} else if (type === NotebookChangeType.CellExecuted) {
if (this._notebookTextFileModel.transformAndApplyEditForCellUpdated(contentChange, this.textEditorModel)) {
editAppliedSuccessfully = true;
}
}
}
// If edit was already applied, skip replacing entire text model
if (editAppliedSuccessfully) {
return;
}
this.replaceEntireTextEditorModel(notebookModel, type);
this._lastEditFullReplacement = true;
}
}
}
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType) {
this._notebookTextFileModel.replaceEntireTextEditorModel(notebookModel, type, this.textEditorModel);
}
private sendNotebookSerializationStateChange(): void {
let notebookModel = this.getNotebookModel();
if (notebookModel) {
this.notebookService.serializeNotebookStateChange(this.notebookUri, NotebookChangeType.Saved);
}
}
isModelCreated(): boolean {
return this.getNotebookModel() !== undefined;
}
private getNotebookModel(): INotebookModel {
let editor = this.notebookService.findNotebookEditor(this.notebookUri);
if (editor) {
return editor.model;
}
return undefined;
}
get onDidChangeDirty(): Event<void> {
return this._onDidChangeDirty.event;
}
get editorModel() {
return this.textEditorModel;
}
}
export class NotebookInput extends EditorInput {
public static ID: string = 'workbench.editorinputs.notebookInput';
private _providerId: string;
private _providers: string[];
private _standardKernels: IStandardKernelWithProvider[];
private _connectionProfile: IConnectionProfile;
private _defaultKernel: azdata.nb.IKernelSpec;
public hasBootstrapped = false;
// Holds the HTML content for the editor when the editor discards this input and loads another
private _parentContainer: HTMLElement;
private readonly _layoutChanged: Emitter<void> = this._register(new Emitter<void>());
private _model: NotebookEditorModel;
private _untitledEditorModel: UntitledEditorModel;
private _contentManager: IContentManager;
private _providersLoaded: Promise<void>;
private _dirtyListener: IDisposable;
private _notebookEditorOpenedTimestamp: number;
private _modelResolveInProgress: boolean = false;
private _modelResolved: Deferred<void> = new Deferred<void>();
constructor(private _title: string,
private resource: URI,
private _textInput: UntitledEditorInput,
@ITextModelService private textModelService: ITextModelService,
@IInstantiationService private instantiationService: IInstantiationService,
@INotebookService private notebookService: INotebookService,
@IExtensionService private extensionService: IExtensionService
) {
super();
this.resource = resource;
this._standardKernels = [];
this._providersLoaded = this.assignProviders();
this._notebookEditorOpenedTimestamp = Date.now();
if (this._textInput) {
this.hookDirtyListener(this._textInput.onDidChangeDirty, () => this._onDidChangeDirty.fire());
}
}
public get textInput(): UntitledEditorInput {
return this._textInput;
}
public confirmSave(): Promise<ConfirmResult> {
return this._textInput.confirmSave();
}
public revert(): Promise<boolean> {
return this._textInput.revert();
}
public get notebookUri(): URI {
return this.resource;
}
public get contentManager(): IContentManager {
if (!this._contentManager) {
this._contentManager = this.instantiationService.createInstance(NotebookEditorContentManager, this);
}
return this._contentManager;
}
public getName(): string {
if (!this._title) {
this._title = resources.basenameOrAuthority(this.resource);
}
return this._title;
}
public async getProviderInfo(): Promise<IProviderInfo> {
await this._providersLoaded;
return {
providerId: this._providerId ? this._providerId : DEFAULT_NOTEBOOK_PROVIDER,
providers: this._providers ? this._providers : [DEFAULT_NOTEBOOK_PROVIDER]
};
}
public set connectionProfile(value: IConnectionProfile) {
this._connectionProfile = value;
}
public get connectionProfile(): IConnectionProfile {
return this._connectionProfile;
}
public get standardKernels(): IStandardKernelWithProvider[] {
return this._standardKernels;
}
public save(): Promise<boolean> {
let options: ISaveOptions = { force: false };
return this._model.save(options);
}
public set standardKernels(value: IStandardKernelWithProvider[]) {
value.forEach(kernel => {
this._standardKernels.push({
connectionProviderIds: kernel.connectionProviderIds,
name: kernel.name,
displayName: kernel.displayName,
notebookProvider: kernel.notebookProvider
});
});
}
public get defaultKernel(): azdata.nb.IKernelSpec {
return this._defaultKernel;
}
public set defaultKernel(kernel: azdata.nb.IKernelSpec) {
this._defaultKernel = kernel;
}
get layoutChanged(): Event<void> {
return this._layoutChanged.event;
}
public get editorOpenedTimestamp(): number {
return this._notebookEditorOpenedTimestamp;
}
doChangeLayout(): any {
this._layoutChanged.fire();
}
public getTypeId(): string {
return NotebookInput.ID;
}
getResource(): URI {
return this.resource;
}
public get untitledEditorModel(): UntitledEditorModel {
return this._untitledEditorModel;
}
public set untitledEditorModel(value: UntitledEditorModel) {
this._untitledEditorModel = value;
}
async resolve(): Promise<NotebookEditorModel> {
if (!this._modelResolveInProgress) {
this._modelResolveInProgress = true;
} else {
await this._modelResolved;
return this._model;
}
if (this._model) {
return Promise.resolve(this._model);
} else {
let textOrUntitledEditorModel: UntitledEditorModel | IEditorModel;
if (this.resource.scheme === Schemas.untitled) {
if (this._untitledEditorModel) {
this._untitledEditorModel.textEditorModel.onBeforeAttached();
textOrUntitledEditorModel = this._untitledEditorModel;
} else {
let resolvedInput = await this._textInput.resolve();
resolvedInput.textEditorModel.onBeforeAttached();
textOrUntitledEditorModel = resolvedInput;
}
} else {
const textEditorModelReference = await this.textModelService.createModelReference(this.resource);
textEditorModelReference.object.textEditorModel.onBeforeAttached();
textOrUntitledEditorModel = await textEditorModelReference.object.load();
}
this._model = this._register(this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel));
this.hookDirtyListener(this._model.onDidChangeDirty, () => this._onDidChangeDirty.fire());
this._modelResolved.resolve();
return this._model;
}
}
private hookDirtyListener(dirtyEvent: Event<void>, listener: (e: any) => void): void {
let disposable = dirtyEvent(listener);
if (this._dirtyListener) {
this._dirtyListener.dispose();
} else {
this._register({
dispose: () => {
if (this._dirtyListener) {
this._dirtyListener.dispose();
}
}
});
}
this._dirtyListener = disposable;
}
private async assignProviders(): Promise<void> {
await this.extensionService.whenInstalledExtensionsRegistered();
let providerIds: string[] = getProvidersForFileName(this._title, this.notebookService);
if (providerIds && providerIds.length > 0) {
this._providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0];
this._providers = providerIds;
this._standardKernels = [];
this._providers.forEach(provider => {
let standardKernels = getStandardKernelsForProvider(provider, this.notebookService);
this._standardKernels.push(...standardKernels);
});
}
}
public dispose(): void {
if (this._model && this._model.editorModel && this._model.editorModel.textEditorModel) {
this._model.editorModel.textEditorModel.onBeforeDetached();
}
this._disposeContainer();
super.dispose();
}
private _disposeContainer() {
if (!this._parentContainer) {
return;
}
let parentNode = this._parentContainer.parentNode;
if (parentNode) {
parentNode.removeChild(this._parentContainer);
this._parentContainer = null;
}
}
set container(container: HTMLElement) {
this._disposeContainer();
this._parentContainer = container;
}
get container(): HTMLElement {
return this._parentContainer;
}
/**
* An editor that is dirty will be asked to be saved once it closes.
*/
isDirty(): boolean {
if (this._model) {
return this._model.isDirty();
} else if (this._textInput) {
return this._textInput.isDirty();
}
return false;
}
/**
* Sets active editor with dirty value.
* @param isDirty boolean value to set editor dirty
*/
setDirty(isDirty: boolean): void {
if (this._model) {
this._model.setDirty(isDirty);
}
}
updateModel(): void {
this._model.updateModel();
}
public matches(otherInput: any): boolean {
if (super.matches(otherInput) === true) {
return true;
}
if (otherInput instanceof NotebookInput) {
const otherNotebookEditorInput = <NotebookInput>otherInput;
// Compare by resource
return otherNotebookEditorInput.notebookUri.toString() === this.notebookUri.toString();
}
return false;
}
}
class NotebookEditorContentManager implements IContentManager {
constructor(
private notebookInput: NotebookInput,
@IInstantiationService private readonly instantiationService: IInstantiationService) {
}
async loadContent(): Promise<azdata.nb.INotebookContents> {
let notebookEditorModel = await this.notebookInput.resolve();
let contentManager = this.instantiationService.createInstance(LocalContentManager);
let contents = await contentManager.loadFromContentString(notebookEditorModel.contentString);
return contents;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range, IRange } from 'vs/editor/common/core/range';
import { FindMatch } from 'vs/editor/common/model';
import { NotebookContentChange, INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
import { NotebookChangeType } from 'sql/workbench/contrib/notebook/common/models/contracts';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { repeat } from 'vs/base/common/strings';
export class NotebookTextFileModel {
// save active cell's line/column in editor model for the beginning of the source property
private _sourceBeginRange: Range;
// save active cell's line/column in editor model for the beginning of the output property
private _outputBeginRange: Range;
// save active cell guid
private _activeCellGuid: string;
constructor(private _eol: string) {
}
public get activeCellGuid(): string {
return this._activeCellGuid;
}
public set activeCellGuid(guid: string) {
if (this._activeCellGuid !== guid) {
this._sourceBeginRange = undefined;
this._outputBeginRange = undefined;
this._activeCellGuid = guid;
}
}
public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
// convert the range to leverage offsets in the json
if (contentChange && contentChange.modelContentChangedEvent && areRangePropertiesPopulated(cellGuidRange)) {
contentChange.modelContentChangedEvent.changes.forEach(change => {
// When writing to JSON we need to escape double quotes and backslashes
let textEscapedQuotesAndBackslashes = change.text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
let startLineNumber = change.range.startLineNumber + cellGuidRange.startLineNumber - 1;
let endLineNumber = change.range.endLineNumber + cellGuidRange.startLineNumber - 1;
let startLineText = textEditorModel.textEditorModel.getLineContent(startLineNumber);
let endLineText = textEditorModel.textEditorModel.getLineContent(endLineNumber);
/* This gets the text on the start and end lines of the range where we'll be inserting text. We need to convert the escaped strings to unescaped strings.
Example:
Previous state
EDITOR:
""""
TEXTEDITORMODEL:
' "\"\"\"\""'
Now, user wants to insert text after the 4 double quotes, like so:
EDITOR:
""""sample text
TEXTEDITORMODEL (result):
' "\"\"\"\"sample text"'
Notice that we don't have a 1:1 mapping for characters from the editor to the text editor model, because the double quotes need to be escaped
(the same is true for backslashes).
Therefore, we need to determine (at both the start and end lines) the "real" start and end columns in the text editor model by counting escaped characters.
We do this by doing the following:
- Start with (escaped) text in the text editor model
- Unescape this text
- Get a substring of that text from the column in JSON until the change's startColumn (starting from the first " in the text editor model)
- Escape this substring
- Leverage the substring's length to calculate the "real" start/end columns
*/
let unescapedStartSubstring = startLineText.replace(/\\"/g, '"').replace(/\\\\/g, '\\').substr(cellGuidRange.startColumn, change.range.startColumn - 1);
let unescapedEndSubstring = endLineText.replace(/\\"/g, '"').replace(/\\\\/g, '\\').substr(cellGuidRange.startColumn, change.range.endColumn - 1);
// now we have the portion before the text to be inserted for both the start and end lines
// so next step is to escape " and \
let escapedStartSubstring = unescapedStartSubstring.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
let escapedEndSubstring = unescapedEndSubstring.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
let computedStartColumn = escapedStartSubstring.length + cellGuidRange.startColumn + 1;
let computedEndColumn = escapedEndSubstring.length + cellGuidRange.startColumn + 1;
let convertedRange: IRange = {
startLineNumber: startLineNumber,
endLineNumber: endLineNumber,
startColumn: computedStartColumn,
endColumn: computedEndColumn
};
// Need to subtract one because we're going from 1-based to 0-based
let startSpaces: string = repeat(' ', cellGuidRange.startColumn - 1);
// The text here transforms a string from 'This is a string\n this is another string' to:
// This is a string
// this is another string
textEditorModel.textEditorModel.applyEdits([{
range: new Range(convertedRange.startLineNumber, convertedRange.startColumn, convertedRange.endLineNumber, convertedRange.endColumn),
text: textEscapedQuotesAndBackslashes.split(/[\r\n]+/gm).join('\\n\",'.concat(this._eol).concat(startSpaces).concat('\"'))
}]);
});
return true;
}
return false;
}
public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) {
let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' ');
if (contentChange.cells[0].outputs.length > 1) {
newOutput = ', '.concat(newOutput);
} else {
newOutput = '\n'.concat(newOutput).concat('\n');
}
// Execution count will always be after the end of the outputs in JSON. This is a sanity mechanism.
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
if (!executionCountMatch || !executionCountMatch.range) {
return false;
}
let endOutputsRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
if (endOutputsRange && endOutputsRange.startLineNumber < executionCountMatch.range.startLineNumber) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(endOutputsRange.startLineNumber, endOutputsRange.startColumn, endOutputsRange.startLineNumber, endOutputsRange.startColumn),
text: newOutput
}]);
return true;
}
}
return false;
}
public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
if (executionCountMatch && executionCountMatch.range) {
// Execution count can be between 0 and n characters long
let beginExecutionCountColumn = executionCountMatch.range.endColumn;
let endExecutionCountColumn = beginExecutionCountColumn + 1;
let lineContent = textEditorModel.textEditorModel.getLineContent(executionCountMatch.range.endLineNumber);
while (lineContent[endExecutionCountColumn - 1]) {
endExecutionCountColumn++;
}
if (contentChange.cells[0].executionCount) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(executionCountMatch.range.startLineNumber, beginExecutionCountColumn, executionCountMatch.range.endLineNumber, endExecutionCountColumn),
text: contentChange.cells[0].executionCount.toString()
}]);
} else {
// This is a special case when cells are canceled; there will be no execution count included
return true;
}
} else {
return false;
}
return true;
}
public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean {
if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) {
return false;
}
this.updateOutputBeginRange(textEditorModel, contentChange.cells[0].cellGuid);
let outputEndRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
let outputStartRange = this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
if (outputStartRange && outputEndRange) {
textEditorModel.textEditorModel.applyEdits([{
range: new Range(outputStartRange.startLineNumber, outputStartRange.endColumn, outputEndRange.endLineNumber, outputEndRange.endColumn),
text: ''
}]);
return true;
}
return false;
}
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: BaseTextEditorModel) {
let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' ');
let model = textEditorModel.textEditorModel;
let endLine = model.getLineCount();
let endCol = model.getLineMaxColumn(endLine);
textEditorModel.textEditorModel.applyEdits([{
range: new Range(1, 1, endLine, endCol),
text: content
}]);
}
// Find the beginning of a cell's source in the text editor model
private updateSourceBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void {
if (!cellGuid) {
return;
}
this._sourceBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let sourceBefore = textEditorModel.textEditorModel.findPreviousMatch('"source": [', { lineNumber: cellGuidMatches[0].range.startLineNumber, column: cellGuidMatches[0].range.startColumn }, false, true, undefined, true);
if (!sourceBefore || !sourceBefore.range) {
return;
}
let firstQuoteOfSource = textEditorModel.textEditorModel.findNextMatch('"', { lineNumber: sourceBefore.range.startLineNumber, column: sourceBefore.range.endColumn }, false, true, undefined, true);
this._sourceBeginRange = firstQuoteOfSource.range;
} else {
return;
}
}
// Find the beginning of a cell's outputs in the text editor model
private updateOutputBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void {
if (!cellGuid) {
return undefined;
}
this._outputBeginRange = undefined;
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
if (cellGuidMatches && cellGuidMatches.length > 0) {
let outputsBegin = textEditorModel.textEditorModel.findNextMatch('"outputs": [', { lineNumber: cellGuidMatches[0].range.endLineNumber, column: cellGuidMatches[0].range.endColumn }, false, true, undefined, true);
if (!outputsBegin || !outputsBegin.range) {
return undefined;
}
this._outputBeginRange = outputsBegin.range;
} else {
return undefined;
}
}
// Find the end of a cell's outputs in the text editor model
// This will be used as a starting point for any future outputs
private getEndOfOutputs(textEditorModel: BaseTextEditorModel, cellGuid: string) {
let outputsBegin;
if (this._activeCellGuid === cellGuid) {
outputsBegin = this._outputBeginRange;
}
if (!outputsBegin || !(textEditorModel.textEditorModel.getLineContent(outputsBegin.startLineNumber).trim().indexOf('output') > -1)) {
this.updateOutputBeginRange(textEditorModel, cellGuid);
outputsBegin = this._outputBeginRange;
if (!outputsBegin) {
return undefined;
}
}
let outputsEnd = textEditorModel.textEditorModel.matchBracket({ column: outputsBegin.endColumn - 1, lineNumber: outputsBegin.endLineNumber });
if (!outputsEnd || outputsEnd.length < 2) {
return undefined;
}
// single line output [i.e. no outputs exist for a cell]
if (outputsBegin.endLineNumber === outputsEnd[1].startLineNumber) {
// Adding 1 to startColumn to replace text starting one character after '['
return {
startColumn: outputsEnd[0].startColumn + 1,
startLineNumber: outputsEnd[0].startLineNumber,
endColumn: outputsEnd[0].endColumn,
endLineNumber: outputsEnd[0].endLineNumber
};
} else {
// Last 2 lines in multi-line output will look like the following:
// " }"
// " ],"
if (textEditorModel.textEditorModel.getLineContent(outputsEnd[1].endLineNumber - 1).trim() === '}') {
return {
startColumn: textEditorModel.textEditorModel.getLineFirstNonWhitespaceColumn(outputsEnd[1].endLineNumber - 1) + 1,
startLineNumber: outputsEnd[1].endLineNumber - 1,
endColumn: outputsEnd[1].endColumn - 1,
endLineNumber: outputsEnd[1].endLineNumber
};
}
return undefined;
}
}
// Determine what text needs to be replaced when execution counts are updated
private getExecutionCountRange(textEditorModel: BaseTextEditorModel, cellGuid: string) {
let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid);
if (endOutputRange && endOutputRange.endLineNumber) {
return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true);
}
return undefined;
}
// Find a cell's location, given its cellGuid
// If it doesn't exist (e.g. it's not the active cell), attempt to find it
private getCellNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) {
if (this._activeCellGuid !== guid || !this._sourceBeginRange) {
this.updateSourceBeginRange(textEditorModel, guid);
}
return this._sourceBeginRange;
}
private getOutputNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) {
if (this._activeCellGuid !== guid) {
this.updateOutputBeginRange(textEditorModel, guid);
}
return this._outputBeginRange;
}
}
function areRangePropertiesPopulated(range: Range) {
return range && range.startLineNumber !== 0 && range.startColumn !== 0 && range.endLineNumber !== 0 && range.endColumn !== 0;
}
function findOrSetCellGuidMatch(textEditorModel: BaseTextEditorModel, cellGuid: string): FindMatch[] {
if (!textEditorModel || !cellGuid) {
return undefined;
}
return textEditorModel.textEditorModel.findMatches(cellGuid, false, false, true, undefined, true);
}

View File

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

View File

@@ -0,0 +1,111 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { JSONObject, isPrimitive } from '../../common/models/jsonext';
import { MimeModel } from './mimemodel';
import { nbformat } from '../../common/models/nbformat';
import { nb } from 'azdata';
/**
* A multiline string.
*/
export type MultilineString = string | string[];
/**
* A mime-type keyed dictionary of data.
*/
export interface IMimeBundle extends JSONObject {
[key: string]: MultilineString | JSONObject;
}
/**
* Get the data from a notebook output.
*/
export function getData(output: nb.ICellOutput): JSONObject {
let bundle: IMimeBundle = {};
if (
nbformat.isExecuteResult(output) ||
nbformat.isDisplayData(output) ||
nbformat.isDisplayUpdate(output)
) {
bundle = (output as nbformat.IExecuteResult).data;
} else if (nbformat.isStream(output)) {
if (output.name === 'stderr') {
bundle['application/vnd.jupyter.stderr'] = output.text;
} else {
bundle['application/vnd.jupyter.stdout'] = output.text;
}
} else if (nbformat.isError(output)) {
let traceback = output.traceback ? output.traceback.join('\n') : undefined;
bundle['application/vnd.jupyter.stderr'] = undefined;
if (traceback && traceback !== '') {
bundle['application/vnd.jupyter.stderr'] = traceback;
} else if (output.evalue) {
bundle['application/vnd.jupyter.stderr'] = output.ename && output.ename !== '' ? `${output.ename}: ${output.evalue}` : `${output.evalue}`;
}
}
return convertBundle(bundle);
}
/**
* Get the metadata from an output message.
*/
export function getMetadata(output: nbformat.IOutput): JSONObject {
let value: JSONObject = Object.create(null);
if (nbformat.isExecuteResult(output) || nbformat.isDisplayData(output)) {
for (let key in output.metadata) {
value[key] = extract(output.metadata, key);
}
}
return value;
}
/**
* Get the bundle options given output model options.
*/
export function getBundleOptions(options: IOutputModelOptions): MimeModel.IOptions {
let data = getData(options.value);
let metadata = getMetadata(options.value);
let trusted = !!options.trusted;
return { data, metadata, trusted };
}
/**
* Extract a value from a JSONObject.
*/
export function extract(value: JSONObject, key: string): {} {
let item = value[key];
if (isPrimitive(item)) {
return item;
}
return JSON.parse(JSON.stringify(item));
}
/**
* Convert a mime bundle to mime data.
*/
function convertBundle(bundle: nbformat.IMimeBundle): JSONObject {
let map: JSONObject = Object.create(null);
for (let mimeType in bundle) {
map[mimeType] = extract(bundle, mimeType);
}
return map;
}
/**
* The options used to create a notebook output model.
*/
export interface IOutputModelOptions {
/**
* The raw output value.
*/
value: nbformat.IOutput;
/**
* Whether the output is trusted. The default is false.
*/
trusted?: boolean;
}

View File

@@ -0,0 +1,220 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { ReadonlyJSONObject } from '../../common/models/jsonext';
import { IThemeService } from 'vs/platform/theme/common/themeService';
/**
* A namespace for rendermime associated interfaces.
*/
export namespace IRenderMime {
/**
* A model for mime data.
*/
export interface IMimeModel {
/**
* Whether the data in the model is trusted.
*/
readonly trusted: boolean;
/**
* The data associated with the model.
*/
readonly data: ReadonlyJSONObject;
/**
* The metadata associated with the model.
*/
readonly metadata: ReadonlyJSONObject;
/**
* Set the data associated with the model.
*
* #### Notes
* Calling this function may trigger an asynchronous operation
* that could cause the renderer to be rendered with a new model
* containing the new data.
*/
setData(options: ISetDataOptions): void;
/**
* Theme service used to react to theme change events
*/
readonly themeService: IThemeService;
}
/**
* The options used to update a mime model.
*/
export interface ISetDataOptions {
/**
* The new data object.
*/
data?: ReadonlyJSONObject;
/**
* The new metadata object.
*/
metadata?: ReadonlyJSONObject;
}
/**
* A widget which displays the contents of a mime model.
*/
export interface IRenderer {
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*
* #### Notes
* This method may be called multiple times during the lifetime
* of the widget to update it if and when new data is available.
*/
renderModel(model: IRenderMime.IMimeModel): Promise<void>;
/**
* Node to be updated by the renderer
*/
node: HTMLElement;
}
/**
* The interface for a renderer factory.
*/
export interface IRendererFactory {
/**
* Whether the factory is a "safe" factory.
*
* #### Notes
* A "safe" factory produces renderer widgets which can render
* untrusted model data in a usable way. *All* renderers must
* handle untrusted data safely, but some may simply failover
* with a "Run cell to view output" message. A "safe" renderer
* is an indication that its sanitized output will be useful.
*/
readonly safe: boolean;
/**
* The mime types handled by this factory.
*/
readonly mimeTypes: ReadonlyArray<string>;
/**
* The default rank of the factory. If not given, defaults to 100.
*/
readonly defaultRank?: number;
/**
* Create a renderer which displays the mime data.
*
* @param options - The options used to render the data.
*/
createRenderer(options: IRendererOptions): IRenderer;
}
/**
* The options used to create a renderer.
*/
export interface IRendererOptions {
/**
* The preferred mimeType to render.
*/
mimeType: string;
/**
* The html sanitizer.
*/
sanitizer: ISanitizer;
/**
* An optional url resolver.
*/
resolver?: IResolver | null;
/**
* An optional link handler.
*/
linkHandler?: ILinkHandler | null;
/**
* The LaTeX typesetter.
*/
latexTypesetter?: ILatexTypesetter | null;
}
/**
* An object that handles html sanitization.
*/
export interface ISanitizer {
/**
* Sanitize an HTML string.
*/
sanitize(dirty: string): string;
}
/**
* An object that handles links on a node.
*/
export interface ILinkHandler {
/**
* Add the link handler to the node.
*
* @param node: the node for which to handle the link.
*
* @param path: the path to open when the link is clicked.
*
* @param id: an optional element id to scroll to when the path is opened.
*/
handleLink(node: HTMLElement, path: string, id?: string): void;
}
/**
* An object that resolves relative URLs.
*/
export interface IResolver {
/**
* Resolve a relative url to a correct server path.
*/
resolveUrl(url: string): Promise<string>;
/**
* Get the download url of a given absolute server path.
*/
getDownloadUrl(path: string): Promise<string>;
/**
* Whether the URL should be handled by the resolver
* or not.
*
* #### Notes
* This is similar to the `isLocal` check in `URLExt`,
* but can also perform additional checks on whether the
* resolver should handle a given URL.
*/
isLocal?: (url: string) => boolean;
}
/**
* The interface for a LaTeX typesetter.
*/
export interface ILatexTypesetter {
/**
* Typeset a DOM element.
*
* @param element - the DOM element to typeset. The typesetting may
* happen synchronously or asynchronously.
*
* #### Notes
* The application-wide rendermime object has a settable
* `latexTypesetter` property which is used wherever LaTeX
* typesetting is required. Extensions wishing to provide their
* own typesetter may replace that on the global `lab.rendermime`.
*/
typeset(element: HTMLElement): void;
}
}