Skip to content

Commit 646aaf6

Browse files
author
Benjamin Pasero
authored
Cleanup open-url handling (microsoft#91234)
* cleanup open-url handling * open url - allow to open a remote workpsace without opening a window first * help build * undo build change * support files too
1 parent 6f79071 commit 646aaf6

4 files changed

Lines changed: 136 additions & 88 deletions

File tree

src/main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function configureCommandlineSwitchesSync(cliArgs) {
136136
// override for the color profile to use
137137
'force-color-profile'
138138
];
139-
139+
140140
if (process.platform === 'linux') {
141141
SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
142142
}
@@ -344,7 +344,7 @@ function setCurrentWorkingDirectory() {
344344
function registerListeners() {
345345

346346
/**
347-
* Mac: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
347+
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
348348
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
349349
*
350350
* @type {string[]}
@@ -356,7 +356,7 @@ function registerListeners() {
356356
});
357357

358358
/**
359-
* React to open-url requests.
359+
* macOS: react to open-url requests.
360360
*
361361
* @type {string[]}
362362
*/

src/vs/code/electron-main/app.ts

Lines changed: 112 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log';
2424
import { IStateService } from 'vs/platform/state/node/state';
2525
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
2626
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
27-
import { IURLService, IOpenURLOptions } from 'vs/platform/url/common/url';
27+
import { IURLService } from 'vs/platform/url/common/url';
2828
import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc';
2929
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
3030
import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils';
@@ -73,10 +73,10 @@ import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsSer
7373
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
7474
import { IElectronMainService, ElectronMainService } from 'vs/platform/electron/electron-main/electronMainService';
7575
import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
76-
import { assign } from 'vs/base/common/objects';
7776
import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
7877
import { withNullAsUndefined } from 'vs/base/common/types';
7978
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
79+
import { coalesce } from 'vs/base/common/arrays';
8080

8181
export class CodeApplication extends Disposable {
8282

@@ -395,7 +395,7 @@ export class CodeApplication extends Disposable {
395395
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
396396

397397
// Post Open Windows Tasks
398-
appInstantiationService.invokeFunction(this.afterWindowOpen.bind(this));
398+
appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor));
399399

400400
// Tracing: Stop tracing after windows are ready if enabled
401401
if (this.environmentService.args.trace) {
@@ -575,9 +575,8 @@ export class CodeApplication extends Disposable {
575575
electronIpcServer.registerChannel('logger', loggerChannel);
576576
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
577577

578-
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
579-
580578
// ExtensionHost Debug broadcast service
579+
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
581580
electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ElectronExtensionHostDebugBroadcastChannel(windowsMainService));
582581

583582
// Signal phase: ready (services set)
@@ -586,47 +585,67 @@ export class CodeApplication extends Disposable {
586585
// Propagate to clients
587586
this.dialogMainService = accessor.get(IDialogMainService);
588587

589-
// Create a URL handler to open file URIs in the active window
588+
// Check for initial URLs to handle from protocol link invocations
590589
const environmentService = accessor.get(IEnvironmentService);
590+
const pendingWindowOpenablesFromProtocolLinks: IWindowOpenable[] = [];
591+
const pendingProtocolLinksToHandle = coalesce([
592+
593+
// Windows/Linux: protocol handler invokes CLI with --open-url
594+
...environmentService.args['open-url'] ? environmentService.args._urls || [] : [],
595+
596+
// macOS: open-url events
597+
...((<any>global).getOpenUrls() || []) as string[]
598+
].map(pendingUrlToHandle => {
599+
try {
600+
return URI.parse(pendingUrlToHandle);
601+
} catch (error) {
602+
return undefined;
603+
}
604+
})).filter(pendingUriToHandle => {
605+
// filter out any protocol link that wants to open as window so that
606+
// we open the right set of windows on startup and not restore the
607+
// previous workspace too.
608+
const windowOpenable = this.getWindowOpenableFromProtocolLink(pendingUriToHandle);
609+
if (windowOpenable) {
610+
pendingWindowOpenablesFromProtocolLinks.push(windowOpenable);
611+
612+
return false;
613+
}
614+
615+
return true;
616+
});
617+
618+
// Create a URL handler to open file URIs in the active window
619+
const app = this;
591620
urlService.registerHandler({
592-
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
593-
594-
// Catch file/remote URLs
595-
if ((uri.authority === Schemas.file || uri.authority === Schemas.vscodeRemote) && !!uri.path) {
596-
const cli = assign(Object.create(null), environmentService.args);
597-
const urisToOpen: IWindowOpenable[] = [];
598-
599-
// File path
600-
if (uri.authority === Schemas.file) {
601-
// we configure as fileUri, but later validation will
602-
// make sure to open as folder or workspace if possible
603-
urisToOpen.push({ fileUri: URI.file(uri.fsPath) });
604-
}
621+
async handleURL(uri: URI): Promise<boolean> {
622+
623+
// Check for URIs to open in window
624+
const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri);
625+
if (windowOpenableFromProtocolLink) {
626+
windowsMainService.open({
627+
context: OpenContext.API,
628+
cli: { ...environmentService.args },
629+
urisToOpen: [windowOpenableFromProtocolLink],
630+
gotoLineMode: true
631+
});
605632

606-
// Remote path
607-
else {
608-
// Example conversion:
609-
// From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco
610-
// To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco
611-
const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */);
612-
if (secondSlash !== -1) {
613-
const authority = uri.path.substring(1, secondSlash);
614-
const path = uri.path.substring(secondSlash);
615-
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment });
616-
617-
if (hasWorkspaceFileExtension(path)) {
618-
urisToOpen.push({ workspaceUri: remoteUri });
619-
} else {
620-
urisToOpen.push({ folderUri: remoteUri });
621-
}
622-
}
623-
}
633+
return true;
634+
}
624635

625-
if (urisToOpen.length > 0) {
626-
windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true });
636+
// If we have not yet handled the URI and we have no window opened (macOS only)
637+
// we first open a window and then try to open that URI within that window
638+
if (isMacintosh && windowsMainService.getWindowCount() === 0) {
639+
const [window] = windowsMainService.open({
640+
context: OpenContext.API,
641+
cli: { ...environmentService.args },
642+
forceEmpty: true,
643+
gotoLineMode: true
644+
});
627645

628-
return true;
629-
}
646+
await window.ready();
647+
648+
return urlService.open(uri);
630649
}
631650

632651
return false;
@@ -638,37 +657,13 @@ export class CodeApplication extends Disposable {
638657
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
639658
const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter);
640659
const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter);
641-
const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel);
642-
643-
// On Mac, Code can be running without any open windows, so we must create a window to handle urls,
644-
// if there is none
645-
if (isMacintosh) {
646-
urlService.registerHandler({
647-
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
648-
if (windowsMainService.getWindowCount() === 0) {
649-
const cli = { ...environmentService.args };
650-
const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true, gotoLineMode: true });
651-
652-
await window.ready();
653-
654-
return urlService.open(uri);
655-
}
656-
657-
return false;
658-
}
659-
});
660-
}
661-
662-
// Register the multiple URL handler
663-
urlService.registerHandler(multiplexURLHandler);
660+
urlService.registerHandler(new URLHandlerChannelClient(urlHandlerChannel));
664661

