Path: blob/main/src/vs/editor/browser/services/openerService.ts
5256 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../base/browser/dom.js';6import { mainWindow } from '../../../base/browser/window.js';7import { CancellationToken } from '../../../base/common/cancellation.js';8import { IDisposable } from '../../../base/common/lifecycle.js';9import { LinkedList } from '../../../base/common/linkedList.js';10import { ResourceMap } from '../../../base/common/map.js';11import { parse } from '../../../base/common/marshalling.js';12import { matchesScheme, matchesSomeScheme, Schemas } from '../../../base/common/network.js';13import { normalizePath } from '../../../base/common/resources.js';14import { URI } from '../../../base/common/uri.js';15import { ICodeEditorService } from './codeEditorService.js';16import { ICommandService } from '../../../platform/commands/common/commands.js';17import { EditorOpenSource } from '../../../platform/editor/common/editor.js';18import { extractSelection, IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, OpenOptions, ResolveExternalUriOptions } from '../../../platform/opener/common/opener.js';1920class CommandOpener implements IOpener {2122constructor(@ICommandService private readonly _commandService: ICommandService) { }2324async open(target: URI | string, options?: OpenOptions): Promise<boolean> {25if (!matchesScheme(target, Schemas.command)) {26return false;27}2829if (!options?.allowCommands) {30// silently ignore commands when command-links are disabled, also31// suppress other openers by returning TRUE32return true;33}3435if (typeof target === 'string') {36target = URI.parse(target);37}3839if (Array.isArray(options.allowCommands)) {40// Only allow specific commands41if (!options.allowCommands.includes(target.path)) {42// Suppress other openers by returning TRUE43return true;44}45}4647// execute as command48let args: unknown[] = [];49try {50args = parse(decodeURIComponent(target.query));51} catch {52// ignore and retry53try {54args = parse(target.query);55} catch {56// ignore error57}58}59if (!Array.isArray(args)) {60args = [args];61}62await this._commandService.executeCommand(target.path, ...args);63return true;64}65}6667class EditorOpener implements IOpener {6869constructor(@ICodeEditorService private readonly _editorService: ICodeEditorService) { }7071async open(target: URI | string, options: OpenOptions) {72if (typeof target === 'string') {73target = URI.parse(target);74}7576const { selection, uri } = extractSelection(target);77target = uri;7879if (target.scheme === Schemas.file) {80target = normalizePath(target); // workaround for non-normalized paths (https://github.com/microsoft/vscode/issues/12954)81}8283await this._editorService.openCodeEditor(84{85resource: target,86options: {87selection,88source: options?.fromUserGesture ? EditorOpenSource.USER : EditorOpenSource.API,89...options?.editorOptions90}91},92this._editorService.getFocusedCodeEditor(),93options?.openToSide94);9596return true;97}98}99100export class OpenerService implements IOpenerService {101102declare readonly _serviceBrand: undefined;103104private readonly _openers = new LinkedList<IOpener>();105private readonly _validators = new LinkedList<IValidator>();106private readonly _resolvers = new LinkedList<IExternalUriResolver>();107private readonly _resolvedUriTargets = new ResourceMap<URI>(uri => uri.with({ path: null, fragment: null, query: null }).toString());108109private _defaultExternalOpener: IExternalOpener;110private readonly _externalOpeners = new LinkedList<IExternalOpener>();111112constructor(113@ICodeEditorService editorService: ICodeEditorService,114@ICommandService commandService: ICommandService115) {116// Default external opener is going through window.open()117this._defaultExternalOpener = {118openExternal: async href => {119// ensure to open HTTP/HTTPS links into new windows120// to not trigger a navigation. Any other link is121// safe to be set as HREF to prevent a blank window122// from opening.123if (matchesSomeScheme(href, Schemas.http, Schemas.https)) {124dom.windowOpenNoOpener(href);125} else {126mainWindow.location.href = href;127}128return true;129}130};131132// Default opener: any external, maito, http(s), command, and catch-all-editors133this._openers.push({134open: async (target: URI | string, options?: OpenOptions) => {135if (options?.openExternal || matchesSomeScheme(target, Schemas.mailto, Schemas.http, Schemas.https, Schemas.vsls)) {136// open externally137await this._doOpenExternal(target, options);138return true;139}140return false;141}142});143this._openers.push(new CommandOpener(commandService));144this._openers.push(new EditorOpener(editorService));145}146147registerOpener(opener: IOpener): IDisposable {148const remove = this._openers.unshift(opener);149return { dispose: remove };150}151152registerValidator(validator: IValidator): IDisposable {153const remove = this._validators.push(validator);154return { dispose: remove };155}156157registerExternalUriResolver(resolver: IExternalUriResolver): IDisposable {158const remove = this._resolvers.push(resolver);159return { dispose: remove };160}161162setDefaultExternalOpener(externalOpener: IExternalOpener): void {163this._defaultExternalOpener = externalOpener;164}165166registerExternalOpener(opener: IExternalOpener): IDisposable {167const remove = this._externalOpeners.push(opener);168return { dispose: remove };169}170171async open(target: URI | string, options?: OpenOptions): Promise<boolean> {172const targetURI = typeof target === 'string' ? URI.parse(target) : target;173174// Internal schemes are not openable and must instead be handled in event listeners175if (targetURI.scheme === Schemas.internal) {176return false;177}178179// check with contributed validators180if (!options?.skipValidation) {181const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists182for (const validator of this._validators) {183if (!(await validator.shouldOpen(validationTarget, options))) {184return false;185}186}187}188189// check with contributed openers190for (const opener of this._openers) {191const handled = await opener.open(target, options);192if (handled) {193return true;194}195}196197return false;198}199200async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri> {201for (const resolver of this._resolvers) {202try {203const result = await resolver.resolveExternalUri(resource, options);204if (result) {205if (!this._resolvedUriTargets.has(result.resolved)) {206this._resolvedUriTargets.set(result.resolved, resource);207}208return result;209}210} catch {211// noop212}213}214215throw new Error('Could not resolve external URI: ' + resource.toString());216}217218private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise<boolean> {219220//todo@jrieken IExternalUriResolver should support `uri: URI | string`221const uri = typeof resource === 'string' ? URI.parse(resource) : resource;222let externalUri: URI;223224try {225externalUri = (await this.resolveExternalUri(uri, options)).resolved;226} catch {227externalUri = uri;228}229230let href: string;231if (typeof resource === 'string' && uri.toString() === externalUri.toString()) {232// open the url-string AS IS233href = resource;234} else {235// open URI using the toString(noEncode)+encodeURI-trick236href = encodeURI(externalUri.toString(true));237}238239if (options?.allowContributedOpeners) {240const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;241for (const opener of this._externalOpeners) {242const didOpen = await opener.openExternal(href, {243sourceUri: uri,244preferredOpenerId,245}, CancellationToken.None);246if (didOpen) {247return true;248}249}250}251252return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None);253}254255dispose() {256this._validators.clear();257}258}259260261