diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index 5881154c14..6d775b5288 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -329,6 +329,14 @@ "Notebook" ], "configuration": "./language-configuration.json" + }, { + "id": "dib", + "extensions": [ + ".dib" + ], + "aliases": [ + ".NET Interactive Notebook" + ] } ], "menus": { diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 5ba39e2302..4dd171f22c 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -342,8 +342,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements } } - async $tryCreateNotebookDocument(options: INotebookShowOptions): Promise { - let input = await this._notebookService.createNotebookInput(options); + async $tryCreateNotebookDocument(providerId: string, contents?: azdata.nb.INotebookContents): Promise { + let input = await this._notebookService.createNotebookInputFromContents(providerId, contents); return input.resource; } diff --git a/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts index 099218f6b8..87c7cba9ae 100644 --- a/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/common/extHostNotebookDocumentsAndEditors.ts @@ -224,12 +224,7 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume //#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 uriComps = await this._proxy.$tryCreateNotebookDocument(providerId, contents); let uri = URI.revive(uriComps); let notebookCells = contents?.cells?.map(cellContents => { return { diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index d1f8860b2c..35244d32f5 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -1012,7 +1012,7 @@ export interface ExtHostNotebookDocumentsAndEditorsShape { export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable { $trySetTrusted(_uri: UriComponents, isTrusted: boolean): Thenable; $trySaveDocument(uri: UriComponents): Thenable; - $tryCreateNotebookDocument(options: INotebookShowOptions): Promise; + $tryCreateNotebookDocument(providerId: string, contents?: azdata.nb.INotebookContents): Promise; $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: INotebookEditOperation[], opts: IUndoStopOptions): Promise; $runCell(id: string, cellUri: UriComponents): Promise; diff --git a/src/sql/workbench/common/constants.ts b/src/sql/workbench/common/constants.ts index 687fc84dc8..b0515119a4 100644 --- a/src/sql/workbench/common/constants.ts +++ b/src/sql/workbench/common/constants.ts @@ -36,6 +36,9 @@ export const FILE_QUERY_EDITOR_TYPEID = 'workbench.editorInput.fileQueryInput'; export const RESOURCE_VIEWER_TYPEID = 'workbench.editorInput.resourceViewerInput'; export const JUPYTER_PROVIDER_ID = 'jupyter'; +export const INTERACTIVE_PROVIDER_ID = 'dotnet-interactive'; +export const INTERACTIVE_LANGUAGE_MODE = 'dib'; +export const DEFAULT_NB_LANGUAGE_MODE = 'notebook'; export const TSGOPS_WEB_QUALITY = 'tsgops-image'; // The version of the notebook file format that we support diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index cf1a8fb5bd..7cca5bd474 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -447,7 +447,11 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu private async assignProviders(): Promise { await this.extensionService.whenInstalledExtensionsRegistered(); - let providerIds: string[] = getProvidersForFileName(this._title, this.notebookService); + let mode: string; + if (this._textInput instanceof UntitledTextEditorInput) { + mode = this._textInput.model.getMode(); + } + let providerIds: string[] = getProvidersForFileName(this._title, this.notebookService, mode); if (providerIds && providerIds.length > 0) { this._providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0]; this._providers = providerIds; diff --git a/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts index 8b7b522dd7..663c420f97 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts @@ -27,7 +27,7 @@ export class UntitledNotebookInput extends NotebookInput { ) { super(title, resource, textInput, true, textModelService, instantiationService, notebookService, extensionService); // Set the mode explicitly so that the auto language detection doesn't run and mark the model as being JSON - this.textInput.resolve().then(() => this.setMode('notebook')); + this.textInput.resolve().then(() => this.setMode(textInput.model.getMode())); } public override get textInput(): UntitledTextEditorInput { diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 48aec72f13..9a94f39dba 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -238,7 +238,7 @@ export class NotebookServiceStub implements INotebookService { getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise { throw new Error('Method not implemented.'); } - createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { + createNotebookInputFromContents(providerId: string, contents?: nb.INotebookContents, resource?: UriComponents): Promise { throw new Error('Method not implemented.'); } _serviceBrand: undefined; diff --git a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts index 214c85d448..e18d63b582 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts @@ -7,6 +7,7 @@ import * as path from 'vs/base/common/path'; import { nb, ServerInfo } from 'azdata'; import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { URI } from 'vs/base/common/uri'; +import { DEFAULT_NB_LANGUAGE_MODE } from 'sql/workbench/common/constants'; export const clusterEndpointsProperty = 'clusterEndpoints'; export const hadoopEndpointNameGateway = 'gateway'; @@ -17,8 +18,11 @@ export function isStream(output: nb.ICellOutput): output is nb.IStreamResult { return output.output_type === 'stream'; } -export function getProvidersForFileName(fileName: string, notebookService: INotebookService): string[] { +export function getProvidersForFileName(fileName: string, notebookService: INotebookService, languageMode?: string): string[] { let fileExt = path.extname(fileName); + if (!fileExt && languageMode && languageMode !== DEFAULT_NB_LANGUAGE_MODE) { + fileExt = `.${languageMode}`; + } let providers: string[]; // First try to get provider for actual file type if (fileExt) { diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index c828f4c59f..770c2149b3 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -139,7 +139,7 @@ export interface INotebookService { */ notifyCellExecutionStarted(): void; - createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise; + createNotebookInputFromContents(providerId: string, contents?: azdata.nb.INotebookContents, resource?: UriComponents): Promise; openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise; diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts index 272a742b13..7ccad52fda 100644 --- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts @@ -50,7 +50,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { isINotebookInput } from 'sql/workbench/services/notebook/browser/interface'; import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; -import { JUPYTER_PROVIDER_ID, NotebookLanguage } from 'sql/workbench/common/constants'; +import { DEFAULT_NB_LANGUAGE_MODE, INTERACTIVE_LANGUAGE_MODE, INTERACTIVE_PROVIDER_ID, JUPYTER_PROVIDER_ID, NotebookLanguage } from 'sql/workbench/common/constants'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SqlSerializationProvider } from 'sql/workbench/services/notebook/browser/sql/sqlSerializationProvider'; @@ -248,30 +248,60 @@ export class NotebookService extends Disposable implements INotebookService { lifecycleService.onWillShutdown(() => this.shutdown()); } - public async createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { + private getUntitledFileUri(): URI { + // Need to create a new untitled URI, so find the lowest numbered one that's available + let uri: URI; + let counter = 1; + do { + uri = URI.from({ scheme: Schemas.untitled, path: `Notebook-${counter}` }); + counter++; + } while (this._untitledEditorService.get(uri)); + return uri; + } + + public async createNotebookInputFromContents(providerId: string, contents?: nb.INotebookContents, 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)); + uri = this.getUntitledFileUri(); + resource = uri; + } + + let serializedContent: string; + if (contents) { + // Have to serialize contents again first, since our notebook code assumes input is based on the raw file contents + let manager = await this.getOrCreateSerializationManager(providerId, uri); + serializedContent = await manager.contentManager.serializeNotebook(contents); + } + + let options: INotebookShowOptions = { + providerId: providerId, + initialContent: serializedContent + }; + return this.createNotebookInput(options, resource); + } + + private async createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { + let uri: URI; + if (resource) { + uri = URI.revive(resource); + } else { + uri = this.getUntitledFileUri(); } let isUntitled: boolean = uri.scheme === Schemas.untitled; let fileInput: IEditorInput; + let languageMode = options.providerId === INTERACTIVE_PROVIDER_ID ? INTERACTIVE_LANGUAGE_MODE : DEFAULT_NB_LANGUAGE_MODE; if (isUntitled && path.isAbsolute(uri.fsPath)) { - const model = this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }); + const model = this._untitledEditorService.create({ associatedResource: uri, mode: languageMode, initialValue: options.initialContent }); fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); } else { if (isUntitled) { - const model = this._untitledEditorService.create({ untitledResource: uri, mode: 'notebook', initialValue: options.initialContent }); + const model = this._untitledEditorService.create({ untitledResource: uri, mode: languageMode, initialValue: options.initialContent }); fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); } else { - fileInput = this._editorService.createEditorInput({ forceFile: true, resource: uri, mode: 'notebook' }); + fileInput = this._editorService.createEditorInput({ forceFile: true, resource: uri, mode: languageMode }); } } @@ -478,8 +508,9 @@ export class NotebookService extends Disposable implements INotebookService { } getProvidersForFileType(fileType: string): string[] | undefined { - let providers = this._fileToProviderDescriptions.get(fileType.toLowerCase()); - return providers?.map(provider => provider.provider); + let provDescriptions = this._fileToProviderDescriptions.get(fileType.toLowerCase()); + let providers = provDescriptions?.map(provider => provider.provider); + return [...new Set(providers)]; // Remove duplicates } public async getStandardKernelsForProvider(provider: string): Promise {