From 8140f5daf061b92688ccc67b4f3e69f891f040dc Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 11 Apr 2023 12:19:15 -0700 Subject: [PATCH 1/6] Cache view type provider for missing extension restore. --- .../notebook/browser/notebookEditor.ts | 30 +++++++++++++++++-- .../browser/services/notebookServiceImpl.ts | 26 +++++++++++++++- .../notebook/common/notebookService.ts | 2 ++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index b9d6cb7ad0e78..99e5356f74a2c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -24,7 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Selection } from 'vs/editor/common/core/selection'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, createEditorOpenError } from 'vs/workbench/common/editor'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { INotebookEditorOptions, INotebookEditorPane, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -39,6 +39,8 @@ import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorD import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -79,6 +81,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @IFileService private readonly _fileService: IFileService, @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, @IEditorProgressService private readonly _editorProgressService: IEditorProgressService, + @INotebookService private readonly _notebookService: INotebookService, ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); @@ -226,7 +229,26 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } if (model === null) { - throw new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)); + const knownProvider = this._notebookService.getViewTypeProvider(input.viewType); + + if (knownProvider) { + throw createEditorOpenError(new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)), [ + toAction({ + id: 'workbench.notebook.action.installMissing', label: localize('notebookOpenInstallMissingViewType', "Install extension for '{0}'", input.viewType), run: async () => { + const d = this._notebookService.onAddViewType(viewType => { + if (viewType === input.viewType) { + // serializer is registered, try to open again + this._editorService.openEditor({ resource: input.resource }); + d.dispose(); + } + }); + await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run(); + } + }) + ], { allowDialog: true }); + } else { + throw new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)); + } } this._widgetDisposableStore.add(model.notebook.onDidChangeContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); @@ -257,6 +279,10 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._handlePerfMark(perf, input); } catch (e) { console.warn(e); + if (isEditorOpenError(e)) { + throw e; + } + const error = createEditorOpenError(e instanceof Error ? e : new Error((e ? e.message : '')), [ toAction({ id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index c20d99fc33dfd..20e4f6741fc82 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/browser'; +import { runWhenIdle } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; @@ -22,7 +23,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { Memento } from 'vs/workbench/common/memento'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { INotebookEditorContribution, notebookPreloadExtensionPoint, notebookRendererExtensionPoint, notebooksExtensionPoint } from 'vs/workbench/contrib/notebook/browser/notebookExtensionPoint'; import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/common/notebookDiffEditorInput'; @@ -413,6 +414,9 @@ class ModelData implements IDisposable { export class NotebookService extends Disposable implements INotebookService { declare readonly _serviceBrand: undefined; + private static _storageNotebookViewTypeProvider = 'notebook.viewTypeProvider'; + private readonly _memento: Memento; + private readonly _viewTypeCache: MementoObject; private readonly _notebookProviders = new Map(); private _notebookProviderInfoStore: NotebookProviderInfoStore | undefined = undefined; @@ -462,6 +466,7 @@ export class NotebookService extends Disposable implements INotebookService { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); @@ -583,6 +588,9 @@ export class NotebookService extends Disposable implements INotebookService { }; this._register(this._codeEditorService.onDecorationTypeRegistered(onDidAddDecorationType)); this._codeEditorService.listDecorationTypes().forEach(onDidAddDecorationType); + + this._memento = new Memento(NotebookService._storageNotebookViewTypeProvider, this._storageService); + this._viewTypeCache = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); } @@ -652,6 +660,8 @@ export class NotebookService extends Disposable implements INotebookService { registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable { this.notebookProviderInfoStore.get(viewType)?.update({ options: serializer.options }); + this._viewTypeCache[viewType] = extensionData.id.value; + this._persistMementos(); return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); } @@ -668,6 +678,20 @@ export class NotebookService extends Disposable implements INotebookService { return result; } + + private _persistSoonHandle?: IDisposable; + + private _persistMementos(): void { + this._persistSoonHandle?.dispose(); + this._persistSoonHandle = runWhenIdle(() => { + this._memento.saveMemento(); + }, 100); + } + + getViewTypeProvider(viewType: string): string | undefined { + return this._viewTypeCache[viewType]; + } + getRendererInfo(rendererId: string): INotebookRendererInfo | undefined { return this._notebookRenderersInfoStore.get(rendererId); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 98fd58e1d53b7..bfa3776bed17e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -48,6 +48,7 @@ export interface INotebookService { readonly _serviceBrand: undefined; canResolve(viewType: string): Promise; + readonly onAddViewType: Event; readonly onWillRemoveViewType: Event; readonly onDidChangeOutputRenderers: Event; readonly onWillAddNotebookDocument: Event; @@ -61,6 +62,7 @@ export interface INotebookService { getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[]; + getViewTypeProvider(viewType: string): string | undefined; getRendererInfo(id: string): INotebookRendererInfo | undefined; getRenderers(): INotebookRendererInfo[]; From c4cbdaecf01620c4bcf02f55bc1fa142cd35d767 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 11 Apr 2023 15:27:59 -0700 Subject: [PATCH 2/6] Re #179224. Customize action for save error. --- .../browser/services/notebookServiceImpl.ts | 16 +++++++++++++++- .../workingCopy/common/storedFileWorkingCopy.ts | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 20e4f6741fc82..1556bcee01677 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { toAction } from 'vs/base/common/actions'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PixelRatio } from 'vs/base/browser/browser'; import { runWhenIdle } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -39,6 +42,7 @@ import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } fro import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; export class NotebookProviderInfoStore extends Disposable { @@ -668,7 +672,17 @@ export class NotebookService extends Disposable implements INotebookService { async withNotebookDataProvider(viewType: string): Promise { const selected = this.notebookProviderInfoStore.get(viewType); if (!selected) { - throw new Error(`UNKNOWN notebook type '${viewType}'`); + const knownProvider = this.getViewTypeProvider(viewType); + + const actions = knownProvider ? [ + toAction({ + id: 'workbench.notebook.action.installMissingViewType', label: localize('notebookOpenInstallMissingViewType', "Install extension for '{0}'", viewType), run: async () => { + await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run(); + } + }) + ] : []; + + throw createErrorWithActions(`UNKNOWN notebook type '${viewType}'`, actions); } await this.canResolve(selected.id); const result = this._notebookProviders.get(selected.id); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 0f82e5ae635c2..e5606a0e39083 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -20,7 +20,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { hash } from 'vs/base/common/hash'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; import { IAction, toAction } from 'vs/base/common/actions'; import { isWindows } from 'vs/base/common/platform'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; @@ -1069,6 +1069,20 @@ export class StoredFileWorkingCopy extend const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; const canSaveElevated = this.elevatedFileService.isSupported(this.resource); + if (isErrorWithActions(error)) { + primaryActions.push(...error.actions.map(action => { + return toAction({ + id: action.id, + label: action.label, + run: () => { + const result = action.run(); + if (result instanceof Promise) { + } + } + }); + })); + } + // Save Elevated if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { primaryActions.push(toAction({ From e1a4233816f7741859297ee9824074bc75750f96 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 Apr 2023 07:09:43 +0200 Subject: [PATCH 3/6] polish --- .../workingCopy/common/storedFileWorkingCopy.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index e5606a0e39083..8cdf0d3777166 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -1069,18 +1069,9 @@ export class StoredFileWorkingCopy extend const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; const canSaveElevated = this.elevatedFileService.isSupported(this.resource); + // Error with Actions if (isErrorWithActions(error)) { - primaryActions.push(...error.actions.map(action => { - return toAction({ - id: action.id, - label: action.label, - run: () => { - const result = action.run(); - if (result instanceof Promise) { - } - } - }); - })); + primaryActions.push(...error.actions); } // Save Elevated From 0083a00dcc625024de2ea7494efb53623885d4a8 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 Apr 2023 10:48:15 -0700 Subject: [PATCH 4/6] No delay for persisting view type memento --- .../notebook/browser/services/notebookServiceImpl.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 1556bcee01677..22851ba85fcd0 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { toAction } from 'vs/base/common/actions'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PixelRatio } from 'vs/base/browser/browser'; -import { runWhenIdle } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; @@ -693,13 +692,8 @@ export class NotebookService extends Disposable implements INotebookService { } - private _persistSoonHandle?: IDisposable; - private _persistMementos(): void { - this._persistSoonHandle?.dispose(); - this._persistSoonHandle = runWhenIdle(() => { - this._memento.saveMemento(); - }, 100); + this._memento.saveMemento(); } getViewTypeProvider(viewType: string): string | undefined { From 23c1efbd15e64ec19fa3b7520e0cdbaea8d8c8fd Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 Apr 2023 10:50:19 -0700 Subject: [PATCH 5/6] Use log service instead of console --- src/vs/workbench/contrib/notebook/browser/notebookEditor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index b31cf0f0c70f6..b9d0524afcffe 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -45,6 +45,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { streamToBuffer } from 'vs/base/common/buffer'; +import { ILogService } from 'vs/platform/log/common/log'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -88,6 +89,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @INotebookService private readonly _notebookService: INotebookService, @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, + @ILogService private readonly logService: ILogService ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); @@ -306,7 +308,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._handlePerfMark(perf, input); } catch (e) { - console.warn(e); + this.logService.warn('NotebookEditorWidget#setInput failed', e); if (isEditorOpenError(e)) { throw e; } From e4286fc171f3d07728a90162e3973b677fce1eb8 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 Apr 2023 11:30:45 -0700 Subject: [PATCH 6/6] dispose listener if extension installation/enablement fails --- .../contrib/notebook/browser/notebookEditor.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index b9d0524afcffe..a79fcc32540fb 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -256,10 +256,15 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { }); const extensionInfo = this._extensionsWorkbenchService.local.find(e => e.identifier.id === knownProvider); - if (extensionInfo) { - await this._extensionsWorkbenchService.setEnablement(extensionInfo, extensionInfo.enablementState === EnablementState.DisabledWorkspace ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally); - } else { - await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run(); + try { + if (extensionInfo) { + await this._extensionsWorkbenchService.setEnablement(extensionInfo, extensionInfo.enablementState === EnablementState.DisabledWorkspace ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally); + } else { + await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run(); + } + } catch (ex) { + this.logService.error(`Failed to install or enable extension ${knownProvider}`, ex); + d.dispose(); } } }),