diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index 436c909f7a..5881154c14 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -703,7 +703,8 @@ "connectionProviderIds": [ "MSSQL" ], - "blockedOnSAW": true + "blockedOnSAW": true, + "supportedLanguages": ["python"] }, { "name": "sparkkernel", @@ -711,6 +712,7 @@ "connectionProviderIds": [ "MSSQL" ], + "supportedLanguages": ["scala"], "blockedOnSAW": true }, { @@ -719,17 +721,20 @@ "connectionProviderIds": [ "MSSQL" ], + "supportedLanguages": ["r"], "blockedOnSAW": true }, { "name": "python3", "displayName": "Python 3", - "connectionProviderIds": [] + "connectionProviderIds": [], + "supportedLanguages": ["python"] }, { "name": "powershell", "displayName": "PowerShell", - "connectionProviderIds": [] + "connectionProviderIds": [], + "supportedLanguages": ["powershell"] } ] } diff --git a/extensions/notebook/src/jupyter/jupyterKernel.ts b/extensions/notebook/src/jupyter/jupyterKernel.ts index 0b8c111b44..5d9f2d8bcb 100644 --- a/extensions/notebook/src/jupyter/jupyterKernel.ts +++ b/extensions/notebook/src/jupyter/jupyterKernel.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { nb } from 'azdata'; import { Kernel, KernelMessage } from '@jupyterlab/services'; @@ -90,7 +91,7 @@ export class JupyterKernel implements nb.IKernel { requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture { content.code = Array.isArray(content.code) ? content.code.join('') : content.code; content.code = content.code.replace(/\r+\n/gm, '\n'); // Remove \r (if it exists) from newlines - let futureImpl = this.kernelImpl.requestExecute(content as KernelMessage.IExecuteRequest & { cellIndex: number }, disposeOnDone); + let futureImpl = this.kernelImpl.requestExecute(content as KernelMessage.IExecuteRequest & { notebookUri: vscode.Uri, cellUri: vscode.Uri, language: string, cellIndex: number }, disposeOnDone); return new JupyterFuture(futureImpl); } diff --git a/extensions/notebook/src/test/model/kernel.test.ts b/extensions/notebook/src/test/model/kernel.test.ts index 2a4648a13d..98182d5f14 100644 --- a/extensions/notebook/src/test/model/kernel.test.ts +++ b/extensions/notebook/src/test/model/kernel.test.ts @@ -98,7 +98,10 @@ describe('Jupyter Session', function (): void { // When I request execute let future = kernel.requestExecute({ code: code, - cellIndex: 0 + cellIndex: 0, + cellUri: undefined, + notebookUri: undefined, + language: '' }, true); // Then expect wrapper to be returned diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index f8d44692b9..0877cf53db 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -5051,20 +5051,11 @@ declare module 'azdata' { } export interface INotebookMetadata { - kernelspec?: IKernelInfo | IKernelSpec | undefined; + kernelspec?: IKernelSpec | undefined; language_info?: ILanguageInfo | undefined; tags?: string[] | undefined; } - /** - * @deprecated Use IKernelSpec instead - */ - export interface IKernelInfo { - name: string; - language?: string | undefined; - display_name?: string | undefined; - } - export interface ILanguageInfo { name: string; version?: string | undefined; diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 3bbbe56c9a..f19729c682 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -30,7 +30,44 @@ declare module 'azdata' { setTrusted(state: boolean): void; } + export interface ISessionOptions { + /** + * The spec for the kernel being used to create this session. + */ + kernelSpec?: IKernelSpec; + } + + export interface IKernelSpec { + /** + * The list of languages that are supported for this kernel. + */ + supportedLanguages?: string[]; + /** + * The original name for this kernel. + */ + oldName?: string; + /** + * The original display name for this kernel. + */ + oldDisplayName?: string; + /** + * The original language name for this kernel. + */ + oldLanguage?: string; + } + + export interface ILanguageInfo { + /** + * The original name for this language. + */ + oldName?: string; + } + export interface IStandardKernel { + /** + * The list of languages that are supported for this kernel. + */ + supportedLanguages: string[]; readonly blockedOnSAW?: boolean; } @@ -59,7 +96,15 @@ declare module 'azdata' { /** * URI of the notebook document that is sending this execute request. */ - notebookUri?: vscode.Uri; + notebookUri: vscode.Uri; + /** + * URI of the notebook cell that is sending this execute request. + */ + cellUri: vscode.Uri; + /** + * The language of the notebook document that is executing this request. + */ + language: string; /** * The index of the cell which the code being executed is from. */ diff --git a/src/sql/base/common/locConstants.ts b/src/sql/base/common/locConstants.ts index c5fc5884ae..52de8514f5 100644 --- a/src/sql/base/common/locConstants.ts +++ b/src/sql/base/common/locConstants.ts @@ -48,5 +48,7 @@ export const desktopContributionMiinstallVsix = localize({ key: 'miinstallVsix', export const workspaceTrustDescription = localize('workspace.trust.description', "Controls whether or not workspace trust is enabled within Azure Data Studio."); export function workspaceTrustEmptyWindowDescription(settingName: string): string { return localize('workspace.trust.emptyWindow.description', "Controls whether or not the empty window is trusted by default within Azure Data Studio. When used with `#{0}#`, you can enable the full functionality of Azure Data Studio without prompting in an empty window.", settingName); } export const functionalityNotSupportedError = localize('vscodeFunctionalityNotSupportedError', "This VS Code functionality is not supported in Azure Data Studio."); -export const invalidArgumentsError = localize('vscodeInvalidArgumentsError', "Invalid arguments"); +export const invalidArgumentsError = localize('vscodeInvalidArgumentsError', "Invalid arguments."); +export const docCreationFailedError = localize('vscodeDocCreationFailedError', "Failed to create notebook document."); export const cellToolbarCompatibilityMessage = localize('notebook.cellToolbarLocation.compatibilityDescription', "Where the cell toolbar should be shown, or whether it should be hidden. Note: This setting is only enabled for extension compatibility purposes, and so does not affect anything."); +export const docNotFoundForUriError = localize('docNotFoundForUriError', 'Could not open a notebook document for the specified URI.'); diff --git a/src/sql/workbench/api/browser/mainThreadNotebook.ts b/src/sql/workbench/api/browser/mainThreadNotebook.ts index f8a4e4ce4c..537c9eafe7 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebook.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebook.ts @@ -102,8 +102,12 @@ export class MainThreadNotebook extends Disposable implements MainThreadNotebook } } - public $updateProviderDescriptionLanguages(providerId: string, languages: string[]): void { - notebookRegistry.updateProviderDescriptionLanguages(providerId, languages); + public $updateProviderKernels(providerId: string, languages: azdata.nb.IStandardKernel[]): void { + notebookRegistry.updateProviderKernels(providerId, languages); + } + + public $updateKernelLanguages(providerId: string, kernelName: string, languages: string[]): void { + notebookRegistry.updateKernelLanguages(providerId, kernelName, languages); } //#endregion } diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 95a8630a4d..5ba39e2302 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -27,8 +27,6 @@ import { localize } from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { NewNotebookAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; class MainThreadNotebookEditor extends Disposable { private _contentChangedEmitter = new Emitter(); @@ -322,8 +320,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements @IInstantiationService private _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @IFileService private readonly _fileService: IFileService, - @ITextFileService private readonly _textFileService: ITextFileService, - @ICommandService private readonly _commandService: ICommandService + @ITextFileService private readonly _textFileService: ITextFileService ) { super(); if (extHostContext) { @@ -345,6 +342,11 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements } } + async $tryCreateNotebookDocument(options: INotebookShowOptions): Promise { + let input = await this._notebookService.createNotebookInput(options); + return input.resource; + } + $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise { return Promise.resolve(this.doOpenEditor(resource, options)); } @@ -712,12 +714,4 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements } }); } - - $createNotebookDocument(providerId: string, contents: azdata.nb.INotebookContents): Promise { - return this._commandService.executeCommand(NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID, { - providerId: providerId, - initialContent: contents, - initialDirtyState: false - }); - } } diff --git a/src/sql/workbench/api/common/extHostNotebook.ts b/src/sql/workbench/api/common/extHostNotebook.ts index 3f7dc4e9f7..5bd34380c9 100644 --- a/src/sql/workbench/api/common/extHostNotebook.ts +++ b/src/sql/workbench/api/common/extHostNotebook.ts @@ -35,13 +35,9 @@ export class ExtHostNotebook implements ExtHostNotebookShape { //#region APIs called by main thread async $getSerializationManagerDetails(providerHandle: number, notebookUri: UriComponents): Promise { let uri = URI.revive(notebookUri); - let uriString = uri.toString(); - let adapter = this.findSerializationManagerForUri(uriString); - if (!adapter) { - adapter = await this._withSerializationProvider(providerHandle, (provider) => { - return this.getOrCreateSerializationManager(provider, uri); - }); - } + let adapter = await this._withSerializationProvider(providerHandle, (provider) => { + return this.getOrCreateSerializationManager(provider, uri); + }); return { handle: adapter.handle, @@ -50,13 +46,9 @@ export class ExtHostNotebook implements ExtHostNotebookShape { } async $getExecuteManagerDetails(providerHandle: number, notebookUri: UriComponents): Promise { let uri = URI.revive(notebookUri); - let uriString = uri.toString(); - let adapter = this.findExecuteManagerForUri(uriString); - if (!adapter) { - adapter = await this._withExecuteProvider(providerHandle, (provider) => { - return this.getOrCreateExecuteManager(provider, uri); - }); - } + let adapter = await this._withExecuteProvider(providerHandle, (provider) => { + return this.getOrCreateExecuteManager(provider, uri); + }); return { handle: adapter.handle, @@ -66,11 +58,10 @@ export class ExtHostNotebook implements ExtHostNotebookShape { $handleNotebookClosed(notebookUri: UriComponents): void { let uri = URI.revive(notebookUri); let uriString = uri.toString(); - let manager = this.findExecuteManagerForUri(uriString); - if (manager) { + this.findExecuteManagersForUri(uriString).forEach(manager => { manager.provider.handleNotebookClosed(uri); this._adapters.delete(manager.handle); - } + }); } $doStartServer(managerHandle: number, kernelSpec: azdata.nb.IKernelSpec): Thenable { @@ -264,8 +255,16 @@ export class ExtHostNotebook implements ExtHostNotebookShape { } createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable, rendererScripts?: vscode.NotebookRendererScript[]): vscode.NotebookController { - let addLanguagesHandler = (id, languages) => this._proxy.$updateProviderDescriptionLanguages(id, languages); - let controller = new ADSNotebookController(extension, id, viewType, label, addLanguagesHandler, this._extHostNotebookDocumentsAndEditors, handler, extension.enableProposedApi ? rendererScripts : undefined); + let languagesHandler = (languages: string[]) => this._proxy.$updateKernelLanguages(viewType, viewType, languages); + let controller = new ADSNotebookController(extension, id, viewType, label, this._extHostNotebookDocumentsAndEditors, languagesHandler, handler, extension.enableProposedApi ? rendererScripts : undefined); + let newKernel: azdata.nb.IStandardKernel = { + name: viewType, + displayName: controller.label, + connectionProviderIds: [], + supportedLanguages: [] // These will get set later from the controller + }; + this._proxy.$updateProviderKernels(viewType, [newKernel]); + let executeProvider = new VSCodeExecuteProvider(controller); this.registerExecuteProvider(executeProvider); return controller; @@ -283,37 +282,29 @@ export class ExtHostNotebook implements ExtHostNotebookShape { return matchingAdapters; } - private findSerializationManagerForUri(uriString: string): SerializationManagerAdapter { - for (let manager of this.getAdapters(SerializationManagerAdapter)) { - if (manager.uriString === uriString) { - return manager; - } - } - return undefined; - } - - private findExecuteManagerForUri(uriString: string): ExecuteManagerAdapter { - for (let manager of this.getAdapters(ExecuteManagerAdapter)) { - if (manager.uriString === uriString) { - return manager; - } - } - return undefined; + private findExecuteManagersForUri(uriString: string): ExecuteManagerAdapter[] { + return this.getAdapters(ExecuteManagerAdapter).filter(adapter => adapter.uriString === uriString); } private async getOrCreateSerializationManager(provider: azdata.nb.NotebookSerializationProvider, notebookUri: URI): Promise { - let manager = await provider.getSerializationManager(notebookUri); let uriString = notebookUri.toString(); - let adapter = new SerializationManagerAdapter(provider, manager, uriString); - adapter.handle = this._addNewAdapter(adapter); + let adapter = this.getAdapters(SerializationManagerAdapter).find(a => a.uriString === uriString && a.provider.providerId === provider.providerId); + if (!adapter) { + let manager = await provider.getSerializationManager(notebookUri); + adapter = new SerializationManagerAdapter(provider, manager, uriString); + adapter.handle = this._addNewAdapter(adapter); + } return adapter; } private async getOrCreateExecuteManager(provider: azdata.nb.NotebookExecuteProvider, notebookUri: URI): Promise { - let manager = await provider.getExecuteManager(notebookUri); let uriString = notebookUri.toString(); - let adapter = new ExecuteManagerAdapter(provider, manager, uriString); - adapter.handle = this._addNewAdapter(adapter); + let adapter = this.getAdapters(ExecuteManagerAdapter).find(a => a.uriString === uriString && a.provider.providerId === provider.providerId); + if (!adapter) { + let manager = await provider.getExecuteManager(notebookUri); + adapter = new ExecuteManagerAdapter(provider, manager, uriString); + adapter.handle = this._addNewAdapter(adapter); + } return adapter; } diff --git a/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts index 59711096ea..099218f6b8 100644 --- a/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts @@ -23,6 +23,7 @@ import { ExtHostNotebookDocumentData } from 'sql/workbench/api/common/extHostNot import { ExtHostNotebookEditor } from 'sql/workbench/api/common/extHostNotebookEditor'; import { VSCodeNotebookDocument } from 'sql/workbench/api/common/notebooks/vscodeNotebookDocument'; import { VSCodeNotebookEditor } from 'sql/workbench/api/common/notebooks/vscodeNotebookEditor'; +import { docNotFoundForUriError } from 'sql/base/common/locConstants'; type Adapter = azdata.nb.NavigationProvider; @@ -113,17 +114,19 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume if (delta.addedDocuments) { for (const data of delta.addedDocuments) { const resource = URI.revive(data.uri); - ok(!this._documents.has(resource.toString()), `document '${resource} already exists!'`); - - const documentData = new ExtHostNotebookDocumentData( - this._proxy, - resource, - data.providerId, - data.isDirty, - data.cells - ); - this._documents.set(resource.toString(), documentData); - addedDocuments.push(documentData); + // Can potentially have a document with this URI already if it was created + // separately from the notebook editor. + if (!this._documents.has(resource.toString())) { + const documentData = new ExtHostNotebookDocumentData( + this._proxy, + resource, + data.providerId, + data.isDirty, + data.cells + ); + this._documents.set(resource.toString(), documentData); + addedDocuments.push(documentData); + } } } @@ -220,6 +223,42 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume //#endregion //#region Extension accessible methods + async createNotebookDocument(providerId: string, contents?: azdata.nb.INotebookContents): Promise { + let options: INotebookShowOptions = {}; + if (contents) { + options.providerId = providerId; + options.initialContent = JSON.stringify(contents); + } + let uriComps = await this._proxy.$tryCreateNotebookDocument(options); + let uri = URI.revive(uriComps); + let notebookCells = contents?.cells?.map(cellContents => { + return { + contents: cellContents, + uri: undefined + }; + }); + + let documentData = new ExtHostNotebookDocumentData( + this._proxy, + uri, + providerId, + false, + notebookCells ?? [] + ); + this._documents.set(uri.toString(), documentData); + this._onDidOpenNotebook.fire(documentData.document); + + return uri; + } + + async openNotebookDocument(uri: vscode.Uri): Promise { + let docData = this._documents.get(uri.toString()); + if (!docData) { + throw new Error(docNotFoundForUriError); + } + return docData.document; + } + showNotebookDocument(uri: vscode.Uri, showOptions: azdata.nb.NotebookShowOptions): Thenable { return this.doShowNotebookDocument(uri, showOptions); } @@ -289,9 +328,5 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume this._adapters.delete(handle); }); } - - createNotebookDocument(providerId: string, contents: azdata.nb.INotebookContents): Promise { - return this._proxy.$createNotebookDocument(providerId, contents); - } //#endregion } diff --git a/src/sql/workbench/api/common/notebooks/adsNotebookController.ts b/src/sql/workbench/api/common/notebooks/adsNotebookController.ts index 0ee6fe7a32..196bc094d4 100644 --- a/src/sql/workbench/api/common/notebooks/adsNotebookController.ts +++ b/src/sql/workbench/api/common/notebooks/adsNotebookController.ts @@ -19,6 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; type SelectionChangedEvent = { selected: boolean, notebook: vscode.NotebookDocument; }; type MessageReceivedEvent = { editor: vscode.NotebookEditor, message: any; }; type ExecutionHandler = (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable; +type LanguagesHandler = (languages: string[]) => void; type InterruptHandler = (notebook: vscode.NotebookDocument) => void | Promise; /** @@ -39,8 +40,8 @@ export class ADSNotebookController implements vscode.NotebookController { private _id: string, private _viewType: string, private _label: string, - private _addLanguagesHandler: (providerId, languages) => void, private _extHostNotebookDocumentsAndEditors: ExtHostNotebookDocumentsAndEditors, + private _languagesHandler: LanguagesHandler, private _handler?: ExecutionHandler, preloads?: vscode.NotebookRendererScript[] ) { @@ -107,7 +108,7 @@ export class ADSNotebookController implements vscode.NotebookController { public set supportedLanguages(value: string[]) { this._kernelData.supportedLanguages = value; - this._addLanguagesHandler(this._viewType, value); + this._languagesHandler(value); this._languagesAdded.resolve(); } diff --git a/src/sql/workbench/api/common/notebooks/notebookUtils.ts b/src/sql/workbench/api/common/notebooks/notebookUtils.ts index 9d9048aaa5..c9786c46f9 100644 --- a/src/sql/workbench/api/common/notebooks/notebookUtils.ts +++ b/src/sql/workbench/api/common/notebooks/notebookUtils.ts @@ -8,21 +8,30 @@ import type * as azdata from 'azdata'; import { URI } from 'vs/base/common/uri'; import { asArray } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; -import { OutputTypes } from 'sql/workbench/services/notebook/common/contracts'; +import { CellTypes, MimeTypes, OutputTypes } from 'sql/workbench/services/notebook/common/contracts'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; import { NotebookCellKind } from 'vs/workbench/api/common/extHostTypes'; -export function convertToVSCodeNotebookCell(cellSource: string | string[], index: number, uri: URI, language: string): vscode.NotebookCell { +export const DotnetInteractiveJupyterLanguagePrefix = '.net-'; +export const DotnetInteractiveLanguagePrefix = 'dotnet-interactive.'; +export const DotnetInteractiveJupyterLabelPrefix = '.NET ('; +export const DotnetInteractiveLabel = '.NET Interactive'; + +export function convertToVSCodeNotebookCell(cellKind: azdata.nb.CellType, cellIndex: number, cellUri: URI, docUri: URI, cellLanguage: string, cellSource?: string | string[]): vscode.NotebookCell { return { - index: index, + kind: cellKind === CellTypes.Code ? NotebookCellKind.Code : NotebookCellKind.Markup, + index: cellIndex, document: { - uri: uri, - languageId: language, - getText: () => Array.isArray(cellSource) ? cellSource.join('') : cellSource, + uri: cellUri, + languageId: cellLanguage, + getText: () => Array.isArray(cellSource) ? cellSource.join('') : (cellSource ?? ''), }, notebook: { - uri: uri - } + uri: docUri + }, + outputs: [], + metadata: {}, + mime: undefined }; } @@ -33,7 +42,7 @@ export function convertToADSCellOutput(outputs: vscode.NotebookCellOutput | vsco outputData[item.mime] = VSBuffer.wrap(item.data).toString(); } return { - output_type: 'execute_result', + output_type: OutputTypes.ExecuteResult, data: outputData, execution_count: executionOrder, metadata: output.metadata, @@ -59,7 +68,7 @@ export function convertToVSCodeCellOutput(output: azdata.nb.ICellOutput): vscode case OutputTypes.Stream: let streamOutput = output as azdata.nb.IStreamResult; convertedOutputItems = [{ - mime: 'text/html', + mime: MimeTypes.HTML, data: VSBuffer.fromString(Array.isArray(streamOutput.text) ? streamOutput.text.join('') : streamOutput.text).buffer }]; break; @@ -67,7 +76,7 @@ export function convertToVSCodeCellOutput(output: azdata.nb.ICellOutput): vscode let errorOutput = output as azdata.nb.IErrorResult; let errorString = errorOutput.ename + ': ' + errorOutput.evalue + (errorOutput.traceback ? '\n' + errorOutput.traceback?.join('\n') : ''); convertedOutputItems = [{ - mime: 'text/html', + mime: MimeTypes.HTML, data: VSBuffer.fromString(errorString).buffer }]; break; @@ -79,28 +88,26 @@ export function convertToVSCodeCellOutput(output: azdata.nb.ICellOutput): vscode }; } -export function convertToADSNotebookContents(notebookData: vscode.NotebookData): azdata.nb.INotebookContents { +export function convertToADSNotebookContents(notebookData: vscode.NotebookData | undefined): azdata.nb.INotebookContents { let result = { - cells: notebookData.cells?.map(cell => { + cells: notebookData?.cells?.map(cell => { let executionOrder = cell.executionSummary?.executionOrder; - return { - cell_type: cell.kind === NotebookCellKind.Code ? 'code' : 'markdown', + let convertedCell: azdata.nb.ICellContents = { + cell_type: cell.kind === NotebookCellKind.Code ? CellTypes.Code : CellTypes.Markdown, source: cell.value, - metadata: { - language: cell.languageId - }, execution_count: executionOrder, outputs: cell.outputs ? convertToADSCellOutput(cell.outputs, executionOrder) : undefined }; + convertedCell.metadata = cell.metadata?.custom?.metadata ?? {}; + if (!convertedCell.metadata.language) { + convertedCell.metadata.language = cell.languageId; + } + return convertedCell; }), - metadata: notebookData.metadata ?? {}, - nbformat: notebookData.metadata?.custom?.nbformat ?? NBFORMAT, - nbformat_minor: notebookData.metadata?.custom?.nbformat_minor ?? NBFORMAT_MINOR + metadata: notebookData?.metadata?.custom?.metadata ?? {}, + nbformat: notebookData?.metadata?.custom?.nbformat ?? NBFORMAT, + nbformat_minor: notebookData?.metadata?.custom?.nbformat_minor ?? NBFORMAT_MINOR }; - - // Clear out extra lingering vscode custom metadata - delete result.metadata.custom; - return result; } @@ -108,20 +115,27 @@ export function convertToVSCodeNotebookData(notebook: azdata.nb.INotebookContent let result: vscode.NotebookData = { cells: notebook.cells?.map(cell => { return { - kind: cell.cell_type === 'code' ? NotebookCellKind.Code : NotebookCellKind.Markup, - value: Array.isArray(cell.source) ? cell.source.join('\n') : cell.source, - languageId: cell.metadata?.language, + kind: cell.cell_type === CellTypes.Code ? NotebookCellKind.Code : NotebookCellKind.Markup, + value: Array.isArray(cell.source) ? cell.source.join('') : cell.source, + languageId: cell.metadata?.language ?? notebook.metadata.language_info?.name, outputs: cell.outputs?.map(output => convertToVSCodeCellOutput(output)), executionSummary: { executionOrder: cell.execution_count + }, + metadata: { + custom: { + metadata: cell.metadata + } } }; }), - metadata: notebook.metadata - }; - result.metadata.custom = { - nbformat: notebook.nbformat, - nbformat_minor: notebook.nbformat_minor + metadata: { + custom: { + metadata: notebook.metadata, + nbformat: notebook.nbformat, + nbformat_minor: notebook.nbformat_minor + } + } }; return result; } diff --git a/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts b/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts index f46766897d..2f962dfcf9 100644 --- a/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts +++ b/src/sql/workbench/api/common/notebooks/vscodeExecuteProvider.ts @@ -8,6 +8,7 @@ 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 { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; class VSCodeFuture implements azdata.nb.IFuture { private _inProgress = true; @@ -71,16 +72,25 @@ class VSCodeKernel implements azdata.nb.IKernel { private readonly _info: azdata.nb.IInfoReply; private readonly _kernelSpec: azdata.nb.IKernelSpec; - constructor(private readonly _controller: ADSNotebookController, private readonly _options: azdata.nb.ISessionOptions, language: string) { + constructor(private readonly _controller: ADSNotebookController, private readonly _options: azdata.nb.ISessionOptions) { this._id = this._options.kernelId ?? (VSCodeKernel.kernelId++).toString(); - this._name = this._options.kernelName ?? this._controller.notebookType; + this._kernelSpec = this._options.kernelSpec ?? { + name: this._controller.notebookType, + display_name: this._controller.label, + }; + if (!this._kernelSpec.language) { + this._kernelSpec.language = this._controller.supportedLanguages[0]; + this._kernelSpec.supportedLanguages = this._controller.supportedLanguages; + } + + this._name = this._kernelSpec.name; this._info = { protocol_version: '', implementation: '', implementation_version: '', language_info: { - name: language, - version: '', + name: this._kernelSpec.language, + oldName: this._kernelSpec.oldLanguage }, banner: '', help_links: [{ @@ -88,11 +98,6 @@ class VSCodeKernel implements azdata.nb.IKernel { url: '' }] }; - this._kernelSpec = { - name: this._name, - language: language, - display_name: this._name - }; } public get id(): string { @@ -123,17 +128,20 @@ class VSCodeKernel implements azdata.nb.IKernel { return this._info; } + public get spec(): azdata.nb.IKernelSpec { + return this._kernelSpec; + } + getSpec(): Thenable { - return Promise.resolve(this._kernelSpec); + return Promise.resolve(this.spec); } requestExecute(content: azdata.nb.IExecuteRequest, disposeOnDone?: boolean): azdata.nb.IFuture { let executePromise: Promise; if (this._controller.executeHandler) { - let cell = convertToVSCodeNotebookCell(content.code, content.cellIndex, content.notebookUri, this._kernelSpec.language); + let cell = convertToVSCodeNotebookCell(CellTypes.Code, content.cellIndex, content.cellUri, content.notebookUri, content.language ?? this._kernelSpec.language, content.code); executePromise = Promise.resolve(this._controller.executeHandler([cell], cell.notebook, this._controller)); - } - else { + } else { executePromise = Promise.resolve(); } @@ -153,8 +161,8 @@ class VSCodeKernel implements azdata.nb.IKernel { class VSCodeSession implements azdata.nb.ISession { private _kernel: VSCodeKernel; private _defaultKernelLoaded = false; - constructor(controller: ADSNotebookController, private readonly _options: azdata.nb.ISessionOptions, language: string) { - this._kernel = new VSCodeKernel(controller, this._options, language); + constructor(controller: ADSNotebookController, private readonly _options: azdata.nb.ISessionOptions) { + this._kernel = new VSCodeKernel(controller, this._options); } public set defaultKernelLoaded(value) { @@ -189,10 +197,14 @@ class VSCodeSession implements azdata.nb.ISession { return 'connected'; } - public get kernel(): azdata.nb.IKernel { + public get vsKernel(): VSCodeKernel { return this._kernel; } + public get kernel(): azdata.nb.IKernel { + return this.vsKernel; + } + changeKernel(kernelInfo: azdata.nb.IKernelSpec): Thenable { return Promise.resolve(this._kernel); } @@ -207,7 +219,7 @@ class VSCodeSession implements azdata.nb.ISession { } class VSCodeSessionManager implements azdata.nb.SessionManager { - private _sessions: azdata.nb.ISession[] = []; + private _sessions: VSCodeSession[] = []; constructor(private readonly _controller: ADSNotebookController) { } @@ -221,16 +233,16 @@ class VSCodeSessionManager implements azdata.nb.SessionManager { } public get specs(): azdata.nb.IAllKernels { - let languages = this._controller.supportedLanguages?.length > 0 ? this._controller.supportedLanguages : [this._controller.label]; + // Have to return the default kernel here, since the manager specs are accessed before kernels get added + let defaultKernel: azdata.nb.IKernelSpec = { + name: this._controller.notebookType, + language: this._controller.supportedLanguages[0], + display_name: this._controller.label, + supportedLanguages: this._controller.supportedLanguages ?? [] + }; return { - defaultKernel: languages[0], - kernels: languages.map(language => { - return { - name: language, - language: language, - display_name: language - }; - }) + defaultKernel: defaultKernel.name, + kernels: [defaultKernel] }; } @@ -238,8 +250,7 @@ class VSCodeSessionManager implements azdata.nb.SessionManager { if (!this.isReady) { return Promise.reject(new Error(nls.localize('errorStartBeforeReady', "Cannot start a session, the manager is not yet initialized"))); } - - let session: azdata.nb.ISession = new VSCodeSession(this._controller, options, this.specs.defaultKernel); + let session = new VSCodeSession(this._controller, options); let index = this._sessions.findIndex(session => session.path === options.path); if (index > -1) { this._sessions.splice(index); diff --git a/src/sql/workbench/api/common/notebooks/vscodeNotebookDocument.ts b/src/sql/workbench/api/common/notebooks/vscodeNotebookDocument.ts index 9449cd59b7..16783ab186 100644 --- a/src/sql/workbench/api/common/notebooks/vscodeNotebookDocument.ts +++ b/src/sql/workbench/api/common/notebooks/vscodeNotebookDocument.ts @@ -11,7 +11,7 @@ export class VSCodeNotebookDocument implements vscode.NotebookDocument { private readonly _convertedCells: vscode.NotebookCell[]; constructor(private readonly _notebookDoc: azdata.nb.NotebookDocument) { - this._convertedCells = this._notebookDoc.cells?.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.source, index, this._notebookDoc.uri, this._notebookDoc.kernelSpec?.language)); + this._convertedCells = this._notebookDoc.cells?.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.cell_type, index, cell.uri, this._notebookDoc.uri, cell.contents.metadata?.language, cell.contents.source)); } public get uri() { return this._notebookDoc.uri; } diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 227b800e68..423388f2dd 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -956,7 +956,8 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterExecuteProvider(handle: number): void; $onFutureMessage(futureId: number, type: FutureMessageType, payload: azdata.nb.IMessage): void; $onFutureDone(futureId: number, done: INotebookFutureDone): void; - $updateProviderDescriptionLanguages(providerId: string, languages: string[]): void; + $updateProviderKernels(providerId: string, languages: azdata.nb.IStandardKernel[]): void; + $updateKernelLanguages(providerId: string, kernelName: string, languages: string[]): void; } export interface INotebookDocumentsAndEditorsDelta { @@ -1011,6 +1012,7 @@ export interface ExtHostNotebookDocumentsAndEditorsShape { export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable { $trySetTrusted(_uri: UriComponents, isTrusted: boolean): Thenable; $trySaveDocument(uri: UriComponents): Thenable; + $tryCreateNotebookDocument(options: INotebookShowOptions): Promise; $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: INotebookEditOperation[], opts: IUndoStopOptions): Promise; $runCell(id: string, cellUri: UriComponents): Promise; @@ -1019,7 +1021,6 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable $clearAllOutputs(id: string): Promise; $changeKernel(id: string, kernel: azdata.nb.IKernelSpec): Promise; $registerNavigationProvider(providerId: string, handle: number); - $createNotebookDocument(providerId: string, contents: azdata.nb.INotebookContents): Promise; } export interface ExtHostExtensionManagementShape { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 129f7872a0..003fdfde99 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -552,6 +552,19 @@ export class SqlThemeIcon { } } +export interface ICellMetadata { + language?: string | undefined; + tags?: string[] | undefined; + azdata_cell_guid?: string | undefined; + connection_name?: string; + /** + * .NET Interactive metadata. This is only required for compatibility with the .NET Interactive extension. + */ + dotnet_interactive?: { + language: string; + } +} + export interface ISerializationManagerDetails { handle: number; hasContentManager: boolean; diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html index de9fc75439..f38a06bf8e 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html @@ -12,7 +12,12 @@
- +
+ +
+ {{cellModel.displayLanguage}} +
+
{{parametersText}}
diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index c377f21743..4ad7eecd33 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -48,6 +48,7 @@ const DEFAULT_OR_LOCAL_CONTEXT_ID = '-1'; export class CodeComponent extends CellView implements OnInit, OnChanges { @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; + @ViewChild('cellLanguage', { read: ElementRef }) private languageElement: ElementRef; public get cellModel(): ICellModel { return this._cellModel; @@ -265,6 +266,12 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { this.setFocusAndScroll(); } })); + this._register(this.cellModel.onLanguageChanged(language => { + let nativeElement = this.languageElement.nativeElement; + nativeElement.innerText = this.cellModel.displayLanguage; + this.updateLanguageMode(); + this._changeRef.detectChanges(); + })); this._register(this.cellModel.onCollapseStateChanged(isCollapsed => { this.onCellCollapse(isCollapsed); })); @@ -379,8 +386,6 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { this.cellModel.setOverrideLanguage(magic.language); this.updateLanguageMode(); } - } else { - this.cellModel.setOverrideLanguage(undefined); } } } catch (err) { diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.css b/src/sql/workbench/contrib/notebook/browser/cellViews/code.css index 5f45d1d403..09bbdb061a 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.css +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.css @@ -98,6 +98,10 @@ code-component .carbon-taskbar .codicon.hideIcon.execCountHundred { margin-left: -6px; } +code-component collapse-component { + flex-grow: 1; +} + code-component .hide-component-button { height: 16px; width: 100%; @@ -106,6 +110,15 @@ code-component .hide-component-button { background-repeat: no-repeat; background-position: center; background-color: transparent; + margin-top: 4px; + margin-bottom: 4px; +} + +code-component .cellLanguage { + padding: 2px 15px; + display: inline-block; + text-align: center; + font-size: 16px; } code-component .parameter { diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index bc80fc08b0..cf1a8fb5bd 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -40,6 +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'; export type ModeViewSaveHandler = (handle: number) => Thenable; const languageAssociationRegistry = Registry.as(LanguageAssociationExtensions.LanguageAssociations); @@ -351,7 +352,8 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu connectionProviderIds: kernel.connectionProviderIds, name: kernel.name, displayName: kernel.displayName, - notebookProvider: kernel.notebookProvider + notebookProvider: kernel.notebookProvider, + supportedLanguages: kernel.supportedLanguages }); }); } @@ -540,6 +542,26 @@ export class NotebookEditorContentLoader implements IContentLoader { async loadContent(): Promise { let notebookEditorModel = await this.notebookInput.resolve(); - return this.contentManager.deserializeNotebook(notebookEditorModel.contentString); + let notebookContents = await this.contentManager.deserializeNotebook(notebookEditorModel.contentString); + + // 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; + + 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/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 88c19b2bce..0569b0b2cb 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -410,7 +410,9 @@ registerComponentType({ 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' ], rank: 120, safe: true, @@ -418,6 +420,19 @@ registerComponentType({ selector: MimeRendererComponent.SELECTOR }); +/** + * A mime renderer component for VS Code Notebook error data. + */ +registerComponentType({ + mimeTypes: [ + 'application/vnd.code.notebook.error' + ], + rank: 121, + safe: true, + ctor: MimeRendererComponent, + selector: MimeRendererComponent.SELECTOR +}); + /** * A placeholder component for deprecated rendered JavaScript. */ diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts index 1ac6f13529..3492de4cfe 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts @@ -67,7 +67,8 @@ class TestNotebookModel extends NotebookModelStub { name: 'StandardKernel1', displayName: 'StandardKernel1', connectionProviderIds: ['Kernel1 connection 1', 'Kernel1 connection2'], - notebookProvider: 'kernel provider1' + notebookProvider: 'kernel provider1', + supportedLanguages: ['python'] } ], [ @@ -76,7 +77,8 @@ class TestNotebookModel extends NotebookModelStub { name: 'StandardKernel2', displayName: 'StandardKernel2', connectionProviderIds: ['Kernel1 connection 2', 'Kernel1 connection2'], - notebookProvider: 'kernel provider2' + notebookProvider: 'kernel provider2', + supportedLanguages: ['python'] } ] ] diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 7a6c2bd169..5437cb324b 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -40,7 +40,8 @@ suite('Notebook Input', function (): void { name: 'TestName', displayName: 'TestDisplayName', connectionProviderIds: ['TestId'], - notebookProvider: testProvider + notebookProvider: testProvider, + supportedLanguages: ['python'] }]); }); let testManager: ISerializationManager = { @@ -129,12 +130,14 @@ suite('Notebook Input', function (): void { name: 'TestName1', displayName: 'TestDisplayName1', connectionProviderIds: ['TestId1'], - notebookProvider: 'TestProvider' + notebookProvider: 'TestProvider', + supportedLanguages: ['python'] }, { name: 'TestName2', displayName: 'TestDisplayName2', connectionProviderIds: ['TestId2'], - notebookProvider: 'TestProvider' + notebookProvider: 'TestProvider', + supportedLanguages: ['python'] }]; untitledNotebookInput.standardKernels = testKernels; assert.deepStrictEqual(untitledNotebookInput.standardKernels, testKernels); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts index 1d07a824f9..9083fb4dbb 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts @@ -236,7 +236,8 @@ suite.skip('NotebookService:', function (): void { standardKernels: [{ name: 'kernel1', connectionProviderIds: [], - displayName: 'Kernel 1' + displayName: 'Kernel 1', + supportedLanguages: ['python'] }], provider: 'otherProvider' }; diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts index 1f576ee760..9206049b1e 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts @@ -21,12 +21,14 @@ suite('notebookUtils', function (): void { const testKernel: nb.IStandardKernel = { name: 'testName', displayName: 'testDisplayName', - connectionProviderIds: ['testId1', 'testId2'] + connectionProviderIds: ['testId1', 'testId2'], + supportedLanguages: ['python'] }; const sqlStandardKernel: nb.IStandardKernel = { name: notebookConstants.SQL, displayName: notebookConstants.SQL, - connectionProviderIds: [notebookConstants.SQL_CONNECTION_PROVIDER] + connectionProviderIds: [notebookConstants.SQL_CONNECTION_PROVIDER], + supportedLanguages: ['sql'] }; function setupMockNotebookService() { @@ -108,7 +110,8 @@ suite('notebookUtils', function (): void { name: 'testName', displayName: 'testDisplayName', connectionProviderIds: ['testId1', 'testId2'], - notebookProvider: 'testProvider' + notebookProvider: 'testProvider', + supportedLanguages: ['python'] }]); }); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 60f271fd75..5f168a5e90 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -17,7 +17,7 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { URI, UriComponents } from 'vs/base/common/uri'; import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; -import { IEditorPane } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { INotebookView, INotebookViewCell, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; @@ -235,6 +235,9 @@ export class ServerManagerStub implements nb.ServerManager { } export class NotebookServiceStub implements INotebookService { + createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { + throw new Error('Method not implemented.'); + } _serviceBrand: undefined; get onNotebookEditorAdd(): vsEvent.Event { throw new Error('Method not implemented.'); diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 04af171484..c171041022 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -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(); private _loaded: boolean; private _stdInVisible: boolean; - private _metadata: nb.ICellMetadata; + private _metadata: ICellMetadata; private _isCollapsed: boolean; + private _onLanguageChanged = new Emitter(); private _onCollapseStateChanged = new Emitter(); 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 { + return this._onLanguageChanged.event; + } + public get onCollapseStateChanged(): Event { 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 { @@ -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 { + private handleStdIn(msg: nb.IStdinMessage): void | Thenable { 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) { diff --git a/src/sql/workbench/services/notebook/browser/models/clientSession.ts b/src/sql/workbench/services/notebook/browser/models/clientSession.ts index 6b867f1e25..6ddc9986af 100644 --- a/src/sql/workbench/services/notebook/browser/models/clientSession.ts +++ b/src/sql/workbench/services/notebook/browser/models/clientSession.ts @@ -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 { + private async startSessionInstance(kernelSpec: nb.IKernelSpec): Promise { 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; } diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index e24d2aaaea..59fffe5640 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -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; readonly onCollapseStateChanged: Event; readonly onParameterStateChanged: Event; readonly onCellModeChanged: Event; diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index 1be94ee92a..a12a2990b5 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -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)) { diff --git a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts index 741bdedfc3..214c85d448 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts @@ -59,6 +59,7 @@ export interface IStandardKernelWithProvider { readonly displayName: string; readonly connectionProviderIds: string[]; readonly notebookProvider: string; + readonly supportedLanguages: string[]; } export interface IEndpoint { diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index 2a5bc08aac..a64f425615 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -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; + openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise; getUntitledUriPath(originalTitle: string): string; diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts index 649ae3806c..d878bf63d1 100644 --- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts @@ -248,13 +248,18 @@ export class NotebookService extends Disposable implements INotebookService { lifecycleService.onWillShutdown(() => this.shutdown()); } - public async openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise { - const uri = URI.revive(resource); - - const editorOptions: ITextEditorOptions = { - preserveFocus: options.preserveFocus, - pinned: !options.preview - }; + public async createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { + 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 { + 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] + }] }); } diff --git a/src/sql/workbench/services/notebook/browser/outputs/factories.ts b/src/sql/workbench/services/notebook/browser/outputs/factories.ts index bede827e13..3482691fb4 100644 --- a/src/sql/workbench/services/notebook/browser/outputs/factories.ts +++ b/src/sql/workbench/services/notebook/browser/outputs/factories.ts @@ -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 { + 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. */ diff --git a/src/sql/workbench/services/notebook/common/notebookConstants.ts b/src/sql/workbench/services/notebook/common/notebookConstants.ts index 9042ad0a3c..bfd90e51e1 100644 --- a/src/sql/workbench/services/notebook/common/notebookConstants.ts +++ b/src/sql/workbench/services/notebook/common/notebookConstants.ts @@ -22,5 +22,6 @@ export enum KernelsLanguage { SparkScala = 'scala', SparkR = 'sparkr', PowerShell = 'powershell', - CSharp = 'cs' + CSharp = 'csharp', + FSharp = 'fsharp' } diff --git a/src/sql/workbench/services/notebook/common/notebookRegistry.ts b/src/sql/workbench/services/notebook/common/notebookRegistry.ts index cd31311620..8a1ffcf21d 100644 --- a/src/sql/workbench/services/notebook/common/notebookRegistry.ts +++ b/src/sql/workbench/services/notebook/common/notebookRegistry.ts @@ -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(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 }); 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 169535928a..be5767fe76 100644 --- a/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts +++ b/src/sql/workbench/test/electron-browser/api/vscodeNotebookApi.test.ts @@ -64,21 +64,23 @@ suite('Notebook Serializer', () => { } }], metadata: { - kernelspec: { - name: 'python3', - display_name: 'Python 3', - language: 'python' - }, - language_info: { - name: 'python', - version: '3.8.10', - mimetype: 'text/x-python', - codemirror_mode: { - name: 'ipython', - version: '3' - } - }, custom: { + metadata: { + kernelspec: { + name: 'python3', + display_name: 'Python 3', + language: 'python' + }, + language_info: { + name: 'python', + version: '3.8.10', + mimetype: 'text/x-python', + codemirror_mode: { + name: 'ipython', + version: '3' + } + } + }, nbformat: NBFORMAT, nbformat_minor: NBFORMAT_MINOR } @@ -161,6 +163,13 @@ suite('Notebook Serializer', () => { }], executionSummary: { executionOrder: 1 + }, + metadata: { + custom: { + metadata: { + language: 'python' + } + } } }, { kind: NotebookCellKind.Code, @@ -176,24 +185,33 @@ suite('Notebook Serializer', () => { }], executionSummary: { executionOrder: 2 + }, + metadata: { + custom: { + metadata: { + language: 'python' + } + } } }], metadata: { - kernelspec: { - name: 'python3', - display_name: 'Python 3', - language: 'python' - }, - language_info: { - name: 'python', - version: '3.8.10', - mimetype: 'text/x-python', - codemirror_mode: { - name: 'ipython', - version: '3' - } - }, custom: { + metadata: { + kernelspec: { + name: 'python3', + display_name: 'Python 3', + language: 'python' + }, + language_info: { + name: 'python', + version: '3.8.10', + mimetype: 'text/x-python', + codemirror_mode: { + name: 'ipython', + version: '3' + } + } + }, nbformat: NBFORMAT, nbformat_minor: NBFORMAT_MINOR } @@ -349,7 +367,10 @@ suite('Notebook Serializer', () => { cells: [{ contents: { cell_type: 'code', - source: '1+1' + source: '1+1', + metadata: { + language: 'python' + } } }, { contents: { @@ -413,7 +434,7 @@ suite('Notebook Serializer', () => { } test('Retrieve range of cells from VS Code NotebookDocument', async () => { - let expectedCells: vscode.NotebookCell[] = testDoc.cells.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.source, index, testDoc.uri, testDoc.kernelSpec.language)); + let expectedCells: vscode.NotebookCell[] = testDoc.cells.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.cell_type, index, cell.uri, testDoc.uri, cell.contents.metadata?.language, cell.contents.source)); let vsDoc = new VSCodeNotebookDocument(testDoc); let actualCells = vsDoc.getCells(); @@ -430,7 +451,7 @@ suite('Notebook Serializer', () => { }); test('Retrieve specific cell from VS Code NotebookDocument', async () => { - let expectedCells: vscode.NotebookCell[] = testDoc.cells.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.source, index, testDoc.uri, testDoc.kernelSpec.language)); + let expectedCells: vscode.NotebookCell[] = testDoc.cells.map((cell, index) => convertToVSCodeNotebookCell(cell.contents.cell_type, index, cell.uri, testDoc.uri, cell.contents.metadata?.language, cell.contents.source)); let vsDoc = new VSCodeNotebookDocument(testDoc); let firstCell = vsDoc.cellAt(0); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a2efe5d178..ea05e07e71 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -50,7 +50,6 @@ import { throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/ser import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import type * as vscode from 'vscode'; -import type * as azdata from 'azdata'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { values } from 'vs/base/common/collections'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; @@ -93,12 +92,12 @@ import { matchesScheme } from 'vs/platform/opener/common/opener'; // import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; {{SQL CARBON EDIT}} Disable VS Code notebooks // import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; {{SQL CARBON EDIT}} Remove until we need it import { ExtHostNotebook } from 'sql/workbench/api/common/extHostNotebook'; -import { functionalityNotSupportedError, invalidArgumentsError } from 'sql/base/common/locConstants'; +import { docCreationFailedError, functionalityNotSupportedError, invalidArgumentsError } from 'sql/base/common/locConstants'; import { ExtHostNotebookDocumentsAndEditors } from 'sql/workbench/api/common/extHostNotebookDocumentsAndEditors'; import { VSCodeNotebookDocument } from 'sql/workbench/api/common/notebooks/vscodeNotebookDocument'; import { VSCodeNotebookEditor } from 'sql/workbench/api/common/notebooks/vscodeNotebookEditor'; -import { convertToADSNotebookContents } from 'sql/workbench/api/common/notebooks/notebookUtils'; import { IdGenerator } from 'vs/base/common/idGenerator'; +import { convertToADSNotebookContents } from 'sql/workbench/api/common/notebooks/notebookUtils'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -893,16 +892,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor, ex }, async openNotebookDocument(uriOrType?: URI | string, content?: vscode.NotebookData): Promise { // {{SQL CARBON EDIT}} Use our own notebooks - let doc: azdata.nb.NotebookDocument; + let uri: URI; if (URI.isUri(uriOrType)) { - let editor = await extHostNotebookDocumentsAndEditors.showNotebookDocument(uriOrType, {}); - doc = editor.document; + uri = uriOrType; + await extHostNotebookDocumentsAndEditors.openNotebookDocument(uriOrType); } else if (typeof uriOrType === 'string') { - let convertedContents = convertToADSNotebookContents(content); - doc = await extHostNotebookDocumentsAndEditors.createNotebookDocument(uriOrType, convertedContents); + uri = URI.revive(await extHostNotebookDocumentsAndEditors.createNotebookDocument(uriOrType, convertToADSNotebookContents(content))); } else { throw new Error(invalidArgumentsError); } + let doc = extHostNotebookDocumentsAndEditors.getDocument(uri.toString())?.document; + if (!doc) { + throw new Error(docCreationFailedError); + } return new VSCodeNotebookDocument(doc); }, get onDidOpenNotebookDocument(): Event { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 155eade6d3..c18547a2db 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { groupBy } from 'vs/base/common/arrays'; +// import { groupBy } from 'vs/base/common/arrays'; {{SQL CARBON EDIT}} import { CancellationToken } from 'vs/base/common/cancellation'; -import { compare } from 'vs/base/common/strings'; +// import { compare } from 'vs/base/common/strings'; {{SQL CARBON EDIT}} import { URI } from 'vs/base/common/uri'; import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; @@ -28,38 +28,40 @@ export class ResourceNotebookCellEdit extends ResourceEdit { export class BulkCellEdits { + // {{SQL CARBON EDIT}} Remove private modifiers to fix value-not-read build errors constructor( - private readonly _undoRedoGroup: UndoRedoGroup, + _undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, - private readonly _progress: IProgress, - private readonly _token: CancellationToken, - private readonly _edits: ResourceNotebookCellEdit[], - @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, + _progress: IProgress, + _token: CancellationToken, + _edits: ResourceNotebookCellEdit[], + @INotebookEditorModelResolverService _notebookModelService: INotebookEditorModelResolverService, ) { } async apply(): Promise { - const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); + // {{SQL CARBON EDIT}} Use our own notebooks + // const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); - for (let group of editsByNotebook) { - if (this._token.isCancellationRequested) { - break; - } - const [first] = group; - const ref = await this._notebookModelService.resolve(first.resource); + // for (let group of editsByNotebook) { + // if (this._token.isCancellationRequested) { + // break; + // } + // const [first] = group; + // const ref = await this._notebookModelService.resolve(first.resource); - // check state - if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) { - ref.dispose(); - throw new Error(`Notebook '${first.resource}' has changed in the meantime`); - } + // // check state + // if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) { + // ref.dispose(); + // throw new Error(`Notebook '${first.resource}' has changed in the meantime`); + // } - // apply edits - const edits = group.map(entry => entry.cellEdit); - ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup); - ref.dispose(); + // // apply edits + // const edits = group.map(entry => entry.cellEdit); + // ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup); + // ref.dispose(); - this._progress.report(undefined); - } + // this._progress.report(undefined); + // } } } diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index cb01453836..e4d5ca50f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -205,7 +205,7 @@ notebooksExtensionPoint.setHandler(extensions => { let adsProvider: ProviderDescriptionRegistration = { provider: notebookContribution.type, fileExtensions: extensions ?? [], - standardKernels: [] // Kernels get added later when a NotebookController is created for this provider + standardKernels: [] // The actual Kernels are the NotebookControllers that are contributed later }; adsNotebookRegistry.registerProviderDescription(adsProvider); }