Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as glob from '../../../../base/common/glob.js';6import { GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput, IFileLimitedEditorInputOptions, isResourceEditorInput } from '../../../common/editor.js';7import { EditorInput } from '../../../common/editor/editorInput.js';8import { INotebookService, SimpleNotebookProviderInfo } from './notebookService.js';9import { URI } from '../../../../base/common/uri.js';10import { isEqual, joinPath } from '../../../../base/common/resources.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';13import { INotebookEditorModelResolverService } from './notebookEditorModelResolverService.js';14import { IDisposable, IReference } from '../../../../base/common/lifecycle.js';15import { CellEditType, CellUri, IResolvedNotebookEditorModel } from './notebookCommon.js';16import { ILabelService } from '../../../../platform/label/common/label.js';17import { Schemas } from '../../../../base/common/network.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { AbstractResourceEditorInput } from '../../../common/editor/resourceEditorInput.js';20import { IResourceEditorInput } from '../../../../platform/editor/common/editor.js';21import { onUnexpectedError } from '../../../../base/common/errors.js';22import { VSBuffer } from '../../../../base/common/buffer.js';23import { IWorkingCopyIdentifier } from '../../../services/workingCopy/common/workingCopy.js';24import { NotebookProviderInfo } from './notebookProvider.js';25import { NotebookPerfMarks } from './notebookPerformance.js';26import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';27import { IExtensionService } from '../../../services/extensions/common/extensions.js';28import { localize } from '../../../../nls.js';29import { IEditorService } from '../../../services/editor/common/editorService.js';30import { IMarkdownString } from '../../../../base/common/htmlContent.js';31import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';32import { ICustomEditorLabelService } from '../../../services/editor/common/customEditorLabelService.js';3334export interface NotebookEditorInputOptions {35startDirty?: boolean;36/**37* backupId for webview38*/39_backupId?: string;40_workingCopy?: IWorkingCopyIdentifier;41}4243export class NotebookEditorInput extends AbstractResourceEditorInput {4445static getOrCreate(instantiationService: IInstantiationService, resource: URI, preferredResource: URI | undefined, viewType: string, options: NotebookEditorInputOptions = {}) {46const editor = instantiationService.createInstance(NotebookEditorInput, resource, preferredResource, viewType, options);47if (preferredResource) {48editor.setPreferredResource(preferredResource);49}50return editor;51}5253static readonly ID: string = 'workbench.input.notebook';5455protected editorModelReference: IReference<IResolvedNotebookEditorModel> | null = null;56private _sideLoadedListener: IDisposable;57private _defaultDirtyState: boolean = false;5859constructor(60resource: URI,61preferredResource: URI | undefined,62public readonly viewType: string,63public readonly options: NotebookEditorInputOptions,64@INotebookService private readonly _notebookService: INotebookService,65@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,66@IFileDialogService private readonly _fileDialogService: IFileDialogService,67@ILabelService labelService: ILabelService,68@IFileService fileService: IFileService,69@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,70@IExtensionService extensionService: IExtensionService,71@IEditorService editorService: IEditorService,72@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,73@ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService74) {75super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService);76this._defaultDirtyState = !!options.startDirty;7778// Automatically resolve this input when the "wanted" model comes to life via79// some other way. This happens only once per input and resolve disposes80// this listener81this._sideLoadedListener = _notebookService.onDidAddNotebookDocument(e => {82if (e.viewType === this.viewType && e.uri.toString() === this.resource.toString()) {83this.resolve().catch(onUnexpectedError);84}85});8687this._register(extensionService.onWillStop(e => {88if (!e.auto && !this.isDirty()) {89return;90}9192const reason = e.auto93? localize('vetoAutoExtHostRestart', "An extension provided notebook for '{0}' is still open that would close otherwise.", this.getName())94: localize('vetoExtHostRestart', "An extension provided notebook for '{0}' could not be saved.", this.getName());9596e.veto((async () => {97const editors = editorService.findEditors(this);98if (e.auto) {99return true;100}101if (editors.length > 0) {102const result = await editorService.save(editors[0]);103if (result.success) {104return false; // Don't Veto105}106}107return true; // Veto108})(), reason);109}));110}111112override dispose() {113this._sideLoadedListener.dispose();114this.editorModelReference?.dispose();115this.editorModelReference = null;116super.dispose();117}118119override get typeId(): string {120return NotebookEditorInput.ID;121}122123override get editorId(): string | undefined {124return this.viewType;125}126127override get capabilities(): EditorInputCapabilities {128let capabilities = EditorInputCapabilities.None;129130if (this.resource.scheme === Schemas.untitled) {131capabilities |= EditorInputCapabilities.Untitled;132}133134if (this.editorModelReference) {135if (this.editorModelReference.object.isReadonly()) {136capabilities |= EditorInputCapabilities.Readonly;137}138} else {139if (this.filesConfigurationService.isReadonly(this.resource)) {140capabilities |= EditorInputCapabilities.Readonly;141}142}143144if (!(capabilities & EditorInputCapabilities.Readonly)) {145capabilities |= EditorInputCapabilities.CanDropIntoEditor;146}147148return capabilities;149}150151override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {152if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) {153return super.getDescription(verbosity);154}155156return undefined; // no description for untitled notebooks without associated file path157}158159override isReadonly(): boolean | IMarkdownString {160if (!this.editorModelReference) {161return this.filesConfigurationService.isReadonly(this.resource);162}163return this.editorModelReference.object.isReadonly();164}165166override isDirty() {167if (!this.editorModelReference) {168return this._defaultDirtyState;169}170return this.editorModelReference.object.isDirty();171}172173override isSaving(): boolean {174const model = this.editorModelReference?.object;175if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) {176return false; // require the model to be dirty, file-backed and not in an error state177}178179// if a short auto save is configured, treat this as being saved180return this.filesConfigurationService.hasShortAutoSaveDelay(this);181}182183override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {184if (this.editorModelReference) {185186if (this.hasCapability(EditorInputCapabilities.Untitled)) {187return this.saveAs(group, options);188} else {189await this.editorModelReference.object.save(options);190}191192return this;193}194195return undefined;196}197198override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IUntypedEditorInput | undefined> {199if (!this.editorModelReference) {200return undefined;201}202203const provider = this._notebookService.getContributedNotebookType(this.viewType);204205if (!provider) {206return undefined;207}208209const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this.editorModelReference.object.resource;210let target: URI | undefined;211if (this.editorModelReference.object.hasAssociatedFilePath()) {212target = pathCandidate;213} else {214target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems);215if (!target) {216return undefined; // save cancelled217}218}219220if (!provider.matches(target)) {221const patterns = provider.selectors.map(pattern => {222if (typeof pattern === 'string') {223return pattern;224}225226if (glob.isRelativePattern(pattern)) {227return `${pattern} (base ${pattern.base})`;228}229230if (pattern.exclude) {231return `${pattern.include} (exclude: ${pattern.exclude})`;232} else {233return `${pattern.include}`;234}235236}).join(', ');237throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`);238}239240return await this.editorModelReference.object.saveAs(target);241}242243private async _suggestName(provider: NotebookProviderInfo, suggestedFilename: string) {244// guess file extensions245const firstSelector = provider.selectors[0];246let selectorStr = firstSelector && typeof firstSelector === 'string' ? firstSelector : undefined;247if (!selectorStr && firstSelector) {248const include = (firstSelector as { include?: string }).include;249if (typeof include === 'string') {250selectorStr = include;251}252}253254if (selectorStr) {255const matches = /^\*\.([A-Za-z_-]*)$/.exec(selectorStr);256if (matches && matches.length > 1) {257const fileExt = matches[1];258if (!suggestedFilename.endsWith(fileExt)) {259return joinPath(await this._fileDialogService.defaultFilePath(), suggestedFilename + '.' + fileExt);260}261}262}263264return joinPath(await this._fileDialogService.defaultFilePath(), suggestedFilename);265}266267// called when users rename a notebook document268override async rename(group: GroupIdentifier, target: URI): Promise<IMoveResult | undefined> {269if (this.editorModelReference) {270return { editor: { resource: target }, options: { override: this.viewType } };271272}273return undefined;274}275276override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise<void> {277if (this.editorModelReference && this.editorModelReference.object.isDirty()) {278await this.editorModelReference.object.revert(options);279}280}281282override async resolve(_options?: IFileLimitedEditorInputOptions, perf?: NotebookPerfMarks): Promise<IResolvedNotebookEditorModel | null> {283if (!await this._notebookService.canResolve(this.viewType)) {284return null;285}286287perf?.mark('extensionActivated');288289// we are now loading the notebook and don't need to listen to290// "other" loading anymore291this._sideLoadedListener.dispose();292293if (!this.editorModelReference) {294const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false;295const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad, viewType: this.editorId });296if (this.editorModelReference) {297// Re-entrant, double resolve happened. Dispose the addition references and proceed298// with the truth.299ref.dispose();300return (<IReference<IResolvedNotebookEditorModel>>this.editorModelReference).object;301}302this.editorModelReference = ref;303if (this.isDisposed()) {304this.editorModelReference.dispose();305this.editorModelReference = null;306return null;307}308this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire()));309this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire()));310this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose()));311if (this.editorModelReference.object.isDirty()) {312this._onDidChangeDirty.fire();313}314} else {315this.editorModelReference.object.load({ limits: this.ensureLimits(_options) });316}317318if (this.options._backupId) {319const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType);320if (!(info instanceof SimpleNotebookProviderInfo)) {321throw new Error('CANNOT open file notebook with this provider');322}323324const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId })));325this.editorModelReference.object.notebook.applyEdits([326{327editType: CellEditType.Replace,328index: 0,329count: this.editorModelReference.object.notebook.length,330cells: data.cells331}332], true, undefined, () => undefined, undefined, false);333334if (this.options._workingCopy) {335this.options._backupId = undefined;336this.options._workingCopy = undefined;337this.options.startDirty = undefined;338}339}340341return this.editorModelReference.object;342}343344override toUntyped(): IResourceEditorInput {345return {346resource: this.resource,347options: {348override: this.viewType349}350};351}352353override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {354if (super.matches(otherInput)) {355return true;356}357if (otherInput instanceof NotebookEditorInput) {358return this.viewType === otherInput.viewType && isEqual(this.resource, otherInput.resource);359}360if (isResourceEditorInput(otherInput) && otherInput.resource.scheme === CellUri.scheme) {361return isEqual(this.resource, CellUri.parse(otherInput.resource)?.notebook);362}363return false;364}365}366367export interface ICompositeNotebookEditorInput {368readonly editorInputs: NotebookEditorInput[];369}370371export function isCompositeNotebookEditorInput(thing: unknown): thing is ICompositeNotebookEditorInput {372return !!thing373&& typeof thing === 'object'374&& Array.isArray((<ICompositeNotebookEditorInput>thing).editorInputs)375&& ((<ICompositeNotebookEditorInput>thing).editorInputs.every(input => input instanceof NotebookEditorInput));376}377378export function isNotebookEditorInput(thing: EditorInput | undefined): thing is NotebookEditorInput {379return !!thing380&& typeof thing === 'object'381&& thing.typeId === NotebookEditorInput.ID;382}383384385