diff --git a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts index f48a63ec9b456..9cf0fb6518cda 100644 --- a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts +++ b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts @@ -22,16 +22,16 @@ import * as jsoncparser from 'jsonc-parser'; import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileOperationError } from '@theia/filesystem/lib/common/files'; +import { ResourceProvider } from '@theia/core/lib/common'; @injectable() export class MonacoSnippetSuggestProvider implements monaco.languages.CompletionItemProvider { private static readonly _maxPrefix = 10000; - @inject(FileService) - protected readonly fileService: FileService; + @inject(ResourceProvider) + protected readonly resourceProvider: ResourceProvider; protected readonly snippets = new Map(); protected readonly pendingSnippets = new Map[]>(); @@ -114,7 +114,7 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } - fromURI(uri: string | URI, options: SnippetLoadOptions): Disposable { + fromURI(uri: string, options: SnippetLoadOptions): Disposable { const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); const pending = this.loadURI(uri, options, toDispose); const { language } = options; @@ -136,10 +136,11 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion /** * should NOT throw to prevent load errors on suggest */ - protected async loadURI(uri: string | URI, options: SnippetLoadOptions, toDispose: DisposableCollection): Promise { + protected async loadURI(uri: string, options: SnippetLoadOptions, toDispose: DisposableCollection): Promise { + const resourceUri = new URI(uri); + const resource = await this.resourceProvider(resourceUri); try { - const resource = typeof uri === 'string' ? new URI(uri) : uri; - const { value } = await this.fileService.read(resource); + const value = await resource.readContents(); if (toDispose.disposed) { return; } @@ -149,6 +150,8 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion if (!(e instanceof FileOperationError)) { console.error(e); } + } finally { + resource.dispose(); } } diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index 2fd9708737d8f..022d2b6ce723c 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -48,9 +48,9 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca entryPoint: { backend: plugin.main }, - iconUrl: plugin.icon && PluginPackage.toPluginUrl(plugin, plugin.icon), - readmeUrl: PluginPackage.toPluginUrl(plugin, './README.md'), - licenseUrl: PluginPackage.toPluginUrl(plugin, './LICENSE') + iconUrl: plugin.icon && PluginPackage.toPluginUri(plugin, plugin.icon), + readmeUrl: PluginPackage.toPluginUri(plugin, './README.md'), + licenseUrl: PluginPackage.toPluginUri(plugin, './LICENSE') }; return result; } diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 1a9e4f5b862a5..644674e28e221 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -58,8 +58,24 @@ export interface PluginPackage { icon?: string; } export namespace PluginPackage { - export function toPluginUrl(pck: PluginPackage, relativePath: string): string { - return `hostedPlugin/${getPluginId(pck)}/${encodeURIComponent(relativePath)}`; + export const RESOURCE_SCHEME = 'pluginresource'; + + export function toPluginUri(pck: PluginPackage, relativePath: string): string { + return PluginUri.toPluginUri(getPluginId(pck), relativePath); + } + + export function toPlugingUriPath(pck: PluginPackage, relativePath: string): string { + return PluginUri.toPlugingUriPath(getPluginId(pck), relativePath); + } +} + +export namespace PluginUri { + export function toPlugingUriPath(pluginId: string, relativePath: string): string { + return `/hostedPlugin/${pluginId}/${encodeURIComponent(relativePath)}`; + } + + export function toPluginUri(pluginId: string, relativePath: string): string { + return `${PluginPackage.RESOURCE_SCHEME}://${toPlugingUriPath(pluginId, relativePath)}`; } } diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index 6c66ee88b7b99..90f8e936ffc7a 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -19,7 +19,7 @@ import { RPCProtocolImpl } from '../../../common/rpc-protocol'; import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; import { MAIN_RPC_CONTEXT, Plugin, emptyPlugin } from '../../../common/plugin-api-rpc'; import { createAPIFactory } from '../../../plugin/plugin-context'; -import { getPluginId, PluginMetadata, PluginPackage } from '../../../common/plugin-protocol'; +import { PluginMetadata, PluginPackage } from '../../../common/plugin-protocol'; import * as theia from '@theia/plugin'; import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry'; import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution'; @@ -69,7 +69,7 @@ const pluginManager = new PluginManagerExtImpl({ if (isElectron()) { ctx.importScripts(plugin.pluginPath); } else { - ctx.importScripts('/hostedPlugin/' + getPluginId(plugin.model) + '/' + plugin.pluginPath); + ctx.importScripts(PluginPackage.toPlugingUriPath(plugin.rawModel, plugin.pluginPath)); } } diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 9267483bfad05..2ff493d135bfd 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -328,7 +328,7 @@ export class TheiaPluginScanner implements PluginScanner { } protected toPluginUrl(pck: PluginPackage, relativePath: string): string { - return PluginPackage.toPluginUrl(pck, relativePath); + return PluginPackage.toPluginUri(pck, relativePath); } protected readColors(pck: PluginPackage): ColorDefinition[] | undefined { @@ -418,10 +418,14 @@ export class TheiaPluginScanner implements PluginScanner { const result: SnippetContribution[] = []; for (const contribution of pck.contributes.snippets) { if (contribution.path) { + const absolutePath = path.join(pck.packagePath, contribution.path); + const normalizedPath = path.normalize(absolutePath); + const relativePath = path.relative(pck.packagePath, normalizedPath); + result.push({ language: contribution.language, source: pck.displayName || pck.name, - uri: FileUri.create(path.join(pck.packagePath, contribution.path)).toString() + uri: PluginPackage.toPluginUri(pck, relativePath).toString() }); } } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index da88ad2daaf0a..daf39284c9775 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -64,6 +64,7 @@ import { WebviewResourceCache } from './webview/webview-resource-cache'; import { PluginIconThemeService, PluginIconThemeFactory, PluginIconThemeDefinition, PluginIconTheme } from './plugin-icon-theme-service'; import { PluginTreeViewNodeLabelProvider } from './view/plugin-tree-view-node-label-provider'; import { WebviewWidgetFactory } from './webview/webview-widget-factory'; +import { PluginResourceResolver } from './plugin-resource'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -96,6 +97,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(UntitledResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(UntitledResourceResolver); + bind(PluginResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(PluginResourceResolver); + bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ onStart(): MaybePromise { ctx.container.get(HostedPluginSupport).onStart(ctx.container); diff --git a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts index bce47cbfefd7a..47b65afd0b06a 100644 --- a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts +++ b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts @@ -24,7 +24,7 @@ import debounce = require('lodash.debounce'); import * as jsoncparser from 'jsonc-parser'; import { injectable, inject, postConstruct } from 'inversify'; import { IconThemeService, IconTheme, IconThemeDefinition } from '@theia/core/lib/browser/icon-theme-service'; -import { IconThemeContribution, DeployedPlugin, UiTheme, getPluginId } from '../../common/plugin-protocol'; +import { IconThemeContribution, DeployedPlugin, UiTheme, getPluginId, PluginUri } from '../../common/plugin-protocol'; import URI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; @@ -331,7 +331,7 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh const iconUri = this.locationUri.resolve(iconPath); const relativePath = this.packageRootUri.path.relative(iconUri.path.normalize()); return relativePath && `url('${new Endpoint({ - path: `hostedPlugin/${this.pluginId}/${encodeURIComponent(relativePath.normalize().toString())}` + path: PluginUri.toPlugingUriPath(this.pluginId, relativePath.toString()) }).getRestUrl().toString()}')`; } diff --git a/packages/plugin-ext/src/main/browser/plugin-resource.ts b/packages/plugin-ext/src/main/browser/plugin-resource.ts new file mode 100644 index 0000000000000..095d74b7f1d19 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/plugin-resource.ts @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (C) 2019 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Resource, ResourceReadOptions, ResourceResolver, MaybePromise } from '@theia/core/lib/common'; +import { Endpoint } from '@theia/core/lib/browser'; +import URI from '@theia/core/lib/common/uri'; +import { PluginPackage } from '../../common'; +import { injectable } from 'inversify'; + +export class PluginResource implements Resource { + readonly uri: URI; + + constructor(pluginId: string, relativePath: string) { + this.uri = PluginResource.getUri(pluginId, relativePath); + } + + private static getUri(pluginId: string, relativePath: string): URI { + return new Endpoint({ + path: `hostedPlugin/${pluginId}/${encodeURIComponent(relativePath.normalize().toString())}` + }).getRestUrl(); + } + + async readContents(options?: ResourceReadOptions): Promise { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + + request.onreadystatechange = function (): void { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('Could not fetch plugin resource')); + } + } + }; + + request.open('GET', this.uri.toString(), true); + request.send(); + }); + } + + dispose(): void { + + } +} + +@injectable() +export class PluginResourceResolver implements ResourceResolver { + resolve(uri: URI): MaybePromise { + if (uri.scheme !== PluginPackage.RESOURCE_SCHEME) { + throw new Error('Not a plugin resource'); + } + const pathAsString = uri.path.toString(); + const matches = new RegExp('/hostedPlugin/(.*)/(.*)').exec(pathAsString); + if (!matches) { + throw new Error('path does not match: ' + uri.toString()); + } + return new PluginResource(matches[1], matches[2]); + } +} diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts index 58986f7c742a3..6a5a3aee5f157 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -17,9 +17,10 @@ import { injectable } from 'inversify'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ThemeService, Theme } from '@theia/core/lib/browser/theming'; -import { IconUrl } from '../../common/plugin-protocol'; +import { IconUrl, PluginPackage } from '../../common/plugin-protocol'; import { Reference, SyncReferenceCollection } from '@theia/core/lib/common/reference'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import URI from '@theia/core/lib/common/uri'; export interface PluginIconKey { url: IconUrl @@ -121,8 +122,9 @@ export class PluginSharedStyle { } static toExternalIconUrl(iconUrl: string): string { - if (iconUrl.startsWith('hostedPlugin/')) { - return new Endpoint({ path: iconUrl }).getRestUrl().toString(); + const uri = new URI(iconUrl); + if (PluginPackage.RESOURCE_SCHEME === uri.scheme) { + return new Endpoint({ path: uri.path.toString() }).getRestUrl().toString(); } return iconUrl; } diff --git a/packages/plugin-ext/src/plugin/plugin-icon-path.ts b/packages/plugin-ext/src/plugin/plugin-icon-path.ts index edcdc51a7f9a4..ec93ef7dae5cd 100644 --- a/packages/plugin-ext/src/plugin/plugin-icon-path.ts +++ b/packages/plugin-ext/src/plugin/plugin-icon-path.ts @@ -45,6 +45,6 @@ export namespace PluginIconPath { const absolutePath = path.isAbsolute(arg) ? arg : path.join(packagePath, arg); const normalizedPath = path.normalize(absolutePath); const relativePath = path.relative(packagePath, normalizedPath); - return PluginPackage.toPluginUrl(plugin.rawModel, relativePath); + return PluginPackage.toPluginUri(plugin.rawModel, relativePath); } } diff --git a/packages/plugin-ext/src/plugin/quick-open.ts b/packages/plugin-ext/src/plugin/quick-open.ts index 6c16d42bb73ef..bef53dca140eb 100644 --- a/packages/plugin-ext/src/plugin/quick-open.ts +++ b/packages/plugin-ext/src/plugin/quick-open.ts @@ -332,7 +332,7 @@ export class QuickInputExt implements QuickInput { const absolutePath = path.isAbsolute(arg) ? arg : path.join(packagePath, arg); const normalizedPath = path.normalize(absolutePath); const relativePath = path.relative(packagePath, normalizedPath); - return PluginPackage.toPluginUrl(this.plugin.rawModel, relativePath); + return PluginPackage.toPluginUri(this.plugin.rawModel, relativePath); }; if ('id' in iconPath || iconPath instanceof ThemeIcon) { return iconPath; diff --git a/packages/vsx-registry/src/browser/vsx-extension.tsx b/packages/vsx-registry/src/browser/vsx-extension.tsx index 62a53bebc4060..00291ddd0e27a 100644 --- a/packages/vsx-registry/src/browser/vsx-extension.tsx +++ b/packages/vsx-registry/src/browser/vsx-extension.tsx @@ -27,6 +27,7 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { VSXEnvironment } from '../common/vsx-environment'; import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; import { VSXExtensionNamespaceAccess, VSXUser } from '../common/vsx-registry-types'; +import { PluginSharedStyle } from '@theia/plugin-ext/lib/main/browser/plugin-shared-style'; @injectable() export class VSXExtensionData { @@ -147,7 +148,7 @@ export class VSXExtension implements VSXExtensionData, TreeElement { const plugin = this.plugin; const iconUrl = plugin && plugin.metadata.model.iconUrl; if (iconUrl) { - return new Endpoint({ path: iconUrl }).getRestUrl().toString(); + return PluginSharedStyle.toExternalIconUrl(iconUrl); } return this.data['iconUrl']; }