Add notebook extension support for .NET Interactive. (#18334)

* Also updated kernel dropdown to only include SQL aliased kernels when using SQL notebook provider.
This commit is contained in:
Cory Rivera
2022-02-25 11:58:59 -08:00
committed by GitHub
parent 02341088eb
commit ffdefd3b52
41 changed files with 649 additions and 278 deletions

View File

@@ -33,6 +33,8 @@ import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState';
import { IPosition } from 'vs/editor/common/core/position';
import { CellOutputEdit, CellOutputDataEdit } from 'sql/workbench/services/notebook/browser/models/cellEdit';
import { ILogService } from 'vs/platform/log/common/log';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ICellMetadata } from 'sql/workbench/api/common/sqlExtHostTypes';
let modelId = 0;
const ads_execute_command = 'ads_execute_command';
@@ -70,8 +72,9 @@ export class CellModel extends Disposable implements ICellModel {
private _onCellLoaded = new Emitter<string>();
private _loaded: boolean;
private _stdInVisible: boolean;
private _metadata: nb.ICellMetadata;
private _metadata: ICellMetadata;
private _isCollapsed: boolean;
private _onLanguageChanged = new Emitter<string>();
private _onCollapseStateChanged = new Emitter<boolean>();
private _modelContentChangedEvent: IModelContentChangedEvent;
private _isCommandExecutionSettingEnabled: boolean = false;
@@ -95,7 +98,8 @@ export class CellModel extends Disposable implements ICellModel {
@optional(INotebookService) private _notebookService?: INotebookService,
@optional(ICommandService) private _commandService?: ICommandService,
@optional(IConfigurationService) private _configurationService?: IConfigurationService,
@optional(ILogService) private _logService?: ILogService
@optional(ILogService) private _logService?: ILogService,
@optional(IModeService) private _modeService?: IModeService
) {
super();
this.id = `${modelId++}`;
@@ -124,6 +128,10 @@ export class CellModel extends Disposable implements ICellModel {
return other !== undefined && other.id === this.id;
}
public get onLanguageChanged(): Event<string> {
return this._onLanguageChanged.event;
}
public get onCollapseStateChanged(): Event<boolean> {
return this._onCollapseStateChanged.event;
}
@@ -385,6 +393,19 @@ export class CellModel extends Disposable implements ICellModel {
return this._options.notebook.language;
}
public get displayLanguage(): string {
let result: string;
if (this._cellType === CellTypes.Markdown) {
result = 'Markdown';
} else if (this._modeService) {
let language = this._modeService.getLanguageName(this.language);
result = language ?? this.language;
} else {
result = this.language;
}
return result;
}
public get savedConnectionName(): string | undefined {
return this._savedConnectionName;
}
@@ -394,7 +415,10 @@ export class CellModel extends Disposable implements ICellModel {
}
public setOverrideLanguage(newLanguage: string) {
this._language = newLanguage;
if (newLanguage !== this._language) {
this._language = newLanguage;
this._onLanguageChanged.fire(newLanguage);
}
}
public get onExecutionStateChange(): Event<CellExecutionState> {
@@ -616,7 +640,9 @@ export class CellModel extends Disposable implements ICellModel {
code: content,
cellIndex: this.notebookModel.findCellIndex(this),
stop_on_error: true,
notebookUri: this.notebookModel.notebookUri
notebookUri: this.notebookModel.notebookUri,
cellUri: this.cellUri,
language: this.language
}, false);
this.setFuture(future as FutureInternal);
this.fireExecutionStateChanged();
@@ -728,7 +754,7 @@ export class CellModel extends Disposable implements ICellModel {
this._future = future;
future.setReplyHandler({ handle: (msg) => this.handleReply(msg) });
future.setIOPubHandler({ handle: (msg) => this.handleIOPub(msg) });
future.setStdInHandler({ handle: (msg) => this.handleSdtIn(msg) });
future.setStdInHandler({ handle: (msg) => this.handleStdIn(msg) });
}
/**
* Clear outputs can be done as part of the "Clear Outputs" action on a cell or as part of running a cell
@@ -933,7 +959,7 @@ export class CellModel extends Disposable implements ICellModel {
* 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> {
private handleStdIn(msg: nb.IStdinMessage): void | Thenable<void> {
let handler = async () => {
if (!this._stdInHandler) {
// No-op
@@ -995,7 +1021,7 @@ export class CellModel extends Disposable implements ICellModel {
}
this._attachments = cell.attachments;
this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid();
this.setLanguageFromContents(cell);
this.setLanguageFromContents(cell.cell_type, cell.metadata);
this._savedConnectionName = this._metadata.connection_name;
if (cell.outputs) {
for (let output of cell.outputs) {
@@ -1051,13 +1077,16 @@ export class CellModel extends Disposable implements ICellModel {
this.fireOutputsChanged(false);
}
private setLanguageFromContents(cell: nb.ICellContents): void {
if (cell.cell_type === CellTypes.Markdown) {
private setLanguageFromContents(cellType: string, metadata: ICellMetadata): void {
if (cellType === CellTypes.Markdown) {
this._language = 'markdown';
} else if (cell.metadata && cell.metadata.language) {
this._language = cell.metadata.language;
} else if (metadata?.language) {
this._language = metadata.language;
} else if (metadata?.dotnet_interactive?.language) {
this._language = `dotnet-interactive.${metadata.dotnet_interactive.language}`;
} else {
this._language = this._options?.notebook?.language;
}
// else skip, we set default language anyhow
}
private addOutput(output: nb.ICellOutput) {

View File

@@ -99,25 +99,25 @@ export class ClientSession implements IClientSession {
await this._executeManager.sessionManager.ready;
}
if (this._defaultKernel) {
await this.startSessionInstance(this._defaultKernel.name);
await this.startSessionInstance(this._defaultKernel);
}
}
}
private async startSessionInstance(kernelName: string): Promise<void> {
private async startSessionInstance(kernelSpec: nb.IKernelSpec): Promise<void> {
let session: nb.ISession;
try {
// TODO #3164 should use URI instead of path for startNew
session = await this._executeManager.sessionManager.startNew({
path: this.notebookUri.fsPath,
kernelName: kernelName
// TODO add kernel name if saved in the document
kernelName: kernelSpec.name,
kernelSpec: kernelSpec
});
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));
this.options.notificationService.warn(localize('kernelRequiresConnection', "Kernel {0} was not found. The default kernel will be used instead.", kernelSpec.name));
session = await this._executeManager.sessionManager.startNew({
path: this.notebookUri.fsPath,
kernelName: undefined
@@ -128,7 +128,7 @@ export class ClientSession implements IClientSession {
}
}
this._session = session;
await this.runKernelConfigActions(kernelName);
await this.runKernelConfigActions(kernelSpec.name);
this._statusChangedEmitter.fire(session);
}
@@ -278,7 +278,7 @@ export class ClientSession implements IClientSession {
kernel = await this._session.changeKernel(options);
await this.runKernelConfigActions(kernel.name);
} else {
kernel = await this.startSessionInstance(options.name).then(() => this.kernel);
kernel = await this.startSessionInstance(options).then(() => this.kernel);
}
return kernel;
}

View File

@@ -501,6 +501,7 @@ export interface ICellModel {
cellUri: URI;
id: string;
readonly language: string;
readonly displayLanguage: string;
readonly cellGuid: string;
source: string | string[];
cellType: CellType;
@@ -530,6 +531,7 @@ export interface ICellModel {
isCollapsed: boolean;
isParameter: boolean;
isInjectedParameter: boolean;
readonly onLanguageChanged: Event<string>;
readonly onCollapseStateChanged: Event<boolean>;
readonly onParameterStateChanged: Event<boolean>;
readonly onCellModeChanged: Event<boolean>;

View File

@@ -38,6 +38,7 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { AddCellEdit, CellOutputEdit, ConvertCellTypeEdit, DeleteCellEdit, MoveCellEdit, CellOutputDataEdit, SplitCellEdit } from 'sql/workbench/services/notebook/browser/models/cellEdit';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { deepClone } from 'vs/base/common/objects';
import { DotnetInteractiveLabel } from 'sql/workbench/api/common/notebooks/notebookUtils';
/*
* Used to control whether a message in a dialog/wizard is displayed as an error,
@@ -1006,7 +1007,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
}
if (this._capabilitiesService?.providers) {
if (this._capabilitiesService?.providers && this.executeManager.providerId === SQL_NOTEBOOK_PROVIDER) {
let providers = this._capabilitiesService.providers;
for (const server in providers) {
let alias = providers[server].connection.notebookKernelAlias;
@@ -1045,6 +1046,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._defaultKernel = notebookConstants.sqlKernelSpec;
this._providerId = SQL_NOTEBOOK_PROVIDER;
}
if (!this._defaultLanguageInfo?.name) {
// update default language
this._defaultLanguageInfo = {
@@ -1136,12 +1138,22 @@ export class NotebookModel extends Disposable implements INotebookModel {
language = KernelsLanguage.Python;
} else if (language.toLowerCase() === 'c#') {
language = KernelsLanguage.CSharp;
} else if (language.toLowerCase() === 'f#') {
language = KernelsLanguage.FSharp;
}
} else {
language = KernelsLanguage.Python;
}
// Update cell language if it was using the previous default, but skip updating the cell
// if it was using a more specific language.
let oldLanguage = this._language;
this._language = language.toLowerCase();
this._cells?.forEach(cell => {
if (!cell.language || cell.language === oldLanguage) {
cell.setOverrideLanguage(this._language);
}
});
}
public changeKernel(displayName: string): void {
@@ -1336,6 +1348,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
let standardKernel = this._standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName));
if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) {
// Special case .NET Interactive kernel name to handle inconsistencies between notebook providers and jupyter kernel specs
if (this._savedKernelInfo.display_name === DotnetInteractiveLabel) {
this._savedKernelInfo.oldName = this._savedKernelInfo.name;
}
this._savedKernelInfo.name = standardKernel.name;
this._savedKernelInfo.display_name = standardKernel.displayName;
}
@@ -1421,7 +1438,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._savedKernelInfo = {
name: kernel.name,
display_name: spec.display_name,
language: spec.language
language: spec.language,
supportedLanguages: spec.supportedLanguages,
oldName: spec.oldName,
oldDisplayName: spec.oldDisplayName,
oldLanguage: spec.oldLanguage
};
this.clientSession?.configureKernel(this._savedKernelInfo);
} catch (err) {
@@ -1547,7 +1568,29 @@ export class NotebookModel extends Disposable implements INotebookModel {
let metadata = Object.create(null) as nb.INotebookMetadata;
// TODO update language and kernel when these change
metadata.kernelspec = this._savedKernelInfo;
delete metadata.kernelspec?.supportedLanguages;
metadata.language_info = this.languageInfo;
// Undo special casing for .NET Interactive
if (metadata.kernelspec?.oldName) {
metadata.kernelspec.name = metadata.kernelspec.oldName;
delete metadata.kernelspec.oldName;
}
if (metadata.kernelspec?.oldDisplayName) {
metadata.kernelspec.display_name = metadata.kernelspec.oldDisplayName;
delete metadata.kernelspec.oldDisplayName;
}
if (metadata.kernelspec?.oldLanguage) {
metadata.kernelspec.language = metadata.kernelspec.oldLanguage;
delete metadata.kernelspec.oldLanguage;
}
if (metadata.language_info?.oldName) {
metadata.language_info.name = metadata.language_info?.oldName;
delete metadata.language_info?.oldName;
}
metadata.tags = this._tags;
metadata.multi_connection_mode = this._multiConnectionMode ? this._multiConnectionMode : undefined;
if (this.configurationService.getValue(saveConnectionNameConfigName)) {

View File

@@ -59,6 +59,7 @@ export interface IStandardKernelWithProvider {
readonly displayName: string;
readonly connectionProviderIds: string[];
readonly notebookProvider: string;
readonly supportedLanguages: string[];
}
export interface IEndpoint {

View File

@@ -17,7 +17,7 @@ import { NotebookChangeType, CellType } from 'sql/workbench/services/notebook/co
import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
import { Range } from 'vs/editor/common/core/range';
import { IEditorPane } from 'vs/workbench/common/editor';
import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor';
import { INotebookInput } from 'sql/workbench/services/notebook/browser/interface';
import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
@@ -137,6 +137,8 @@ export interface INotebookService {
*/
notifyCellExecutionStarted(): void;
createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise<IEditorInput | undefined>;
openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise<IEditorPane | undefined>;
getUntitledUriPath(originalTitle: string): string;

View File

@@ -248,13 +248,18 @@ export class NotebookService extends Disposable implements INotebookService {
lifecycleService.onWillShutdown(() => this.shutdown());
}
public async openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise<IEditorPane | undefined> {
const uri = URI.revive(resource);
const editorOptions: ITextEditorOptions = {
preserveFocus: options.preserveFocus,
pinned: !options.preview
};
public async createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise<IEditorInput | undefined> {
let uri: URI;
if (resource) {
uri = URI.revive(resource);
} else {
// Need to create a new untitled URI, so find the lowest numbered one that's available
let counter = 1;
do {
uri = URI.from({ scheme: Schemas.untitled, path: `Notebook-${counter}` });
counter++;
} while (this._untitledEditorService.get(uri));
}
let isUntitled: boolean = uri.scheme === Schemas.untitled;
let fileInput: IEditorInput;
@@ -269,6 +274,7 @@ export class NotebookService extends Disposable implements INotebookService {
fileInput = this._editorService.createEditorInput({ forceFile: true, resource: uri, mode: 'notebook' });
}
}
// We only need to get the Notebook language association as such we only need to use ipynb
const inputCreator = languageAssociationRegistry.getAssociationForLanguage(NotebookLanguage.Ipynb);
if (inputCreator) {
@@ -286,7 +292,21 @@ export class NotebookService extends Disposable implements INotebookService {
}
}
}
return await this._editorService.openEditor(fileInput, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position));
if (!fileInput) {
throw new Error(localize('failedToCreateNotebookInput', "Failed to create notebook input for provider '{0}'", options.providerId));
}
return fileInput;
}
public async openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise<IEditorPane | undefined> {
const editorOptions: ITextEditorOptions = {
preserveFocus: options.preserveFocus,
pinned: !options.preview
};
let input = await this.createNotebookInput(options, resource);
return await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position));
}
/**
@@ -321,7 +341,8 @@ export class NotebookService extends Disposable implements INotebookService {
let descriptor = new StandardKernelsDescriptor(notebookConstants.SQL, [{
name: notebookConstants.SQL,
displayName: notebookConstants.SQL,
connectionProviderIds: sqlConnectionTypes
connectionProviderIds: sqlConnectionTypes,
supportedLanguages: [notebookConstants.sqlKernelSpec.language]
}]);
this._providerToStandardKernels.set(notebookConstants.SQL, descriptor);
}
@@ -785,7 +806,12 @@ export class NotebookService extends Disposable implements INotebookService {
notebookRegistry.registerProviderDescription({
provider: serializationProvider.providerId,
fileExtensions: [DEFAULT_NOTEBOOK_FILETYPE],
standardKernels: [{ name: notebookConstants.SQL, displayName: notebookConstants.SQL, connectionProviderIds: [notebookConstants.SQL_CONNECTION_PROVIDER] }]
standardKernels: [{
name: notebookConstants.SQL,
displayName: notebookConstants.SQL,
connectionProviderIds: [notebookConstants.SQL_CONNECTION_PROVIDER],
supportedLanguages: [notebookConstants.sqlKernelSpec.language]
}]
});
}

View File

@@ -55,12 +55,26 @@ export const textRendererFactory: IRenderMime.IRendererFactory = {
mimeTypes: [
'text/plain',
'application/vnd.jupyter.stdout',
'application/vnd.jupyter.stderr'
'application/vnd.jupyter.stderr',
'application/vnd.code.notebook.stdout',
'application/vnd.code.notebook.stderr'
],
defaultRank: 120,
createRenderer: options => new widgets.RenderedText(options)
};
/**
* A mime renderer factory for VS Code Notebook error data.
*/
export const errorRendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: [
'application/vnd.code.notebook.error'
],
defaultRank: 121,
createRenderer: options => new widgets.ErrorText(options)
};
/**
* A placeholder factory for deprecated rendered JavaScript.
*/
@@ -101,6 +115,7 @@ export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFacto
imageRendererFactory,
javaScriptRendererFactory,
textRendererFactory,
errorRendererFactory,
dataResourceRendererFactory,
ipywidgetFactory
];