665662
// Watch Electron URLs and forward them to the UrlService
666-
const args = this.environmentService.args;
667-
const urls = args['open-url'] ? args._urls : [];
668-
const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService, this.environmentService);
669-
this._register(urlListener);
663+
this._register(new ElectronURLListener(pendingProtocolLinksToHandle, urlService, windowsMainService, this.environmentService));
670664

671665
// Open our first window
666+
const args = this.environmentService.args;
672667
const macOpenFiles: string[] = (<any>global).macOpenFiles;
673668
const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
674669
const hasCliArgs = args._.length;
@@ -677,6 +672,19 @@ export class CodeApplication extends Disposable {
677672
const noRecentEntry = args['skip-add-to-recently-opened'] === true;
678673
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
679674

675+
// check for a pending window to open from URI
676+
// e.g. when running code with --open-uri from
677+
// a protocol handler
678+
if (pendingWindowOpenablesFromProtocolLinks.length > 0) {
679+
return windowsMainService.open({
680+
context,
681+
cli: args,
682+
urisToOpen: pendingWindowOpenablesFromProtocolLinks,
683+
gotoLineMode: true,
684+
initialStartup: true
685+
});
686+
}
687+
680688
// new window if "-n" or "--remote" was used without paths
681689
if ((args['new-window'] || args.remote) && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
682690
return windowsMainService.open({
@@ -698,7 +706,6 @@ export class CodeApplication extends Disposable {
698706
urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)),
699707
noRecentEntry,
700708
waitMarkerFileURI,
701-
gotoLineMode: false,
702709
initialStartup: true
703710
});
704711
}
@@ -716,6 +723,40 @@ export class CodeApplication extends Disposable {
716723
});
717724
}
718725

