Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts
5255 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, toLocalResource } 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';33import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';34import { IPathService } from '../../../services/path/common/pathService.js';35import { isAbsolute } from '../../../../base/common/path.js';3637export interface NotebookEditorInputOptions {38startDirty?: boolean;39/**40* backupId for webview41*/42_backupId?: string;43_workingCopy?: IWorkingCopyIdentifier;44}4546export class NotebookEditorInput extends AbstractResourceEditorInput {4748static getOrCreate(instantiationService: IInstantiationService, resource: URI, preferredResource: URI | undefined, viewType: string, options: NotebookEditorInputOptions = {}) {49const editor = instantiationService.createInstance(NotebookEditorInput, resource, preferredResource, viewType, options);50if (preferredResource) {51editor.setPreferredResource(preferredResource);52}53return editor;54}5556static readonly ID: string = 'workbench.input.notebook';5758protected editorModelReference: IReference<IResolvedNotebookEditorModel> | null = null;59private _sideLoadedListener: IDisposable;60private _defaultDirtyState: boolean = false;6162constructor(63resource: URI,64preferredResource: URI | undefined,65public readonly viewType: string,66public readonly options: NotebookEditorInputOptions,67@INotebookService private readonly _notebookService: INotebookService,68@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,69@IFileDialogService private readonly _fileDialogService: IFileDialogService,70@ILabelService labelService: ILabelService,71@IFileService fileService: IFileService,72@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,73@IExtensionService extensionService: IExtensionService,74@IEditorService editorService: IEditorService,75@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,76@ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService,77@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,78@IPathService private readonly pathService: IPathService79) {80super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService);81this._defaultDirtyState = !!options.startDirty;8283// Automatically resolve this input when the "wanted" model comes to life via84// some other way. This happens only once per input and resolve disposes85// this listener86this._sideLoadedListener = _notebookService.onDidAddNotebookDocument(e => {87if (e.viewType === this.viewType && e.uri.toString() === this.resource.toString()) {88this.resolve().catch(onUnexpectedError);89}90});9192this._register(extensionService.onWillStop(e => {93if (!e.auto && !this.isDirty()) {94return;95}9697const reason = e.auto98? localize('vetoAutoExtHostRestart', "An extension provided notebook for '{0}' is still open that would close otherwise.", this.getName())99: localize('vetoExtHostRestart', "An extension provided notebook for '{0}' could not be saved.", this.getName());100101e.veto((async () => {102const editors = editorService.findEditors(this);103if (e.auto) {104return true;105}106if (editors.length > 0) {107const result = await editorService.save(editors[0]);108if (result.success) {109return false; // Don't Veto110}111}112return true; // Veto113})(), reason);114}));115}116117override dispose() {118this._sideLoadedListener.dispose();119this.editorModelReference?.dispose();120this.editorModelReference = null;121super.dispose();122}123124override get typeId(): string {125return NotebookEditorInput.ID;126}127128override get editorId(): string | undefined {129return this.viewType;130}131132override get capabilities(): EditorInputCapabilities {133let capabilities = EditorInputCapabilities.None;134135if (this.resource.scheme === Schemas.untitled) {136capabilities |= EditorInputCapabilities.Untitled;137}138139if (this.editorModelReference) {140if (this.editorModelReference.object.isReadonly()) {141capabilities |= EditorInputCapabilities.Readonly;142}143} else {144if (this.filesConfigurationService.isReadonly(this.resource)) {145capabilities |= EditorInputCapabilities.Readonly;146}147}148149if (!(capabilities & EditorInputCapabilities.Readonly)) {150capabilities |= EditorInputCapabilities.CanDropIntoEditor;151}152153return capabilities;154}155156override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {157if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) {158return super.getDescription(verbosity);159}160161return undefined; // no description for untitled notebooks without associated file path162}163164override isReadonly(): boolean | IMarkdownString {165if (!this.editorModelReference) {166return this.filesConfigurationService.isReadonly(this.resource);167}168return this.editorModelReference.object.isReadonly();169}170171override isDirty() {172if (!this.editorModelReference) {173return this._defaultDirtyState;174}175return this.editorModelReference.object.isDirty();176}177178override isSaving(): boolean {179const model = this.editorModelReference?.object;180if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) {181return false; // require the model to be dirty, file-backed and not in an error state182}183184// if a short auto save is configured, treat this as being saved185return this.filesConfigurationService.hasShortAutoSaveDelay(this);186}187188override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {189if (this.editorModelReference) {190191if (this.hasCapability(EditorInputCapabilities.Untitled)) {192return this.saveAs(group, options);193} else {194await this.editorModelReference.object.save(options);195}196197return this;198}199200return undefined;201}202203override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IUntypedEditorInput | undefined> {204if (!this.editorModelReference) {205return undefined;206}207208const provider = this._notebookService.getContributedNotebookType(this.viewType);209210if (!provider) {211return undefined;212}213214const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled)215? await this._suggestName(provider)216: this.editorModelReference.object.resource;217218let target: URI | undefined;219if (this.editorModelReference.object.hasAssociatedFilePath()) {220target = pathCandidate;221} else {222target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems);223if (!target) {224return undefined; // save cancelled225}226}227228if (!provider.matches(target)) {229const patterns = provider.selectors.map(pattern => {230if (typeof pattern === 'string') {231return pattern;232}233234if (glob.isRelativePattern(pattern)) {235return `${pattern} (base ${pattern.base})`;236}237238if (pattern.exclude) {239return `${pattern.include} (exclude: ${pattern.exclude})`;240} else {241return `${pattern.include}`;242}243244}).join(', ');245throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`);246}247248return await this.editorModelReference.object.saveAs(target);249}250251private async _suggestName(provider: NotebookProviderInfo) {252const resource = await this.ensureAbsolutePath(this.ensureProviderExtension(provider));253const remoteAuthority = this.environmentService.remoteAuthority;254return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme);255}256257private async ensureAbsolutePath(resource: URI): Promise<URI> {258if (resource.scheme !== Schemas.untitled || isAbsolute(resource.path)) {259return resource;260}261262const defaultFilePath = await this._fileDialogService.defaultFilePath();263return URI.joinPath(defaultFilePath, resource.path);264}265266private ensureProviderExtension(provider: NotebookProviderInfo) {267const firstSelector = provider.selectors[0];268let selectorStr = firstSelector && typeof firstSelector === 'string' ? firstSelector : undefined;269if (!selectorStr && firstSelector) {270const include = (firstSelector as { include?: string }).include;271if (typeof include === 'string') {272selectorStr = include;273}274}275276const resource = this.resource;277if (selectorStr) {278const matches = /^\*\.([A-Za-z_-]*)$/.exec(selectorStr);279if (matches && matches.length > 1) {280const fileExt = matches[1];281if (!resource.path.endsWith(fileExt)) {282return resource.with({ path: resource.path + '.' + fileExt });283}284}285}286287return resource;288}289290// called when users rename a notebook document291override async rename(group: GroupIdentifier, target: URI): Promise<IMoveResult | undefined> {292if (this.editorModelReference) {293return { editor: { resource: target }, options: { override: this.viewType } };294295}296return undefined;297}298299override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise<void> {300if (this.editorModelReference && this.editorModelReference.object.isDirty()) {301await this.editorModelReference.object.revert(options);302}303}304305override async resolve(_options?: IFileLimitedEditorInputOptions, perf?: NotebookPerfMarks): Promise<IResolvedNotebookEditorModel | null> {306if (!await this._notebookService.canResolve(this.viewType)) {307return null;308}309310perf?.mark('extensionActivated');311312// we are now loading the notebook and don't need to listen to313// "other" loading anymore314this._sideLoadedListener.dispose();315316if (!this.editorModelReference) {317const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false;318const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad, viewType: this.editorId });319if (this.editorModelReference) {320// Re-entrant, double resolve happened. Dispose the addition references and proceed321// with the truth.322ref.dispose();323return (<IReference<IResolvedNotebookEditorModel>>this.editorModelReference).object;324}325this.editorModelReference = ref;326if (this.isDisposed()) {327this.editorModelReference.dispose();328this.editorModelReference = null;329return null;330}331this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire()));332this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire()));333this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose()));334if (this.editorModelReference.object.isDirty()) {335this._onDidChangeDirty.fire();336}337} else {338this.editorModelReference.object.load({ limits: this.ensureLimits(_options) });339}340341if (this.options._backupId) {342const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType);343if (!(info instanceof SimpleNotebookProviderInfo)) {344throw new Error('CANNOT open file notebook with this provider');345}346347const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId })));348this.editorModelReference.object.notebook.applyEdits([349{350editType: CellEditType.Replace,351index: 0,352count: this.editorModelReference.object.notebook.length,353cells: data.cells354}355], true, undefined, () => undefined, undefined, false);356357if (this.options._workingCopy) {358this.options._backupId = undefined;359this.options._workingCopy = undefined;360this.options.startDirty = undefined;361}362}363364return this.editorModelReference.object;365}366367override toUntyped(): IResourceEditorInput {368return {369resource: this.resource,370options: {371override: this.viewType372}373};374}375376override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {377if (super.matches(otherInput)) {378return true;379}380if (otherInput instanceof NotebookEditorInput) {381return this.viewType === otherInput.viewType && isEqual(this.resource, otherInput.resource);382}383if (isResourceEditorInput(otherInput) && otherInput.resource.scheme === CellUri.scheme) {384return isEqual(this.resource, CellUri.parse(otherInput.resource)?.notebook);385}386return false;387}388}389390export interface ICompositeNotebookEditorInput {391readonly editorInputs: NotebookEditorInput[];392}393394export function isCompositeNotebookEditorInput(thing: unknown): thing is ICompositeNotebookEditorInput {395return !!thing396&& typeof thing === 'object'397&& Array.isArray((<ICompositeNotebookEditorInput>thing).editorInputs)398&& ((<ICompositeNotebookEditorInput>thing).editorInputs.every(input => input instanceof NotebookEditorInput));399}400401export function isNotebookEditorInput(thing: EditorInput | undefined): thing is NotebookEditorInput {402return !!thing403&& typeof thing === 'object'404&& thing.typeId === NotebookEditorInput.ID;405}406407408