diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index b0f361a487216..24b9b18c788ab 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -31,6 +31,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { onUnexpectedError } from 'vs/base/common/errors'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class DebugSession implements IDebugSession { @@ -66,7 +67,8 @@ export class DebugSession implements IDebugSession { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @INotificationService private readonly notificationService: INotificationService, @IProductService private readonly productService: IProductService, - @IWindowsService private readonly windowsService: IWindowsService + @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService ) { this.id = generateUuid(); this.repl = new ReplModel(this); @@ -167,7 +169,7 @@ export class DebugSession implements IDebugSession { return dbgr.createDebugAdapter(this).then(debugAdapter => { - this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.windowsService); + this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.windowsService, this.openerService); return this.raw.start().then(() => { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index fe5e98b83dd8a..d8ad2212ace83 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -17,6 +17,7 @@ import { IWindowsService } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { env as processEnv } from 'vs/base/common/process'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; /** * This interface represents a single command line argument split into a "prefix" and a "path" half. @@ -74,7 +75,8 @@ export class RawDebugSession { dbgr: IDebugger, private readonly telemetryService: ITelemetryService, public readonly customTelemetryService: ITelemetryService | undefined, - private readonly windowsService: IWindowsService + private readonly windowsService: IWindowsService, + private readonly openerService: IOpenerService ) { this.debugAdapter = debugAdapter; @@ -652,7 +654,7 @@ export class RawDebugSession { const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); return createErrorWithActions(userMessage, { actions: [new Action('debug.moreInfo', label, undefined, true, () => { - window.open(error.url); + this.openerService.open(URI.parse(error.url)); return Promise.resolve(null); })] }); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts index d6ebbfd721be2..d967dce1868f0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts @@ -13,9 +13,10 @@ import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { NullOpenerService } from 'vs/platform/opener/common/opener'; function createMockSession(model: DebugModel, name = 'mockSession', parentSession?: DebugSession | undefined): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); } suite('Debug - Model', () => { @@ -427,7 +428,7 @@ suite('Debug - Model', () => { // Repl output test('repl output', () => { - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); const repl = new ReplModel(session); repl.appendToRepl('first line\n', severity.Error); repl.appendToRepl('second line ', severity.Error); diff --git a/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts index 97f55700a404a..b8ded4b910a56 100644 --- a/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts +++ b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts @@ -11,6 +11,8 @@ import { IExtensionsViewlet } from 'vs/workbench/contrib/extensions/common/exten import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable } from 'vs/base/common/lifecycle'; import { language } from 'vs/base/common/platform'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class ExperimentalPrompts extends Disposable implements IWorkbenchContribution { @@ -18,7 +20,8 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib @IExperimentService private readonly experimentService: IExperimentService, @IViewletService private readonly viewletService: IViewletService, @INotificationService private readonly notificationService: INotificationService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); @@ -65,7 +68,7 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib run: () => { logTelemetry(commandText); if (command.externalLink) { - window.open(command.externalLink); + this.openerService.open(URI.parse(command.externalLink)); } else if (command.curatedExtensionsKey && Array.isArray(command.curatedExtensionsList)) { this.viewletService.openViewlet('workbench.view.extensions', true) .then(viewlet => viewlet as IExtensionsViewlet) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index dc9e6489cd7a7..272fa9d0591b4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -54,6 +54,7 @@ import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/browser/w import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { generateUuid } from 'vs/base/common/uuid'; import { platform } from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; function removeEmbeddedSVGs(documentContent: string): string { const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); @@ -185,7 +186,7 @@ export class ExtensionEditor extends BaseEditor { @IStorageService storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, - @IWebviewService private readonly webviewService: IWebviewService, + @IWebviewService private readonly webviewService: IWebviewService ) { super(ExtensionEditor.ID, telemetryService, themeService, storageService); this.extensionReadme = null; @@ -354,8 +355,8 @@ export class ExtensionEditor extends BaseEditor { toggleClass(template.publisher, 'clickable', !!extension.url); toggleClass(template.rating, 'clickable', !!extension.url); if (extension.url) { - this.transientDisposables.add(this.onClick(template.name, () => window.open(extension.url))); - this.transientDisposables.add(this.onClick(template.rating, () => window.open(`${extension.url}#review-details`))); + this.transientDisposables.add(this.onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); + this.transientDisposables.add(this.onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}#review-details`)))); this.transientDisposables.add(this.onClick(template.publisher, () => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) @@ -363,7 +364,7 @@ export class ExtensionEditor extends BaseEditor { })); if (extension.licenseUrl) { - this.transientDisposables.add(this.onClick(template.license, () => window.open(extension.licenseUrl))); + this.transientDisposables.add(this.onClick(template.license, () => this.openerService.open(URI.parse(extension.licenseUrl!)))); template.license.style.display = 'initial'; } else { template.license.style.display = 'none'; @@ -373,7 +374,7 @@ export class ExtensionEditor extends BaseEditor { } if (extension.repository) { - this.transientDisposables.add(this.onClick(template.repository, () => window.open(extension.repository))); + this.transientDisposables.add(this.onClick(template.repository, () => this.openerService.open(URI.parse(extension.repository!)))); template.repository.style.display = 'initial'; } else { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts index 11aacd0695543..e7e9842b027fa 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts @@ -17,6 +17,7 @@ import { join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; abstract class RepoInfo { abstract get base(): string; @@ -117,6 +118,7 @@ class ReportExtensionSlowAction extends Action { readonly repoInfo: RepoInfo, readonly profile: IExtensionHostProfile, @IDialogService private readonly _dialogService: IDialogService, + @IOpenerService private readonly _openerService: IOpenerService ) { super('report.slow', localize('cmd.report', "Report Issue")); } @@ -140,7 +142,7 @@ class ReportExtensionSlowAction extends Action { - VSCode version: \`${pkg.version}\`\n\n${message}`); const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues/new/?body=${body}&title=${title}`; - window.open(url); + this._openerService.open(URI.parse(url)); this._dialogService.show( Severity.Info, @@ -158,6 +160,7 @@ class ShowExtensionSlowAction extends Action { readonly repoInfo: RepoInfo, readonly profile: IExtensionHostProfile, @IDialogService private readonly _dialogService: IDialogService, + @IOpenerService private readonly _openerService: IOpenerService ) { super('show.slow', localize('cmd.show', "Show Issues")); } @@ -172,7 +175,7 @@ class ShowExtensionSlowAction extends Action { // show issues const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues?utf8=✓&q=is%3Aissue+state%3Aopen+%22Extension+causes+high+cpu+load%22`; - window.open(url); + this._openerService.open(URI.parse(url)); this._dialogService.show( Severity.Info, diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 1cc01b85802ea..b6ac536b0898d 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -44,6 +44,8 @@ import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/pl import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -120,7 +122,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IStorageService storageService: IStorageService, @ILabelService private readonly _labelService: ILabelService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IOpenerService private readonly _openerService: IOpenerService ) { super(RuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); @@ -312,7 +315,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true }); } if (isNonEmptyArray(element.status.runtimeErrors)) { - data.actionbar.push(new ReportExtensionIssueAction(element), { icon: true, label: true }); + data.actionbar.push(new ReportExtensionIssueAction(element, this._openerService), { icon: true, label: true }); } let title: string; @@ -416,7 +419,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { const actions: IAction[] = []; - actions.push(new ReportExtensionIssueAction(e.element)); + actions.push(new ReportExtensionIssueAction(e.element, this._openerService)); actions.push(new Separator()); if (e.element.marketplaceInfo) { @@ -480,7 +483,7 @@ export class ReportExtensionIssueAction extends Action { marketplaceInfo: IExtension; status?: IExtensionsStatus; unresponsiveProfile?: IExtensionHostProfile - }) { + }, @IOpenerService private readonly openerService: IOpenerService) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); this.enabled = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User @@ -490,7 +493,7 @@ export class ReportExtensionIssueAction extends Action { } async run(): Promise { - window.open(this._url); + this.openerService.open(URI.parse(this._url)); } private static _generateNewIssueUrl(extension: { diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index 618dc7edaa9d5..ef308647b8df1 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -20,6 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IProductService } from 'vs/platform/product/common/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export interface IFeedback { feedback: string; @@ -27,7 +28,7 @@ export interface IFeedback { } export interface IFeedbackDelegate { - submitFeedback(feedback: IFeedback): void; + submitFeedback(feedback: IFeedback, openerService: IOpenerService): void; getCharacterLimit(sentiment: number): number; } @@ -66,7 +67,8 @@ export class FeedbackDropdown extends Dropdown { @IIntegrityService private readonly integrityService: IIntegrityService, @IThemeService private readonly themeService: IThemeService, @IStatusbarService private readonly statusbarService: IStatusbarService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IOpenerService private readonly openerService: IOpenerService ) { super(container, options); @@ -415,7 +417,7 @@ export class FeedbackDropdown extends Dropdown { this.feedbackDelegate.submitFeedback({ feedback: this.feedbackDescriptionInput.value, sentiment: this.sentiment - }); + }, this.openerService); this.hide(); } diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index c609aa4132fa3..9eda590df6fcb 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -12,6 +12,8 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarService, StatusbarAlignment, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { localize } from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; class TwitterFeedbackService implements IFeedbackDelegate { @@ -23,11 +25,11 @@ class TwitterFeedbackService implements IFeedbackDelegate { return TwitterFeedbackService.HASHTAGS.join(','); } - submitFeedback(feedback: IFeedback): void { + submitFeedback(feedback: IFeedback, openerService: IOpenerService): void { const queryString = `?${feedback.sentiment === 1 ? `hashtags=${this.combineHashTagsAsString()}&` : null}ref_src=twsrc%5Etfw&related=twitterapi%2Ctwitter&text=${encodeURIComponent(feedback.feedback)}&tw_p=tweetbutton&via=${TwitterFeedbackService.VIA_NAME}`; const url = TwitterFeedbackService.TWITTER_URL + queryString; - window.open(url); + openerService.open(URI.parse(url)); } getCharacterLimit(sentiment: number): number { diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index ecfdd8e2ea0cf..9c8fe678aede6 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -17,6 +17,7 @@ import { PerfviewInput } from 'vs/workbench/contrib/performance/electron-browser import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { URI } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class StartupProfiler implements IWorkbenchContribution { @@ -28,6 +29,7 @@ export class StartupProfiler implements IWorkbenchContribution { @IClipboardService private readonly _clipboardService: IClipboardService, @ILifecycleService lifecycleService: ILifecycleService, @IExtensionService extensionService: IExtensionService, + @IOpenerService private readonly _openerService: IOpenerService ) { // wait for everything to be ready Promise.all([ @@ -116,6 +118,6 @@ export class StartupProfiler implements IWorkbenchContribution { const baseUrl = product.reportIssueUrl; const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&'; - window.open(`${baseUrl}${queryStringPrefix}body=${encodeURIComponent(body)}`); + this._openerService.open(URI.parse(`${baseUrl}${queryStringPrefix}body=${encodeURIComponent(body)}`)); } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 4174d8137f637..33e16e9f22d0f 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -61,6 +61,7 @@ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/v import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -153,6 +154,7 @@ export class SearchView extends ViewletPanel { @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, + @IOpenerService private readonly openerService: IOpenerService ) { super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService); @@ -1473,7 +1475,7 @@ export class SearchView extends ViewletPanel { private onLearnMore = (e: MouseEvent): void => { dom.EventHelper.stop(e, false); - window.open('https://go.microsoft.com/fwlink/?linkid=853977'); + this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977')); } private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void { diff --git a/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts index ec9ac3fb2b770..44cf53887ff37 100644 --- a/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts @@ -16,6 +16,8 @@ import { ISurveyData } from 'vs/platform/product/common/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; class LanguageSurvey { @@ -25,7 +27,8 @@ class LanguageSurvey { notificationService: INotificationService, telemetryService: ITelemetryService, modelService: IModelService, - textFileService: ITextFileService + textFileService: ITextFileService, + openerService: IOpenerService ) { const SESSION_COUNT_KEY = `${data.surveyId}.sessionCount`; const LAST_SESSION_DATE_KEY = `${data.surveyId}.lastSessionDate`; @@ -94,7 +97,7 @@ class LanguageSurvey { run: () => { telemetryService.publicLog(`${data.surveyId}.survey/takeShortSurvey`); telemetryService.getTelemetryInfo().then(info => { - window.open(`${data.surveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); + openerService.open(URI.parse(`${data.surveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`)); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); @@ -126,11 +129,12 @@ class LanguageSurveysContribution implements IWorkbenchContribution { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IModelService modelService: IModelService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @IOpenerService openerService: IOpenerService ) { product.surveys .filter(surveyData => surveyData.surveyId && surveyData.editCount && surveyData.languageId && surveyData.surveyUrl && surveyData.userProbability) - .map(surveyData => new LanguageSurvey(surveyData, storageService, notificationService, telemetryService, modelService, textFileService)); + .map(surveyData => new LanguageSurvey(surveyData, storageService, notificationService, telemetryService, modelService, textFileService, openerService)); } } diff --git a/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts index c11fd38db201c..5e4433ce2ad9c 100644 --- a/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts @@ -13,6 +13,8 @@ import pkg from 'vs/platform/product/node/package'; import product from 'vs/platform/product/node/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -25,7 +27,8 @@ class NPSContribution implements IWorkbenchContribution { constructor( @IStorageService storageService: IStorageService, @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IOpenerService openerService: IOpenerService ) { const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.GLOBAL, ''); if (skipVersion) { @@ -64,7 +67,7 @@ class NPSContribution implements IWorkbenchContribution { label: nls.localize('takeSurvey', "Take Survey"), run: () => { telemetryService.getTelemetryInfo().then(info => { - window.open(`${product.npsSurveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); + openerService.open(URI.parse(`${product.npsSurveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`)); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); @@ -87,4 +90,4 @@ class NPSContribution implements IWorkbenchContribution { if (language === 'en' && product.npsSurveyUrl) { const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(NPSContribution, LifecyclePhase.Restored); -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 17e627687400b..461797d862c1b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1620,7 +1620,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer nls.localize('TaskService.noWorkspace', "Tasks are only available on a workspace folder."), [{ label: nls.localize('TaskService.learnMore', "Learn More"), - run: () => window.open('https://code.visualstudio.com/docs/editor/tasks') + run: () => this.openerService.open(URI.parse('https://code.visualstudio.com/docs/editor/tasks')) }] ); return false; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts index 2c0d6778d39c7..200de4fa5c9b7 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts @@ -9,6 +9,8 @@ import { ITelemetryService, ITelemetryInfo } from 'vs/platform/telemetry/common/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class GettingStarted implements IWorkbenchContribution { @@ -20,7 +22,8 @@ export class GettingStarted implements IWorkbenchContribution { constructor( @IStorageService private readonly storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { this.appName = product.nameLong; @@ -50,7 +53,7 @@ export class GettingStarted implements IWorkbenchContribution { if (platform.isLinux && platform.isRootUser()) { return; } - window.open(url); + this.openerService.open(URI.parse(url)); } private handleWelcome(): void { diff --git a/src/vs/workbench/electron-browser/actions/helpActions.ts b/src/vs/workbench/electron-browser/actions/helpActions.ts index 3c5d242f92f16..ce5e237ef08c9 100644 --- a/src/vs/workbench/electron-browser/actions/helpActions.ts +++ b/src/vs/workbench/electron-browser/actions/helpActions.ts @@ -8,6 +8,8 @@ import * as nls from 'vs/nls'; import product from 'vs/platform/product/node/product'; import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class KeybindingsReferenceAction extends Action { @@ -19,13 +21,14 @@ export class KeybindingsReferenceAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(KeybindingsReferenceAction.URL); + this.openerService.open(URI.parse(KeybindingsReferenceAction.URL)); return Promise.resolve(); } @@ -41,13 +44,14 @@ export class OpenDocumentationUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenDocumentationUrlAction.URL); + this.openerService.open(URI.parse(OpenDocumentationUrlAction.URL)); return Promise.resolve(); } @@ -63,13 +67,14 @@ export class OpenIntroductoryVideosUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenIntroductoryVideosUrlAction.URL); + this.openerService.open(URI.parse(OpenIntroductoryVideosUrlAction.URL)); return Promise.resolve(); } @@ -85,13 +90,14 @@ export class OpenTipsAndTricksUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenTipsAndTricksUrlAction.URL); + this.openerService.open(URI.parse(OpenTipsAndTricksUrlAction.URL)); return Promise.resolve(); } } @@ -107,6 +113,7 @@ export class OpenNewsletterSignupUrlAction extends Action { constructor( id: string, label: string, + @IOpenerService private readonly openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService ) { super(id, label); @@ -116,7 +123,7 @@ export class OpenNewsletterSignupUrlAction extends Action { async run(): Promise { const info = await this.telemetryService.getTelemetryInfo(); - window.open(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`); + this.openerService.open(URI.parse(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`)); } } @@ -127,14 +134,15 @@ export class OpenTwitterUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { if (product.twitterUrl) { - window.open(product.twitterUrl); + this.openerService.open(URI.parse(product.twitterUrl)); } return Promise.resolve(); @@ -148,14 +156,15 @@ export class OpenRequestFeatureUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { if (product.requestFeatureUrl) { - window.open(product.requestFeatureUrl); + this.openerService.open(URI.parse(product.requestFeatureUrl)); } return Promise.resolve(); @@ -169,7 +178,8 @@ export class OpenLicenseUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } @@ -178,9 +188,9 @@ export class OpenLicenseUrlAction extends Action { if (product.licenseUrl) { if (language) { const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; - window.open(`${product.licenseUrl}${queryArgChar}lang=${language}`); + this.openerService.open(URI.parse(`${product.licenseUrl}${queryArgChar}lang=${language}`)); } else { - window.open(product.licenseUrl); + this.openerService.open(URI.parse(product.licenseUrl)); } } @@ -195,7 +205,8 @@ export class OpenPrivacyStatementUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } @@ -204,9 +215,9 @@ export class OpenPrivacyStatementUrlAction extends Action { if (product.privacyStatementUrl) { if (language) { const queryArgChar = product.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; - window.open(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`); + this.openerService.open(URI.parse(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`)); } else { - window.open(product.privacyStatementUrl); + this.openerService.open(URI.parse(product.privacyStatementUrl)); } } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index a2ee3629a4683..a9c2344af12a5 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -356,29 +356,35 @@ export class ElectronWindow extends Disposable { private setupOpenHandlers(): void { - // Handle window.open() calls + // Block window.open() calls const $this = this; - window.open = function (url: string, target: string, features: string, replace: boolean): Window | null { - $this.windowsService.openExternal(url); + window.open = function (): Window | null { + console.error(new Error('Prevented call to window.open(). Use IOpenerService instead!')); return null; }; - // Handle external open calls + // Handle internal open() calls this.openerService.registerOpener({ async open(resource: URI, options?: { openToSide?: boolean; openExternal?: boolean; } | undefined): Promise { - if (!options || !options.openExternal) { - return false; // only override behaviour for external open() - } - const success = await $this.windowsService.openExternal(encodeURI(resource.toString(true))); - if (!success && resource.scheme === Schemas.file) { - await $this.windowsService.showItemInFolder(resource); + // If either the caller wants to open externally or the + // scheme is one where we prefer to open externally + // we handle this resource by delegating the opening to + // the main process to prevent window focus issues. + const scheme = resource.scheme.toLowerCase(); + const preferOpenExternal = (scheme === Schemas.mailto || scheme === Schemas.http || scheme === Schemas.https); + if ((options && options.openExternal) || preferOpenExternal) { + const success = await $this.windowsService.openExternal(encodeURI(resource.toString(true))); + if (!success && resource.scheme === Schemas.file) { + // if opening failed, and this is a file, we can still try to reveal it + await $this.windowsService.showItemInFolder(resource); + } return true; } - return success; + return false; // not handled by us } }); }