726+
private getWindowOpenableFromProtocolLink(uri: URI): IWindowOpenable | undefined {
727+
if (!uri.path) {
728+
return undefined;
729+
}
730+
731+
// File path
732+
if (uri.authority === Schemas.file) {
733+
// we configure as fileUri, but later validation will
734+
// make sure to open as folder or workspace if possible
735+
return { fileUri: URI.file(uri.fsPath) };
736+
}
737+
738+
// Remote path
739+
else if (uri.authority === Schemas.vscodeRemote) {
740+
// Example conversion:
741+
// From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco
742+
// To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco
743+
const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */);
744+
if (secondSlash !== -1) {
745+
const authority = uri.path.substring(1, secondSlash);
746+
const path = uri.path.substring(secondSlash);
747+
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment });
748+
749+
if (hasWorkspaceFileExtension(path)) {
750+
return { workspaceUri: remoteUri };
751+
} else {
752+
return { folderUri: remoteUri };
753+
}
754+
}
755+
}
756+
757+
return undefined;
758+
}
759+
719760
private getWindowOpenableFromPathSync(path: string): IWindowOpenable {
720761
try {
721762
const fileStat = statSync(path);
@@ -734,6 +775,7 @@ export class CodeApplication extends Disposable {
734775
}
735776

736777
private afterWindowOpen(accessor: ServicesAccessor): void {
778+
737779
// Signal phase: after window open
738780
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
739781

@@ -763,7 +805,7 @@ class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHost
763805
super();
764806
}
765807

766-
call(ctx: TContext, command: string, arg?: any): Promise<any> {
808+
async call(ctx: TContext, command: string, arg?: any): Promise<any> {
767809
if (command === 'openExtensionDevelopmentHostWindow') {
768810
const env = arg[1];
769811
const pargs = parseArgs(arg[0], OPTIONS);
@@ -775,7 +817,6 @@ class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHost
775817
userEnv: Object.keys(env).length > 0 ? env : undefined
776818
});
777819
}
778-
return Promise.resolve();
779820
} else {
780821
return super.call(ctx, command, arg);
781822
}

src/vs/code/node/paths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export function validatePaths(args: ParsedArgs): ParsedArgs {
1919
args._ = [];
2020
}
2121

22+
// Normalize paths and watch out for goto line mode
2223
if (!args['remote']) {
23-
// Normalize paths and watch out for goto line mode
2424
const paths = doValidatePaths(args._, args.goto);
2525
args._ = paths;
2626
}

src/vs/platform/url/electron-main/electronUrlListener.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { URI } from 'vs/base/common/uri';
1212
import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
1313
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
1414
import { isWindows } from 'vs/base/common/platform';
15-
import { coalesce } from 'vs/base/common/arrays';
1615
import { disposableTimeout } from 'vs/base/common/async';
1716

1817
function uriFromRawUrl(url: string): URI | null {
@@ -23,6 +22,16 @@ function uriFromRawUrl(url: string): URI | null {
2322
}
2423
}
2524

25+
/**
26+
* A listener for URLs that are opened from the OS and handled by VSCode.
27+
* Depending on the platform, this works differently:
28+
* - Windows: we use `app.setAsDefaultProtocolClient()` to register VSCode with the OS
29+
* and additionally add the `open-url` command line argument to identify.
30+
* - macOS: we rely on `app.on('open-url')` to be called by the OS
31+
* - Linux: we have a special shortcut installed (`resources/linux/code-url-handler.desktop`)
32+
* that calls VSCode with the `open-url` command line argument
33+
* (https://github.com/microsoft/vscode/pull/56727)
34+
*/
2635
export class ElectronURLListener {
2736

2837
private uris: URI[] = [];
@@ -31,36 +40,34 @@ export class ElectronURLListener {
3140
private disposables = new DisposableStore();
3241

3342
constructor(
34-
initial: string | string[],
35-
@IURLService private readonly urlService: IURLService,
36-
@IWindowsMainService windowsMainService: IWindowsMainService,
37-
@IEnvironmentService environmentService: IEnvironmentService
43+
initialUrisToHandle: URI[],
44+
private readonly urlService: IURLService,
45+
windowsMainService: IWindowsMainService,
46+
environmentService: IEnvironmentService
3847
) {
39-
const globalBuffer = ((<any>global).getOpenUrls() || []) as string[];
40-
const rawBuffer = [
41-
...(typeof initial === 'string' ? [initial] : initial),
42-
...globalBuffer
43-
];
4448

45-
this.uris = coalesce(rawBuffer.map(uriFromRawUrl));
49+
// the initial set of URIs we need to handle once the window is ready
50+
this.uris = initialUrisToHandle;
4651

52+
// Windows: install as protocol handler
4753
if (isWindows) {
4854
const windowsParameters = environmentService.isBuilt ? [] : [`"${environmentService.appRoot}"`];
4955
windowsParameters.push('--open-url', '--');
5056
app.setAsDefaultProtocolClient(product.urlProtocol, process.execPath, windowsParameters);
5157
}
5258

59+
// macOS: listen to `open-url` events from here on to handle
5360
const onOpenElectronUrl = Event.map(
5461
Event.fromNodeEventEmitter(app, 'open-url', (event: ElectronEvent, url: string) => ({ event, url })),
5562
({ event, url }) => {
56-
// always prevent default and return the url as string
57-
event.preventDefault();
63+
event.preventDefault(); // always prevent default and return the url as string
5864
return url;
5965
});
6066

6167
const onOpenUrl = Event.filter<URI | null, URI>(Event.map(onOpenElectronUrl, uriFromRawUrl), (uri): uri is URI => !!uri);
6268
onOpenUrl(this.urlService.open, this.urlService, this.disposables);
6369

70+
// Send initial links to the window once it has loaded
6471
const isWindowReady = windowsMainService.getWindows()
6572
.filter(w => w.isReady)
6673
.length > 0;

0 commit comments

Comments
 (0)