View File

@@ -319,6 +319,34 @@ export class RenderedText extends RenderedCommon {
}
}
export class ErrorText extends RenderedCommon {
/**
* Construct a new error text widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-ErrorText');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
let err = JSON.parse(String(model.data[this.mimeType]));
let text = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message;
return renderers.renderText({
host: this.node,
source: text
});
}
}
/**
* A widget for displaying deprecated JavaScript output.
*/

View File

@@ -22,5 +22,6 @@ export enum KernelsLanguage {
SparkScala = 'scala',
SparkR = 'sparkr',
PowerShell = 'powershell',
CSharp = 'cs'
CSharp = 'csharp',
FSharp = 'fsharp'
}

View File

@@ -55,6 +55,12 @@ let providerDescriptionType: IJSONSchema = {
items: {
type: 'string'
}
},
supportedLanguages: {
type: 'array',
items: {
type: 'string'
}
}
}
}
@@ -112,7 +118,8 @@ export interface INotebookProviderRegistry {
readonly onNewDescriptionRegistration: Event<{ id: string, registration: ProviderDescriptionRegistration }>;
updateProviderDescriptionLanguages(providerId: string, languages: string[]): void;
updateProviderKernels(providerId: string, kernels: azdata.nb.IStandardKernel[]): void;
updateKernelLanguages(providerId: string, kernelName: string, languages: string[]): void;
registerProviderDescription(provider: ProviderDescriptionRegistration): void;
registerNotebookLanguageMagic(magic: NotebookLanguageMagicRegistration): void;
}
@@ -124,24 +131,33 @@ class NotebookProviderRegistry implements INotebookProviderRegistry {
private _onNewDescriptionRegistration = new Emitter<{ id: string, registration: ProviderDescriptionRegistration }>();
public readonly onNewDescriptionRegistration: Event<{ id: string, registration: ProviderDescriptionRegistration }> = this._onNewDescriptionRegistration.event;
updateProviderDescriptionLanguages(providerId: string, languages: string[]): void {
private readonly providerNotInRegistryError = (providerId: string): string => localize('providerNotInRegistryError', "The specified provider '{0}' is not present in the notebook registry.", providerId);
updateProviderKernels(providerId: string, kernels: azdata.nb.IStandardKernel[]): void {
let registration = this._providerDescriptionRegistration.get(providerId);
if (!registration) {
throw new Error(localize('providerNotInRegistryError', "The specified provider '{0}' is not present in the notebook registry.", providerId));
throw new Error(this.providerNotInRegistryError(providerId));
}
let kernels = languages.map<azdata.nb.IStandardKernel>(language => {
return {
name: language,
displayName: language,
connectionProviderIds: []
};
});
registration.standardKernels = kernels;
// Update provider description with new info
this.registerProviderDescription(registration);
}
updateKernelLanguages(providerId: string, kernelName: string, languages: string[]): void {
let registration = this._providerDescriptionRegistration.get(providerId);
if (!registration) {
throw new Error(this.providerNotInRegistryError(providerId));
}
let kernel = registration.standardKernels?.find(kernel => kernel.name === kernelName);
if (kernel) {
kernel.supportedLanguages = languages;
}
// Update provider description with new info
this.registerProviderDescription(registration);
}
registerProviderDescription(registration: ProviderDescriptionRegistration): void {
this._providerDescriptionRegistration.set(registration.provider, registration);
this._onNewDescriptionRegistration.fire({ id: registration.provider, registration: registration });