editors - reload error placeholder when file is coming back

This commit is contained in:
Benjamin Pasero 2022-04-08 09:13:06 +02:00
parent cffd590665
commit 897ab9567c
No known key found for this signature in database
GPG key ID: E6380CC4C8219E65
10 changed files with 67 additions and 68 deletions

View file

@ -84,12 +84,8 @@ export function toErrorMessage(error: any = null, verbose: boolean = false): str
} }
export interface IErrorOptions { export interface IErrorWithActions extends Error {
actions?: readonly IAction[]; actions: IAction[];
}
export interface IErrorWithActions {
actions?: readonly IAction[];
} }
export function isErrorWithActions(obj: unknown): obj is IErrorWithActions { export function isErrorWithActions(obj: unknown): obj is IErrorWithActions {
@ -98,12 +94,15 @@ export function isErrorWithActions(obj: unknown): obj is IErrorWithActions {
return candidate instanceof Error && Array.isArray(candidate.actions); return candidate instanceof Error && Array.isArray(candidate.actions);
} }
export function createErrorWithActions(message: string, options: IErrorOptions = Object.create(null)): Error & IErrorWithActions { export function createErrorWithActions(messageOrError: string | Error, actions: IAction[]): IErrorWithActions {
const result = new Error(message); let error: IErrorWithActions;
if (typeof messageOrError === 'string') {
if (options.actions) { error = new Error(messageOrError) as IErrorWithActions;
(result as IErrorWithActions).actions = options.actions; } else {
error = messageOrError as IErrorWithActions;
} }
return result; error.actions = actions;
return error;
} }

View file

@ -26,7 +26,7 @@ import { Link } from 'vs/platform/opener/browser/link';
import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel';
import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry';
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage';
export interface IEditorPlaceholderContents { export interface IEditorPlaceholderContents {
@ -96,7 +96,7 @@ export abstract class EditorPlaceholder extends EditorPane {
// Delegate to implementation for contents // Delegate to implementation for contents
const disposables = new DisposableStore(); const disposables = new DisposableStore();
const { icon, label, actions } = await this.getContents(input, options); const { icon, label, actions } = await this.getContents(input, options, disposables);
// Icon // Icon
const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); const iconContainer = container.appendChild($('.editor-placeholder-icon-container'));
@ -126,7 +126,7 @@ export abstract class EditorPlaceholder extends EditorPane {
return disposables; return disposables;
} }
protected abstract getContents(input: EditorInput, options: IEditorOptions | undefined): Promise<IEditorPlaceholderContents>; protected abstract getContents(input: EditorInput, options: IEditorOptions | undefined, disposables: DisposableStore): Promise<IEditorPlaceholderContents>;
override clearInput(): void { override clearInput(): void {
if (this.container) { if (this.container) {
@ -213,28 +213,32 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder {
@ITelemetryService telemetryService: ITelemetryService, @ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService, @IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService @IInstantiationService instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService
) { ) {
super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService);
} }
protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions): Promise<IEditorPlaceholderContents> { protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise<IEditorPlaceholderContents> {
const resource = input.resource;
const group = this.group; const group = this.group;
const error = options.error;
const isFileNotFound = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND;
// Error Label // Error Label
let label: string; let label: string;
if ((<FileOperationError>options.error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { if (isFileNotFound) {
label = localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); label = localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found.");
} else if (options.error) { } else if (error) {
label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", toErrorMessage(options.error)); label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", toErrorMessage(error));
} else { } else {
label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error.");
} }
// Actions // Actions
let actions: IEditorPlaceholderContentsAction[] | undefined = undefined; let actions: IEditorPlaceholderContentsAction[] | undefined = undefined;
if (isErrorWithActions(options.error)) { if (isErrorWithActions(error) && error.actions.length > 0) {
actions = options.error.actions?.map(action => { actions = error.actions.map(action => {
return { return {
label: action.label, label: action.label,
run: () => action.run() run: () => action.run()
@ -244,11 +248,20 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder {
actions = [ actions = [
{ {
label: localize('retry', "Try Again"), label: localize('retry', "Try Again"),
run: () => group.openEditor(input, { ...this.options, source: EditorOpenSource.USER /* explicit user gesture */ }) run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ })
} }
]; ];
} }
// Auto-reload when file is added
if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) {
disposables.add(this.fileService.onDidFilesChange(e => {
if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) {
group.openEditor(input, options);
}
}));
}
return { icon: '$(error)', label, actions: actions ?? [] }; return { icon: '$(error)', label, actions: actions ?? [] };
} }
} }

View file

@ -15,7 +15,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { withUndefinedAsNull } from 'vs/base/common/types'; import { withUndefinedAsNull } from 'vs/base/common/types';
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
import { createErrorWithActions } from 'vs/base/common/errorMessage';
import { IViewsService } from 'vs/workbench/common/views'; import { IViewsService } from 'vs/workbench/common/views';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
@ -158,7 +157,7 @@ export class DebugTaskRunner {
const errorMessage = typeof taskId === 'string' const errorMessage = typeof taskId === 'string'
? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId) ? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId)
: nls.localize('DebugTaskNotFound', "Could not find the specified task."); : nls.localize('DebugTaskNotFound', "Could not find the specified task.");
return Promise.reject(createErrorWithActions(errorMessage)); return Promise.reject(new Error(errorMessage));
} }
// If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340

View file

@ -6,7 +6,7 @@
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import * as objects from 'vs/base/common/objects'; import * as objects from 'vs/base/common/objects';
import { Action } from 'vs/base/common/actions'; import { toAction } from 'vs/base/common/actions';
import * as errors from 'vs/base/common/errors'; import * as errors from 'vs/base/common/errors';
import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { createErrorWithActions } from 'vs/base/common/errorMessage';
import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils';
@ -747,11 +747,7 @@ export class RawDebugSession implements IDisposable {
const uri = URI.parse(url); const uri = URI.parse(url);
// Use a suffixed id if uri invokes a command, so default 'Open launch.json' command is suppressed on dialog // Use a suffixed id if uri invokes a command, so default 'Open launch.json' command is suppressed on dialog
const actionId = uri.scheme === Schemas.command ? 'debug.moreInfo.command' : 'debug.moreInfo'; const actionId = uri.scheme === Schemas.command ? 'debug.moreInfo.command' : 'debug.moreInfo';
return createErrorWithActions(userMessage, { return createErrorWithActions(userMessage, [toAction({ id: actionId, label, run: () => this.openerService.open(uri, { allowCommands: true }) })]);
actions: [new Action(actionId, label, undefined, true, async () => {
this.openerService.open(uri, { allowCommands: true });
})]
});
} }
if (showErrors && error && error.format && error.showUser) { if (showErrors && error && error.format && error.showUser) {
this.notificationService.error(userMessage); this.notificationService.error(userMessage);

View file

@ -719,11 +719,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
const message = err && err.message || ''; const message = err && err.message || '';
if (/ECONNREFUSED/.test(message)) { if (/ECONNREFUSED/.test(message)) {
const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [
actions: [ new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings())
new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) ]);
]
});
this.notificationService.error(error); this.notificationService.error(error);
return; return;

