Path: blob/main/src/vs/editor/browser/services/openerService.ts
3294 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: any = [];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> {172173// check with contributed validators174if (!options?.skipValidation) {175const targetURI = typeof target === 'string' ? URI.parse(target) : target;176const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists177for (const validator of this._validators) {178if (!(await validator.shouldOpen(validationTarget, options))) {179return false;180}181}182}183184// check with contributed openers185for (const opener of this._openers) {186const handled = await opener.open(target, options);187if (handled) {188return true;189}190}191192return false;193}194195async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri> {196for (const resolver of this._resolvers) {197try {198const result = await resolver.resolveExternalUri(resource, options);199if (result) {200if (!this._resolvedUriTargets.has(result.resolved)) {201this._resolvedUriTargets.set(result.resolved, resource);202}203return result;204}205} catch {206// noop207}208}209210throw new Error('Could not resolve external URI: ' + resource.toString());211}212213private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise<boolean> {214215//todo@jrieken IExternalUriResolver should support `uri: URI | string`216const uri = typeof resource === 'string' ? URI.parse(resource) : resource;217let externalUri: URI;218219try {220externalUri = (await this.resolveExternalUri(uri, options)).resolved;221} catch {222externalUri = uri;223}224225let href: string;226if (typeof resource === 'string' && uri.toString() === externalUri.toString()) {227// open the url-string AS IS228href = resource;229} else {230// open URI using the toString(noEncode)+encodeURI-trick231href = encodeURI(externalUri.toString(true));232}233234if (options?.allowContributedOpeners) {235const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;236for (const opener of this._externalOpeners) {237const didOpen = await opener.openExternal(href, {238sourceUri: uri,239preferredOpenerId,240}, CancellationToken.None);241if (didOpen) {242return true;243}244}245}246247return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None);248}249250dispose() {251this._validators.clear();252}253}254255256