diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index f9d8d6a82db..a067ba65a92 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -18,6 +18,10 @@ async function openRandomNotebookDocument() { return vscode.workspace.openNotebookDocument(uri); } +async function openUntitledNotebookDocument(data?: vscode.NotebookData) { + return vscode.workspace.openNotebookDocument('notebookCoreTest', data); +} + export async function saveAllFilesAndCloseAll() { await saveAllEditors(); await closeAllEditors(); @@ -188,6 +192,27 @@ const apiTestSerializer: vscode.NotebookSerializer = { assert.strictEqual(vscode.window.activeNotebookEditor!.notebook.uri.toString(), document.uri.toString()); }); + test('Opening an utitled notebook without content will only open the editor when shown.', async function () { + const document = await openUntitledNotebookDocument(); + + assert.strictEqual(vscode.window.activeNotebookEditor, undefined); + + // opening a cell-uri opens a notebook editor + await vscode.window.showNotebookDocument(document); + + assert.strictEqual(!!vscode.window.activeNotebookEditor, true); + assert.strictEqual(vscode.window.activeNotebookEditor!.notebook.uri.toString(), document.uri.toString()); + }); + + test('Opening an untitled notebook with content will open a dirty document.', async function () { + const language = 'python'; + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '', language); + const data = new vscode.NotebookData([cell]); + const doc = await vscode.workspace.openNotebookDocument('jupyter-notebook', data); + + assert.strictEqual(doc.isDirty, true); + }); + test('Cannot open notebook from cell-uri with vscode.open-command', async function () { const document = await openRandomNotebookDocument(); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ae38c2ebec3..cc1e493b864 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -278,6 +278,9 @@ const _allApiProposals = { notebookMime: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', }, + notebookReplDocument: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookReplDocument.d.ts', + }, notebookVariableProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index c18d0ac258a..73a5b5a7dba 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -125,30 +125,45 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS } } - async $tryCreateNotebook(options: { viewType: string; content?: NotebookDataDto }): Promise { - const ref = await this._notebookEditorModelResolverService.resolve({ untitledResource: undefined }, options.viewType); - - // untitled notebooks are disposed when they get saved. we should not hold a reference - // to such a disposed notebook and therefore dispose the reference as well - ref.object.notebook.onWillDispose(() => { - ref.dispose(); - }); - - // untitled notebooks are dirty by default - this._proxy.$acceptDirtyStateChanged(ref.object.resource, true); - - // apply content changes... slightly HACKY -> this triggers a change event if (options.content) { - const data = NotebookDto.fromNotebookDataDto(options.content); - ref.object.notebook.reset(data.cells, data.metadata, ref.object.notebook.transientOptions); + const ref = await this._notebookEditorModelResolverService.resolve({ untitledResource: undefined }, options.viewType); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + + // untitled notebooks with content are dirty by default + this._proxy.$acceptDirtyStateChanged(ref.object.resource, true); + + // apply content changes... slightly HACKY -> this triggers a change event + if (options.content) { + const data = NotebookDto.fromNotebookDataDto(options.content); + ref.object.notebook.reset(data.cells, data.metadata, ref.object.notebook.transientOptions); + } + return ref.object.notebook.uri; + } else { + // If we aren't adding content, we don't need to resolve the full editor model yet. + // This will allow us to adjust settings when the editor is opened, e.g. scratchpad + const notebook = await this._notebookEditorModelResolverService.createUntitledNotebookTextModel(options.viewType); + return notebook.uri; } - return ref.object.resource; } async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri, undefined); + + if (uriComponents.scheme === 'untitled') { + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + } + this._modelReferenceCollection.add(uri, ref); return uri; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 5b707fc865e..3f7c3bcd2f8 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -145,8 +145,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return result; } - - private static _convertNotebookRegistrationData(extension: IExtensionDescription, registration: vscode.NotebookRegistrationData | undefined): INotebookContributionData | undefined { if (!registration) { return; @@ -205,13 +203,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return assertIsDefined(document?.apiNotebook); } - - async showNotebookDocument(notebookOrUri: vscode.NotebookDocument | URI, options?: vscode.NotebookDocumentShowOptions): Promise { - - if (URI.isUri(notebookOrUri)) { - notebookOrUri = await this.openNotebookDocument(notebookOrUri); - } - + async showNotebookDocument(notebook: vscode.NotebookDocument, options?: vscode.NotebookDocumentShowOptions): Promise { let resolvedOptions: INotebookDocumentShowOptions; if (typeof options === 'object') { resolvedOptions = { @@ -222,11 +214,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }; } else { resolvedOptions = { - preserveFocus: false + preserveFocus: false, + pinned: true }; } - const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebookOrUri.uri, notebookOrUri.notebookType, resolvedOptions); + const viewType = options?.asRepl ? 'repl' : notebook.notebookType; + const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebook.uri, viewType, resolvedOptions); const editor = editorId && this._editors.get(editorId)?.apiEditor; if (editor) { @@ -234,9 +228,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } if (editorId) { - throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}" because another editor opened in the meantime.`); + throw new Error(`Could NOT open editor for "${notebook.uri.toString()}" because another editor opened in the meantime.`); } else { - throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}".`); + throw new Error(`Could NOT open editor for "${notebook.uri.toString()}".`); } } diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 7a488364abc..792bf51c6e5 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -98,7 +98,7 @@ export class InteractiveDocumentContribution extends Disposable implements IWork providerDisplayName: 'Interactive Notebook', displayName: 'Interactive', filenamePattern: ['*.interactive'], - priority: RegisteredEditorPriority.exclusive + priority: RegisteredEditorPriority.builtin })); } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 86a0b01c560..aca165f547c 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -28,7 +28,7 @@ import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/no import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/common/notebookDiffEditorInput'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookData, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, TransientOptions, NotebookExtensionDescription, INotebookStaticPreloadInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, NotebookExtensionDescription, INotebookStaticPreloadInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo as NotebookStaticPreloadInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; @@ -42,6 +42,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { INotebookDocument, INotebookDocumentService } from 'vs/workbench/services/notebook/common/notebookDocumentService'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import type { EditorInputWithOptions, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import { streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; export class NotebookProviderInfoStore extends Disposable { @@ -743,17 +744,28 @@ export class NotebookService extends Disposable implements INotebookService { // --- notebook documents: create, destory, retrieve, enumerate - createNotebookTextModel(viewType: string, uri: URI, data: NotebookData, transientOptions: TransientOptions): NotebookTextModel { + async createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise { if (this._models.has(uri)) { throw new Error(`notebook for ${uri} already exists`); } - const notebookModel = this._instantiationService.createInstance(NotebookTextModel, viewType, uri, data.cells, data.metadata, transientOptions); + + const info = await this.withNotebookDataProvider(viewType); + if (!(info instanceof SimpleNotebookProviderInfo)) { + throw new Error('CANNOT open file notebook with this provider'); + } + + + const bytes = stream ? await streamToBuffer(stream) : VSBuffer.fromByteArray([]); + const data = await info.serializer.dataToNotebook(bytes); + + + const notebookModel = this._instantiationService.createInstance(NotebookTextModel, info.viewType, uri, data.cells, data.metadata, info.serializer.options); const modelData = new ModelData(notebookModel, this._onWillDisposeDocument.bind(this)); this._models.set(uri, modelData); this._notebookDocumentService.addNotebookDocument(modelData); this._onWillAddNotebookDocument.fire(notebookModel); this._onDidAddNotebookDocument.fire(notebookModel); - this._postDocumentOpenActivation(viewType); + this._postDocumentOpenActivation(info.viewType); return notebookModel; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 7a851b42f9c..b6868371e5e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -356,7 +356,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return true; } if (otherInput instanceof NotebookEditorInput) { - return this.viewType === otherInput.viewType && isEqual(this.resource, otherInput.resource); + return this.editorId === otherInput.editorId && isEqual(this.resource, otherInput.resource); } return false; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 90904bb4571..52bd9d3a7c6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -377,19 +377,9 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { - const info = await this._notebookService.withNotebookDataProvider(this._viewType); - if (!(info instanceof SimpleNotebookProviderInfo)) { - throw new Error('CANNOT open file notebook with this provider'); - } + const notebookModel = this._notebookService.getNotebookTextModel(resource) ?? + await this._notebookService.createNotebookTextModel(this._viewType, resource, stream); - const bytes = await streamToBuffer(stream); - const data = await info.serializer.dataToNotebook(bytes); - - if (token.isCancellationRequested) { - throw new CancellationError(); - } - - const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options); return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._notebookLogService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index dc3d26a0ca3..8231edfcdb9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; import { Event, IWaitUntil } from 'vs/base/common/event'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); @@ -49,6 +50,8 @@ export interface INotebookEditorModelResolverService { isDirty(resource: URI): boolean; + createUntitledNotebookTextModel(viewType: string): Promise; + resolve(resource: URI, viewType?: string, creationOptions?: NotebookEditorModelCreationOptions): Promise>; resolve(resource: IUntitledNotebookResource, viewType: string, creationOtions?: NotebookEditorModelCreationOptions): Promise>; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 1ca105ea97b..f34fbcae53a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -177,46 +177,36 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes return this._data.isDirty(resource); } - async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise>; - async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise>; - async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise> { - let resource: URI; - let hasAssociatedFilePath = false; - if (URI.isUri(arg0)) { - resource = arg0; - } else { - if (!arg0.untitledResource) { - const info = this._notebookService.getContributedNotebookType(assertIsDefined(viewType)); - if (!info) { - throw new Error('UNKNOWN view type: ' + viewType); - } + private createUntitledUri(notebookType: string) { + const info = this._notebookService.getContributedNotebookType(assertIsDefined(notebookType)); + if (!info) { + throw new Error('UNKNOWN notebook type: ' + notebookType); + } - const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; - for (let counter = 1; ; counter++) { - const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: viewType }); - if (!this._notebookService.getNotebookTextModel(candidate)) { - resource = candidate; - break; - } - } - } else if (arg0.untitledResource.scheme === Schemas.untitled) { - resource = arg0.untitledResource; - } else { - resource = arg0.untitledResource.with({ scheme: Schemas.untitled }); - hasAssociatedFilePath = true; + const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; + for (let counter = 1; ; counter++) { + const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: notebookType }); + if (!this._notebookService.getNotebookTextModel(candidate)) { + return candidate; } } + } - if (resource.scheme === CellUri.scheme) { - throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${resource.toString()}`); + private async validateResourceViewType(uri: URI | undefined, viewType: string | undefined) { + if (!uri && !viewType) { + throw new Error('Must provide at least one of resource or viewType'); } - resource = this._uriIdentService.asCanonicalUri(resource); + if (uri?.scheme === CellUri.scheme) { + throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${uri.toString()}`); + } - const existingViewType = this._notebookService.getNotebookTextModel(resource)?.viewType; + const resource = this._uriIdentService.asCanonicalUri(uri ?? this.createUntitledUri(viewType!)); + + const existingNotebook = this._notebookService.getNotebookTextModel(resource); if (!viewType) { - if (existingViewType) { - viewType = existingViewType; + if (existingNotebook) { + viewType = existingNotebook.viewType; } else { await this._extensionService.whenInstalledExtensionsRegistered(); const providers = this._notebookService.getContributedNotebookTypes(resource); @@ -230,9 +220,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes throw new Error(`Missing viewType for '${resource}'`); } - if (existingViewType && existingViewType !== viewType) { + if (existingNotebook && existingNotebook.viewType !== viewType) { - await this._onWillFailWithConflict.fireAsync({ resource, viewType }, CancellationToken.None); + await this._onWillFailWithConflict.fireAsync({ resource: resource, viewType }, CancellationToken.None); // check again, listener should have done cleanup const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType; @@ -240,8 +230,34 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); } } + return { resource, viewType }; + } - const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad); + public async createUntitledNotebookTextModel(viewType: string) { + const resource = this._uriIdentService.asCanonicalUri(this.createUntitledUri(viewType)); + + return (await this._notebookService.createNotebookTextModel(viewType, resource)); + } + + async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise>; + async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise>; + async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise> { + let resource: URI | undefined; + let hasAssociatedFilePath; + if (URI.isUri(arg0)) { + resource = arg0; + } else if (arg0.untitledResource) { + if (arg0.untitledResource.scheme === Schemas.untitled) { + resource = arg0.untitledResource; + } else { + resource = arg0.untitledResource.with({ scheme: Schemas.untitled }); + hasAssociatedFilePath = true; + } + } + + const validated = await this.validateResourceViewType(resource, viewType); + + const reference = this._data.acquire(validated.resource.toString(), validated.viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad); try { const model = await reference.object; return { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 50e49939035..0f7b65b847a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -12,7 +12,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFileStatWithMetadata, IWriteFileOptions } from 'vs/platform/files/common/files'; import { ITextQuery } from 'vs/workbench/services/search/common/search'; @@ -79,7 +79,7 @@ export interface INotebookService { updateMimePreferredRenderer(viewType: string, mimeType: string, rendererId: string, otherMimetypes: readonly string[]): void; saveMimeDisplayOrder(target: ConfigurationTarget): void; - createNotebookTextModel(viewType: string, uri: URI, data: NotebookData, transientOptions: TransientOptions): NotebookTextModel; + createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; getNotebookTextModels(): Iterable; listNotebookDocuments(): readonly NotebookTextModel[]; diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts index 618c875f813..9876f6eb28a 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -6,11 +6,11 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; -import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from 'vs/workbench/common/editor'; import { parse } from 'vs/base/common/marshalling'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; @@ -23,11 +23,7 @@ import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { extname, isEqual } from 'vs/base/common/resources'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { localize2 } from 'vs/nls'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; -import { Schemas } from 'vs/base/common/network'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; @@ -98,7 +94,6 @@ export class ReplDocumentContribution extends Disposable implements IWorkbenchCo constructor( @INotebookService notebookService: INotebookService, @IEditorResolverService editorResolverService: IEditorResolverService, - @IEditorService editorService: IEditorService, @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -106,16 +101,16 @@ export class ReplDocumentContribution extends Disposable implements IWorkbenchCo super(); editorResolverService.registerEditor( - `*.replNotebook`, + `*.ipynb`, { id: 'repl', label: 'repl Editor', - priority: RegisteredEditorPriority.option + priority: RegisteredEditorPriority.builtin }, { - canSupportResource: uri => - (uri.scheme === Schemas.untitled && extname(uri) === '.replNotebook') || - (uri.scheme === Schemas.vscodeNotebookCell && extname(uri) === '.replNotebook'), + // We want to support all notebook types which could have any file extension, + // so we just check if the resource corresponds to a notebook + canSupportResource: uri => notebookService.getNotebookTextModel(uri) !== undefined, singlePerResource: true }, { @@ -129,6 +124,9 @@ export class ReplDocumentContribution extends Disposable implements IWorkbenchCo ref.dispose(); }); return { editor: this.instantiationService.createInstance(ReplEditorInput, resource!), options }; + }, + createEditorInput: async ({ resource, options }) => { + return { editor: this.instantiationService.createInstance(ReplEditorInput, resource), options }; } } ); @@ -181,25 +179,6 @@ class ReplWindowWorkingCopyEditorHandler extends Disposable implements IWorkbenc registerWorkbenchContribution2(ReplWindowWorkingCopyEditorHandler.ID, ReplWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ReplDocumentContribution.ID, ReplDocumentContribution, WorkbenchPhase.BlockRestore); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'repl.newRepl', - title: localize2('repl.editor.open', 'New REPL Editor'), - category: 'Create', - }); - } - - async run(accessor: ServicesAccessor) { - const resource = URI.from({ scheme: Schemas.untitled, path: 'repl.replNotebook' }); - const editorInput: IUntypedEditorInput = { resource, options: { override: 'repl' } }; - - const editorService = accessor.get(IEditorService); - await editorService.openEditor(editorInput, 1); - } -}); - export async function executeReplInput( bulkEditService: IBulkEditService, historyService: IInteractiveHistoryService, diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts index bf82d590a26..52427e40cbf 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts @@ -5,6 +5,7 @@ import { IReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -12,9 +13,10 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -44,16 +46,21 @@ export class ReplEditorInput extends NotebookEditorInput { @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, @IInteractiveHistoryService public readonly historyService: IInteractiveHistoryService, @ITextModelService private readonly _textModelService: ITextModelService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IBulkEditService private readonly bulkEditService: IBulkEditService ) { super(resource, undefined, 'jupyter-notebook', {}, _notebookService, _notebookModelResolverService, _fileDialogService, labelService, fileService, filesConfigurationService, extensionService, editorService, textResourceConfigurationService, customEditorLabelService); - this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + this.isScratchpad = resource.scheme === 'untitled' && configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; } override get typeId(): string { return ReplEditorInput.ID; } + override get editorId(): string | undefined { + return 'repl'; + } + override getName() { return 'REPL'; } @@ -67,12 +74,59 @@ export class ReplEditorInput extends NotebookEditorInput { | scratchPad; } + private async ensureInputBoxCell(notebook: NotebookTextModel) { + let lastCell = notebook.cells[notebook.cells.length - 1]; + + if (!lastCell || lastCell.cellKind === CellKind.Markup || lastCell.getValue().trim() !== '') { + // ensure we have an empty cell at the end for the input box + await this.bulkEditService.apply([ + new ResourceNotebookCellEdit(notebook.uri, + { + editType: CellEditType.Replace, + index: notebook.cells.length, + count: 0, + cells: [{ + cellKind: CellKind.Code, + mime: undefined, + language: 'python', + source: '', + outputs: [], + metadata: {} + }] + } + ) + ]); + + // Or directly on the notebook? + // notebook.applyEdits([ + // { + // editType: CellEditType.Replace, + // index: notebook.cells.length, + // count: 0, + // cells: [ + // { + // cellKind: CellKind.Code, + // language: 'python', + // mime: undefined, + // outputs: [], + // source: '' + // } + // ] + // } + // ], true, undefined, () => undefined, undefined, false); + + lastCell = notebook.cells[notebook.cells.length - 1]; + } + + return lastCell; + } + async resolveInput(notebook: NotebookTextModel) { if (this.inputModelRef) { return this.inputModelRef.object.textEditorModel; } + const lastCell = await this.ensureInputBoxCell(notebook); - const lastCell = notebook.cells[notebook.cells.length - 1]; this.inputModelRef = await this._textModelService.createModelReference(lastCell.uri); return this.inputModelRef.object.textEditorModel; } diff --git a/src/vscode-dts/vscode.proposed.notebookReplDocument.d.ts b/src/vscode-dts/vscode.proposed.notebookReplDocument.d.ts new file mode 100644 index 00000000000..5330758f7ef --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookReplDocument.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface NotebookDocumentShowOptions { + /** + * The notebook should be opened in a REPL editor, + * where the last cell of the notebook is an input box and the rest are read-only. + */ + readonly asRepl?: boolean; + } +}