View file

@ -972,11 +972,9 @@ export class ExtensionsListView extends ViewPane {
const message = err && err.message || ''; const message = err && err.message || '';
if (/ECONNREFUSED/.test(message)) { if (/ECONNREFUSED/.test(message)) {
const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [
actions: [ new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings())
new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) ]);
]
});
this.notificationService.error(error); this.notificationService.error(error);
return; return;

View file

@ -26,7 +26,7 @@ import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IErrorWithActions } from 'vs/base/common/errorMessage'; import { createErrorWithActions } from 'vs/base/common/errorMessage';
import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files';
@ -187,8 +187,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> {
// Offer to create a file from the error if we have a file not found and the name is valid // Offer to create a file from the error if we have a file not found and the name is valid
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && await this.pathService.hasValidBasename(input.preferredResource)) { if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && await this.pathService.hasValidBasename(input.preferredResource)) {
const fileNotFoundError: FileOperationError & IErrorWithActions = new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND); const fileNotFoundError = createErrorWithActions(new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND), [
fileNotFoundError.actions = [
toAction({ toAction({
id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => {
await this.textFileService.create([{ resource: input.preferredResource }]); await this.textFileService.create([{ resource: input.preferredResource }]);
@ -201,7 +200,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> {
}); });
} }
}) })
]; ]);
throw fileNotFoundError; throw fileNotFoundError;
} }

View file

@ -62,24 +62,22 @@ export class NativeTextFileEditor extends TextFileEditor {
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) {
const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB);
throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), { throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), [
actions: [ toAction({
toAction({ id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => {
id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { return this.nativeHostService.relaunch({
return this.nativeHostService.relaunch({ addArgs: [
addArgs: [ `--max-memory=${memoryLimit}`
`--max-memory=${memoryLimit}` ]
] });
}); }
} }),
}), toAction({
toAction({ id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => {
id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' });
return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' }); }
} }),
}), ]);
]
});
} }
// Fallback to handling in super type // Fallback to handling in super type

View file

@ -7,7 +7,7 @@ import * as DOM from 'vs/base/browser/dom';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, toAction } from 'vs/base/common/actions'; import { IAction, toAction } from 'vs/base/common/actions';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IErrorWithActions } from 'vs/base/common/errorMessage'; import { createErrorWithActions } from 'vs/base/common/errorMessage';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { extname, isEqual } from 'vs/base/common/resources'; import { extname, isEqual } from 'vs/base/common/resources';
@ -289,8 +289,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti
} }
} }
} catch (e) { } catch (e) {
const error: Error & IErrorWithActions = e instanceof Error ? e : new Error(e.message); const error = createErrorWithActions(e instanceof Error ? e : new Error(e.message), [
error.actions = [
toAction({ toAction({
id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => { id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => {
const activeEditorPane = this._editorService.activeEditorPane; const activeEditorPane = this._editorService.activeEditorPane;
@ -317,7 +316,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti
return; return;
} }
}) })
]; ]);
throw error; throw error;
} }

View file

@ -127,7 +127,7 @@ suite('Notifications', () => {
assert.strictEqual(called, 1); assert.strictEqual(called, 1);
// Error with Action // Error with Action
let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!;
assert.strictEqual(item7.actions!.primary!.length, 1); assert.strictEqual(item7.actions!.primary!.length, 1);
// Filter // Filter