diff --git a/src/sql/workbench/api/common/notebooks/notebookUtils.ts b/src/sql/workbench/api/common/notebooks/notebookUtils.ts index c9786c46f9..9f6cfd2dd7 100644 --- a/src/sql/workbench/api/common/notebooks/notebookUtils.ts +++ b/src/sql/workbench/api/common/notebooks/notebookUtils.ts @@ -12,10 +12,9 @@ import { CellTypes, MimeTypes, OutputTypes } from 'sql/workbench/services/notebo import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; import { NotebookCellKind } from 'vs/workbench/api/common/extHostTypes'; -export const DotnetInteractiveJupyterLanguagePrefix = '.net-'; -export const DotnetInteractiveLanguagePrefix = 'dotnet-interactive.'; -export const DotnetInteractiveJupyterLabelPrefix = '.NET ('; -export const DotnetInteractiveLabel = '.NET Interactive'; +const DotnetInteractiveJupyterKernelPrefix = '.net-'; +const DotnetInteractiveLanguagePrefix = 'dotnet-interactive.'; +export const DotnetInteractiveDisplayName = '.NET Interactive'; export function convertToVSCodeNotebookCell(cellKind: azdata.nb.CellType, cellIndex: number, cellUri: URI, docUri: URI, cellLanguage: string, cellSource?: string | string[]): vscode.NotebookCell { return { @@ -139,3 +138,87 @@ export function convertToVSCodeNotebookData(notebook: azdata.nb.INotebookContent }; return result; } + +// #region .NET Interactive Kernel Metadata Conversion + +/* +Since ADS relies on notebook kernelSpecs for provider metadata in a lot of places, we have to convert +a .NET Interactive notebook's Jupyter kernelSpec to an internal representation so that it matches up with +the contributed .NET Interactive notebook provider from the Jupyter extension. When saving a notebook, we +then need to restore the original kernelSpec state so that it will work with other notebook apps like +VS Code. VS Code does something similar by shifting a Jupyter notebook's original metadata over to a new +"custom" field, which is then shifted back when saving the notebook. + +This is an example of an internal kernel representation we use to get compatibility working (C#, in this case): +kernelSpec: { + name: 'jupyter-notebook', // Matches the name of the notebook provider from the Jupyter extension + language: 'dotnet-interactive.csharp', // Matches the contributed languages from the .NET Interactive extension + display_name: '.NET Interactive' // The kernel name we need to show in our dropdown to match VS Code's kernel dropdown +} + +This is how that C# kernel spec would need to be saved to work in VS Code: +kernelSpec: { + name: '.net-csharp', + language: 'C#', + display_name: '.NET (C#)' +} +*/ + +/** + * Stores equivalent external kernel metadata in a newly created .NET Interactive notebook, which is used as the default metadata when saving the notebook. This is so that ADS notebooks are still usable in other apps. + * @param kernelSpec The notebook kernel metadata to be modified. + */ +export function addExternalInteractiveKernelMetadata(kernelSpec: azdata.nb.IKernelSpec): void { + if (kernelSpec.name === 'jupyter-notebook' && kernelSpec.display_name === DotnetInteractiveDisplayName && kernelSpec.language) { + let language = kernelSpec.language.replace(DotnetInteractiveLanguagePrefix, ''); + let displayLanguage: string; + switch (language) { + case 'csharp': + displayLanguage = 'C#'; + break; + case 'fsharp': + displayLanguage = 'F#'; + break; + case 'pwsh': + displayLanguage = 'PowerShell'; + break; + default: + displayLanguage = language; + } + if (!kernelSpec.oldName) { + kernelSpec.oldName = `${DotnetInteractiveJupyterKernelPrefix}${language}`; + } + if (!kernelSpec.oldDisplayName) { + kernelSpec.oldDisplayName = `.NET (${displayLanguage})`; + } + if (!kernelSpec.oldLanguage) { + kernelSpec.oldLanguage = displayLanguage; + } + } +} + +/** + * Converts a .NET Interactive notebook's metadata to an internal representation needed for VS Code notebook compatibility. This metadata is then restored when saving the notebook. + * @param metadata The notebook metadata to be modified. + */ +export function convertToInternalInteractiveKernelMetadata(metadata: azdata.nb.INotebookMetadata | undefined): void { + if (metadata?.kernelspec?.name?.startsWith(DotnetInteractiveJupyterKernelPrefix)) { + metadata.kernelspec.oldDisplayName = metadata.kernelspec.display_name; + metadata.kernelspec.display_name = DotnetInteractiveDisplayName; + + let kernelName = metadata.kernelspec.name; + let baseLanguageName = kernelName.replace(DotnetInteractiveJupyterKernelPrefix, ''); + if (baseLanguageName === 'powershell') { + baseLanguageName = 'pwsh'; + } + let languageName = `${DotnetInteractiveLanguagePrefix}${baseLanguageName}`; + + metadata.kernelspec.oldLanguage = metadata.kernelspec.language; + metadata.kernelspec.language = languageName; + + metadata.language_info.oldName = metadata.language_info.name; + metadata.language_info.name = languageName; + } +} + +// #endregion diff --git a/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts b/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts index a103e3cbf9..7f755ec8ec 100644 --- a/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts +++ b/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import type * as azdata from 'azdata'; import { ADSNotebookController } from 'sql/workbench/api/common/notebooks/adsNotebookController'; import * as nls from 'vs/nls'; -import { convertToVSCodeNotebookCell } from 'sql/workbench/api/common/notebooks/notebookUtils'; +import { addExternalInteractiveKernelMetadata, convertToVSCodeNotebookCell } from 'sql/workbench/api/common/notebooks/notebookUtils'; import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { VSCodeNotebookDocument } from 'sql/workbench/api/common/notebooks/vscodeNotebookDocument'; import { URI } from 'vs/base/common/uri'; @@ -86,6 +86,9 @@ class VSCodeKernel implements azdata.nb.IKernel { this._kernelSpec.supportedLanguages = this._controller.supportedLanguages; } + // Store external kernel names for .NET Interactive kernels for when notebook gets saved, so that notebook is usable outside of ADS + addExternalInteractiveKernelMetadata(this._kernelSpec); + this._name = this._kernelSpec.name; this._info = { protocol_version: '', diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index 7bfdc24b00..dec4733d14 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -40,7 +40,7 @@ import { LocalContentManager } from 'sql/workbench/services/notebook/common/loca import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as LanguageAssociationExtensions, ILanguageAssociationRegistry } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; import { NotebookLanguage } from 'sql/workbench/common/constants'; -import { DotnetInteractiveLabel, DotnetInteractiveJupyterLabelPrefix, DotnetInteractiveJupyterLanguagePrefix, DotnetInteractiveLanguagePrefix } from 'sql/workbench/api/common/notebooks/notebookUtils'; +import { convertToInternalInteractiveKernelMetadata } from 'sql/workbench/api/common/notebooks/notebookUtils'; export type ModeViewSaveHandler = (handle: number) => Thenable; const languageAssociationRegistry = Registry.as(LanguageAssociationExtensions.LanguageAssociations); @@ -561,23 +561,8 @@ export class NotebookEditorContentLoader implements IContentLoader { } // Special case .NET Interactive kernel spec to handle inconsistencies between notebook providers and jupyter kernel specs - if (notebookContents.metadata?.kernelspec?.display_name?.startsWith(DotnetInteractiveJupyterLabelPrefix)) { - notebookContents.metadata.kernelspec.oldDisplayName = notebookContents.metadata.kernelspec.display_name; - notebookContents.metadata.kernelspec.display_name = DotnetInteractiveLabel; + convertToInternalInteractiveKernelMetadata(notebookContents.metadata); - let kernelName = notebookContents.metadata.kernelspec.name; - let baseLanguageName = kernelName.replace(DotnetInteractiveJupyterLanguagePrefix, ''); - if (baseLanguageName === 'powershell') { - baseLanguageName = 'pwsh'; - } - let languageName = `${DotnetInteractiveLanguagePrefix}${baseLanguageName}`; - - notebookContents.metadata.kernelspec.oldLanguage = notebookContents.metadata.kernelspec.language; - notebookContents.metadata.kernelspec.language = languageName; - - notebookContents.metadata.language_info.oldName = notebookContents.metadata.language_info.name; - notebookContents.metadata.language_info.name = languageName; - } return notebookContents; } } diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index c72bf28daa..67ae36e435 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -38,7 +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'; +import { DotnetInteractiveDisplayName } from 'sql/workbench/api/common/notebooks/notebookUtils'; import { IPYKERNEL_DISPLAY_NAME } from 'sql/workbench/common/constants'; /* @@ -1352,7 +1352,7 @@ export class NotebookModel extends Disposable implements INotebookModel { if (standardKernel) { if (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) { + if (this._savedKernelInfo.display_name === DotnetInteractiveDisplayName) { this._savedKernelInfo.oldName = this._savedKernelInfo.name; } diff --git a/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts b/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts index be5767fe76..f84c1e8744 100644 --- a/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts +++ b/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts @@ -12,7 +12,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import * as assert from 'assert'; import { OutputTypes } from 'sql/workbench/services/notebook/common/contracts'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; -import { convertToVSCodeNotebookCell, convertToVSCodeCellOutput, convertToADSCellOutput } from 'sql/workbench/api/common/notebooks/notebookUtils'; +import { convertToVSCodeNotebookCell, convertToVSCodeCellOutput, convertToADSCellOutput, convertToInternalInteractiveKernelMetadata, addExternalInteractiveKernelMetadata } from 'sql/workbench/api/common/notebooks/notebookUtils'; import { VSCodeNotebookDocument } from 'sql/workbench/api/common/notebooks/vscodeNotebookDocument'; import { URI } from 'vs/base/common/uri'; import { VSCodeNotebookEditor } from 'sql/workbench/api/common/notebooks/vscodeNotebookEditor'; @@ -484,5 +484,119 @@ suite('Notebook Serializer', () => { }); }); -suite('Notebook Controller', () => { +suite('.NET Interactive Kernel Metadata Conversion', async () => { + test('Convert to internal kernel metadata', async () => { + let originalMetadata: azdata.nb.INotebookMetadata = { + kernelspec: { + name: '.net-csharp', + display_name: '.NET (C#)', + language: 'C#' + }, + language_info: { + name: 'C#' + } + }; + let expectedCovertedMetadata: azdata.nb.INotebookMetadata = { + kernelspec: { + name: '.net-csharp', + display_name: '.NET Interactive', + language: 'dotnet-interactive.csharp', + oldDisplayName: '.NET (C#)', + oldLanguage: 'C#' + }, + language_info: { + name: 'dotnet-interactive.csharp', + oldName: 'C#' + } + }; + + convertToInternalInteractiveKernelMetadata(originalMetadata); + assert.deepStrictEqual(originalMetadata, expectedCovertedMetadata); + }); + + test('Do not convert to internal metadata for non-Interactive kernels', async () => { + let originalMetadata: azdata.nb.INotebookMetadata = { + kernelspec: { + name: 'not-interactive', + display_name: '.NET (C#)', + language: 'C#' + }, + language_info: { + name: 'C#' + } + }; + let expectedCovertedMetadata: azdata.nb.INotebookMetadata = { + kernelspec: { + name: 'not-interactive', + display_name: '.NET (C#)', + language: 'C#' + }, + language_info: { + name: 'C#' + } + }; + + convertToInternalInteractiveKernelMetadata(originalMetadata); + assert.deepStrictEqual(originalMetadata, expectedCovertedMetadata); + }); + + test('Add external kernel metadata', async () => { + let originalKernelSpec: azdata.nb.IKernelSpec = { + name: 'jupyter-notebook', + display_name: '.NET Interactive', + language: 'dotnet-interactive.csharp' + }; + let expectedCovertedKernel: azdata.nb.IKernelSpec = { + name: 'jupyter-notebook', + display_name: '.NET Interactive', + language: 'dotnet-interactive.csharp', + oldName: '.net-csharp', + oldDisplayName: '.NET (C#)', + oldLanguage: 'C#' + }; + addExternalInteractiveKernelMetadata(originalKernelSpec); + assert.deepStrictEqual(originalKernelSpec, expectedCovertedKernel); + }); + + test('Do not add external metadata to non-Interactive kernels', async () => { + // Different kernel name + let originalKernelSpec: azdata.nb.IKernelSpec = { + name: 'not-interactive', + display_name: '.NET Interactive', + language: 'dotnet-interactive.csharp' + }; + let expectedCovertedKernel: azdata.nb.IKernelSpec = { + name: 'not-interactive', + display_name: '.NET Interactive', + language: 'dotnet-interactive.csharp' + }; + addExternalInteractiveKernelMetadata(originalKernelSpec); + assert.deepStrictEqual(originalKernelSpec, expectedCovertedKernel); + + // Different display name + originalKernelSpec = { + name: 'jupyter-notebook', + display_name: 'Not An Interactive Kernel', + language: 'dotnet-interactive.csharp' + }; + expectedCovertedKernel = { + name: 'jupyter-notebook', + display_name: 'Not An Interactive Kernel', + language: 'dotnet-interactive.csharp' + }; + addExternalInteractiveKernelMetadata(originalKernelSpec); + assert.deepStrictEqual(originalKernelSpec, expectedCovertedKernel); + + // No language provided + originalKernelSpec = { + name: 'jupyter-notebook', + display_name: '.NET Interactive' + }; + expectedCovertedKernel = { + name: 'jupyter-notebook', + display_name: '.NET Interactive' + }; + addExternalInteractiveKernelMetadata(originalKernelSpec); + assert.deepStrictEqual(originalKernelSpec, expectedCovertedKernel); + }); });