diff options
Diffstat (limited to 'src/vs/workbench/browser/parts/editor/editorPlaceholder.ts')
-rw-r--r-- | src/vs/workbench/browser/parts/editor/editorPlaceholder.ts | 245 |
1 files changed, 161 insertions, 84 deletions
diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index af524fb057f..0464f14265b 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,14 +5,15 @@ import 'vs/css!./media/editorplaceholder'; import { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, clearNode } from 'vs/base/browser/dom'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Dimension, size, clearNode, $ } from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -23,8 +24,29 @@ import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/edit import { computeEditorAriaLabel, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Link } from 'vs/platform/opener/browser/link'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; +import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +export interface IEditorPlaceholderContents { + icon: string; + label: string; + actions: IEditorPlaceholderContentsAction[]; +} + +export interface IEditorPlaceholderContentsAction { + label: string; + run: () => unknown; +} -abstract class EditorPlaceholderPane extends EditorPane { +export interface IErrorEditorPlaceholderOptions extends IEditorOptions { + error?: Error; +} + +export abstract class EditorPlaceholder extends EditorPane { private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; @@ -32,18 +54,14 @@ abstract class EditorPlaceholderPane extends EditorPane { constructor( id: string, - private readonly title: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(id, telemetryService, themeService, storageService); } - override getTitle(): string { - return this.title; - } - protected createEditor(parent: HTMLElement): void { // Container @@ -66,21 +84,43 @@ abstract class EditorPlaceholderPane extends EditorPane { } // Render Input - this.inputDisposable.value = this.renderInput(input); + this.inputDisposable.value = await this.renderInput(input, options); } - private renderInput(input: EditorInput): IDisposable { + private async renderInput(input: EditorInput, options: IEditorOptions | undefined): Promise<IDisposable> { const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); // Reset any previous contents clearNode(container); - // Update ARIA label - container.setAttribute('aria-label', computeEditorAriaLabel(input, undefined, this.group, undefined)); - - // Delegate to implementation + // Delegate to implementation for contents const disposables = new DisposableStore(); - this.renderBody(container, disposables); + const { icon, label, actions } = await this.getContents(input, options, disposables); + + // Icon + const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); + const iconWidget = new SimpleIconLabel(iconContainer); + iconWidget.text = icon; + + // Label + const labelContainer = container.appendChild($('.editor-placeholder-label-container')); + const labelWidget = document.createElement('span'); + labelWidget.textContent = label; + labelContainer.appendChild(labelWidget); + + // ARIA label + container.setAttribute('aria-label', `${computeEditorAriaLabel(input, undefined, this.group, undefined)}, ${label}`); + + // Actions + const actionsContainer = container.appendChild($('.editor-placeholder-actions-container')); + for (const action of actions) { + disposables.add(this.instantiationService.createInstance(Link, actionsContainer, { + label: action.label, + href: '' + }, { + opener: () => action.run() + })); + } // Adjust scrollbar scrollbar.scanDomNode(); @@ -88,7 +128,7 @@ abstract class EditorPlaceholderPane extends EditorPane { return disposables; } - protected abstract renderBody(container: HTMLElement, disposables: DisposableStore): void; + protected abstract getContents(input: EditorInput, options: IEditorOptions | undefined, disposables: DisposableStore): Promise<IEditorPlaceholderContents>; override clearInput(): void { if (this.container) { @@ -108,6 +148,9 @@ abstract class EditorPlaceholderPane extends EditorPane { // Adjust scrollbar scrollbar.scanDomNode(); + + // Toggle responsive class + container.classList.toggle('max-height-200px', dimension.height <= 200); } override focus(): void { @@ -123,11 +166,12 @@ abstract class EditorPlaceholderPane extends EditorPane { } } -export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { +export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; - static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL); + private static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -135,95 +179,128 @@ export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService ) { - super(WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? - localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : - localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."); + override getTitle(): string { + return WorkspaceTrustRequiredPlaceholderEditor.LABEL; + } - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('manageTrust', "Manage Workspace Trust"), - href: '' - }, { - opener: () => this.commandService.executeCommand('workbench.trust.manage') - })); + protected async getContents(): Promise<IEditorPlaceholderContents> { + return { + icon: '$(workspace-untrusted)', + label: isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? + localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : + localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."), + actions: [ + { + label: localize('manageTrust', "Manage Workspace Trust"), + run: () => this.commandService.executeCommand('workbench.trust.manage') + } + ] + }; } } -abstract class AbstractErrorEditor extends EditorPlaceholderPane { +export class ErrorPlaceholderEditor extends EditorPlaceholder { + + private static readonly ID = 'workbench.editors.errorEditor'; + private static readonly LABEL = localize('errorEditor', "Error Editor"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( - id: string, - label: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IFileService private readonly fileService: IFileService, + @IDialogService private readonly dialogService: IDialogService ) { - super(id, label, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected abstract getErrorMessage(): string; - - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = this.getErrorMessage(); - - // Offer to re-open + protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise<IEditorPlaceholderContents> { + const resource = input.resource; const group = this.group; - const input = this.input; - if (group && input) { - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('retry', "Try Again"), - href: '' - }, { - opener: () => group.openEditor(input, { ...this.options, source: EditorOpenSource.USER /* explicit user gesture */ }) - })); + const error = options.error; + const isFileNotFound = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + + // Error Label + let label: string; + if (isFileNotFound) { + label = localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + } else if (error) { + label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", toErrorMessage(error)); + } else { + label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); } - } -} - -export class UnknownErrorEditor extends AbstractErrorEditor { - static readonly ID = 'workbench.editors.unknownErrorEditor'; - static readonly LABEL = localize('unknownErrorEditor', "Unknown Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnknownErrorEditor, UnknownErrorEditor.ID, UnknownErrorEditor.LABEL); + // Actions + let actions: IEditorPlaceholderContentsAction[] | undefined = undefined; + if (isErrorWithActions(error) && error.actions.length > 0) { + actions = error.actions.map(action => { + return { + label: action.label, + run: () => { + const result = action.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + } + }; + }); + } else if (group) { + actions = [ + { + label: localize('retry', "Try Again"), + run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + } + ]; + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnknownErrorEditor.ID, UnknownErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); - } + // 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); + } + })); + } - protected override getErrorMessage(): string { - return localize('unknownErrorEditorText', "The editor could not be opened due to an unexpected error."); + return { icon: '$(error)', label, actions: actions ?? [] }; } } -export class UnavailableResourceErrorEditor extends AbstractErrorEditor { +registerThemingParticipant((theme, collector) => { - static readonly ID = 'workbench.editors.unavailableResourceErrorEditor'; - static readonly LABEL = localize('unavailableResourceErrorEditor', "Unavailable Resource Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnavailableResourceErrorEditor, UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL); + // Editor Placeholder Error Icon + const editorErrorIconForegroundColor = theme.getColor(editorErrorForeground); + if (editorErrorIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.error.cssSelector} { + color: ${editorErrorIconForegroundColor}; + }`); + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); + // Editor Placeholder Warning Icon + const editorWarningIconForegroundColor = theme.getColor(editorWarningForeground); + if (editorWarningIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.warning.cssSelector} { + color: ${editorWarningIconForegroundColor}; + }`); } - protected override getErrorMessage(): string { - return localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + // Editor Placeholder Info/Trust Icon + const editorInfoIconForegroundColor = theme.getColor(editorInfoForeground); + if (editorInfoIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.info.cssSelector}, + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.workspaceUntrusted.cssSelector} { + color: ${editorInfoIconForegroundColor}; + }`); } -} +}); |