diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 52b38930401..45937179850 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions } from 'vs/platform/opener/common/opener'; +import { EditorOpenContext } from 'vs/platform/editor/common/editor'; export class OpenerService extends Disposable implements IOpenerService { @@ -128,7 +129,7 @@ export class OpenerService extends Disposable implements IOpenerService { } return this._editorService.openCodeEditor( - { resource, options: { selection, } }, + { resource, options: { selection, context: options && options.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, this._editorService.getFocusedCodeEditor(), options && options.openToSide ).then(() => true); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 2094059be47..1f0551e6d10 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -279,10 +279,10 @@ class LinkDetector implements editorCommon.IEditorContribution { if (!occurrence) { return; } - this.openLinkOccurrence(occurrence, mouseEvent.hasSideBySideModifier); + this.openLinkOccurrence(occurrence, mouseEvent.hasSideBySideModifier, true /* from user gesture */); } - public openLinkOccurrence(occurrence: LinkOccurrence, openToSide: boolean): void { + public openLinkOccurrence(occurrence: LinkOccurrence, openToSide: boolean, fromUserGesture = false): void { if (!this.openerService) { return; @@ -292,7 +292,7 @@ class LinkDetector implements editorCommon.IEditorContribution { link.resolve(CancellationToken.None).then(uri => { // open the uri - return this.openerService.open(uri, { openToSide }); + return this.openerService.open(uri, { openToSide, fromUserGesture }); }, err => { const messageOrError = diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 09d22bda225..3185e8c9f96 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -106,6 +106,21 @@ export enum EditorActivation { PRESERVE } +export enum EditorOpenContext { + + /** + * Default: the editor is opening via a programmatic call + * to the editor service API. + */ + API, + + /** + * Indicates that a user action triggered the opening, e.g. + * via mouse or keyboard use. + */ + USER +} + export interface IEditorOptions { /** @@ -179,6 +194,18 @@ export interface IEditorOptions { * Does not use editor overrides while opening the editor */ readonly ignoreOverrides?: boolean; + + /** + * A optional hint to signal in which context the editor opens. + * + * If configured to be `EditorOpenContext.USER`, this hint can be + * used in various places to control the experience. For example, + * if the editor to open fails with an error, a notification could + * inform about this in a modal dialog. If the editor opened through + * some background task, the notification would show in the background, + * not as a modal dialog. + */ + readonly context?: EditorOpenContext; } export interface ITextEditorSelection { diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index fa1ede46ab1..70377fcb3da 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -9,13 +9,27 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; export const IOpenerService = createDecorator('openerService'); -type OpenToSideOptions = { readonly openToSide?: boolean }; +type OpenInternalOptions = { + + /** + * Signals that the intent is to open an editor to the side + * of the currently active editor. + */ + readonly openToSide?: boolean; + + /** + * Signals that the editor to open was triggered through a user + * action, such as keyboard or mouse usage. + */ + readonly fromUserGesture?: boolean; +}; + type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean }; -export type OpenOptions = OpenToSideOptions & OpenExternalOptions; +export type OpenOptions = OpenInternalOptions & OpenExternalOptions; export interface IOpener { - open(resource: URI, options?: OpenToSideOptions): Promise; + open(resource: URI, options?: OpenInternalOptions): Promise; open(resource: URI, options?: OpenExternalOptions): Promise; } @@ -53,7 +67,7 @@ export interface IOpenerService { * @param resource A resource * @return A promise that resolves when the opening is done. */ - open(resource: URI, options?: OpenToSideOptions): Promise; + open(resource: URI, options?: OpenInternalOptions): Promise; open(resource: URI, options?: OpenExternalOptions): Promise; resolveExternalUri(resource: URI, options?: { readonly allowTunneling?: boolean }): Promise<{ resolved: URI, dispose(): void }>; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 83579569c37..03745282b88 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -49,7 +49,8 @@ import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; import { extname } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -125,6 +126,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -916,23 +918,67 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return openEditorPromise; } - private doHandleOpenEditorError(error: Error, editor: EditorInput, options?: EditorOptions): void { + private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: EditorOptions): Promise { // Report error only if this was not us restoring previous error state or // we are told to ignore errors that occur from opening an editor if (this.isRestored && !isPromiseCanceledError(error) && (!options || !options.ignoreError)) { - const actions: INotificationActions = { primary: [] }; + + // Extract possible error actions from the error + let errorActions: ReadonlyArray | undefined = undefined; if (isErrorWithActions(error)) { - actions.primary = (error as IErrorWithActions).actions; + errorActions = (error as IErrorWithActions).actions; } - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('editorOpenError', "Unable to open '{0}': {1}.", editor.getName(), toErrorMessage(error)), - actions - }); + // If the context is USER, we try to show a modal dialog instead of a background notification + if (options && options.context === EditorOpenContext.USER) { + const buttons: string[] = []; + if (Array.isArray(errorActions) && errorActions.length > 0) { + errorActions.forEach(action => buttons.push(action.label)); + } else { + buttons.push(localize('ok', 'OK')); + } - Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); + let cancelId: number | undefined = undefined; + if (buttons.length === 1) { + buttons.push(localize('cancel', "Cancel")); + cancelId = 1; + } + + const result = await this.dialogService.show( + Severity.Error, + localize('editorOpenErrorDialog', "Unable to open '{0}'", editor.getName()), + buttons, + { + detail: toErrorMessage(error), + cancelId + } + ); + + // Make sure to run any error action if present + if (result.choice !== cancelId && Array.isArray(errorActions)) { + const errorAction = errorActions[result.choice]; + if (errorAction) { + errorAction.run(); + } + } + } + + // Otherwise, show a background notification. + else { + const actions: INotificationActions = { primary: [] }; + if (Array.isArray(errorActions)) { + actions.primary = errorActions; + } + + const handle = this.notificationService.notify({ + severity: Severity.Error, + message: localize('editorOpenError', "Unable to open '{0}': {1}.", editor.getName(), toErrorMessage(error)), + actions + }); + + Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); + } } // Event diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 94d7bff9153..255d6671f30 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -9,7 +9,7 @@ import { isUndefinedOrNull, withNullAsUndefined, assertIsDefined } from 'vs/base import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation } from 'vs/platform/editor/common/editor'; +import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -775,6 +775,18 @@ export class EditorOptions implements IEditorOptions { */ ignoreOverrides: boolean | undefined; + /** + * A optional hint to signal in which context the editor opens. + * + * If configured to be `EditorOpenContext.USER`, this hint can be + * used in various places to control the experience. For example, + * if the editor to open fails with an error, a notification could + * inform about this in a modal dialog. If the editor opened through + * some background task, the notification would show in the background, + * not as a modal dialog. + */ + context: EditorOpenContext | undefined; + /** * Overwrites option values from the provided bag. */ @@ -819,6 +831,10 @@ export class EditorOptions implements IEditorOptions { this.ignoreOverrides = options.ignoreOverrides; } + if (typeof options.context === 'number') { + this.context = options.context; + } + return this; } }