From 9d702b7e5b55be1027d750e003014fb660b5d61d Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 31 Oct 2019 07:20:50 +0000 Subject: [PATCH] [webview] fix #5786: unify the icon path resolution Also: - preserve the icon path between user sessions and reconnections - add missing icon-path getter api Signed-off-by: Anton Kosyakov --- .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../src/main/browser/plugin-shared-style.ts | 3 +- .../src/main/browser/style/webview.css | 6 +-- .../src/main/browser/webview/webview.ts | 39 ++++++++++++--- .../src/main/browser/webviews-main.ts | 5 +- .../plugin-ext/src/plugin/plugin-context.ts | 4 +- .../plugin-ext/src/plugin/plugin-icon-path.ts | 50 +++++++++++++++++++ .../plugin-ext/src/plugin/tree/tree-views.ts | 35 +++---------- packages/plugin-ext/src/plugin/webviews.ts | 45 ++++++++--------- 9 files changed, 122 insertions(+), 67 deletions(-) create mode 100644 packages/plugin-ext/src/plugin/plugin-icon-path.ts diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 2ca91122f8433..a786b6666921a 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1251,7 +1251,7 @@ export interface WebviewsMain { $disposeWebview(handle: string): void; $reveal(handle: string, showOptions: theia.WebviewPanelShowOptions): void; $setTitle(handle: string, value: string): void; - $setIconPath(handle: string, value: { light: string, dark: string } | string | undefined): void; + $setIconPath(handle: string, value: IconUrl | undefined): void; $setHtml(handle: string, value: string): void; $setOptions(handle: string, options: theia.WebviewOptions): void; $postMessage(handle: string, value: any): Thenable; 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 be303d75aaa9b..42c7c5a3419a0 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -78,9 +78,8 @@ export class PluginSharedStyle { }): void { const sheet = (this.style.sheet); const cssBody = body(ThemeService.get().getCurrentTheme()); - sheet.insertRule(selector + ' { ' + cssBody + ' }', 0); + sheet.insertRule(selector + ' {\n' + cssBody + '\n}', 0); } - deleteRule(selector: string): void { const sheet = (this.style.sheet); const rules = sheet.rules || sheet.cssRules || []; diff --git a/packages/plugin-ext/src/main/browser/style/webview.css b/packages/plugin-ext/src/main/browser/style/webview.css index 4484abee04005..56025c3cc2ddd 100644 --- a/packages/plugin-ext/src/main/browser/style/webview.css +++ b/packages/plugin-ext/src/main/browser/style/webview.css @@ -25,12 +25,12 @@ border: none; margin: 0; padding: 0; } -.webview-icon { +.theia-webview-icon { background: none !important; min-height: 20px; } -.webview-icon::before { +.theia-webview-icon::before { background-size: 13px; background-repeat: no-repeat; vertical-align: middle; @@ -41,7 +41,7 @@ content: ""; } -.p-TabBar.theia-app-sides .webview-icon::before { +.p-TabBar.theia-app-sides .theia-webview-icon::before { width: var(--theia-private-sidebar-icon-size); height: var(--theia-private-sidebar-icon-size); background-size: contain; diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index caf5b83832fca..b310d0ff2f1ba 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -15,15 +15,17 @@ ********************************************************************************/ import * as mime from 'mime'; +import { JSONExt } from '@phosphor/coreutils/lib/json'; import { injectable, inject, postConstruct } from 'inversify'; import { WebviewPanelOptions, WebviewPortMapping } from '@theia/plugin'; import { BaseWidget, Message } from '@theia/core/lib/browser/widgets/widget'; -import { Disposable } from '@theia/core/lib/common/disposable'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; // TODO: get rid of dependencies to the mini browser import { MiniBrowserContentStyle } from '@theia/mini-browser/lib/browser/mini-browser-content-style'; import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker'; import { StatefulWidget } from '@theia/core/lib/browser/shell/shell-layout-restorer'; import { WebviewPanelViewState } from '../../../common/plugin-api-rpc'; +import { IconUrl } from '../../../common/plugin-protocol'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { WebviewEnvironment } from './webview-environment'; import URI from '@theia/core/lib/common/uri'; @@ -32,7 +34,8 @@ import { Emitter } from '@theia/core/lib/common/event'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { Schemes } from '../../../common/uri-components'; -import { JSONExt } from '@phosphor/coreutils'; +import { PluginSharedStyle } from '../plugin-shared-style'; +import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; // tslint:disable:no-any @@ -102,6 +105,9 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { @inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry; + @inject(PluginSharedStyle) + protected readonly sharedStyle: PluginSharedStyle; + viewState: WebviewPanelViewState = { visible: false, active: false, @@ -209,8 +215,27 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { this.doUpdateContent(); } - setIconClass(iconClass: string): void { - this.title.iconClass = iconClass; + protected iconUrl: IconUrl | undefined; + protected readonly toDisposeOnIcon = new DisposableCollection(); + setIconUrl(iconUrl: IconUrl | undefined): void { + if ((this.iconUrl && iconUrl && JSONExt.deepEqual(this.iconUrl, iconUrl)) || (this.iconUrl === iconUrl)) { + return; + } + this.toDisposeOnIcon.dispose(); + this.toDispose.push(this.toDisposeOnIcon); + this.iconUrl = iconUrl; + if (iconUrl) { + const darkIconUrl = typeof iconUrl === 'object' ? iconUrl.dark : iconUrl; + const lightIconUrl = typeof iconUrl === 'object' ? iconUrl.light : iconUrl; + const iconClass = `webview-${this.identifier.id}-file-icon`; + this.toDisposeOnIcon.push(this.sharedStyle.insertRule( + `.theia-webview-icon.${iconClass}::before`, + theme => `background-image: url(${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl});` + )); + this.title.iconClass = `theia-webview-icon ${iconClass}`; + } else { + this.title.iconClass = ''; + } } setHTML(value: string): void { @@ -327,6 +352,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { return { viewType: this.viewType, title: this.title.label, + iconUrl: this.iconUrl, options: this.options, contentOptions: this.contentOptions, state: this.state @@ -334,9 +360,10 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { } restoreState(oldState: WebviewWidget.State): void { - const { viewType, title, options, contentOptions, state } = oldState; + const { viewType, title, iconUrl, options, contentOptions, state } = oldState; this.viewType = viewType; this.title.label = title; + this.setIconUrl(iconUrl); this.options = options; this._contentOptions = contentOptions; this._state = state; @@ -380,9 +407,9 @@ export namespace WebviewWidget { export interface State { viewType: string title: string + iconUrl?: IconUrl options: WebviewPanelOptions contentOptions: WebviewContentOptions state: any - // TODO: preserve icon class } } diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index 4e8c2c0a5a040..1ea4fc939055a 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -28,6 +28,7 @@ import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { JSONExt } from '@phosphor/coreutils/lib/json'; import { Mutable } from '@theia/core/lib/common/types'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; +import { IconUrl } from '../../common/plugin-protocol'; export class WebviewsMainImpl implements WebviewsMain, Disposable { @@ -159,9 +160,9 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { webview.title.label = value; } - async $setIconPath(handle: string, iconPath: { light: string; dark: string; } | string | undefined): Promise { + async $setIconPath(handle: string, iconUrl: IconUrl | undefined): Promise { const webview = await this.getWebview(handle); - webview.setIconClass(iconPath ? `webview-icon ${handle}-file-icon` : ''); + webview.setIconUrl(iconUrl); } async $setHtml(handle: string, value: string): Promise { diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 6fea4ba46766f..6d1518ffad59a 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -339,10 +339,10 @@ export function createAPIFactory( title: string, showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions = {}): theia.WebviewPanel { - return webviewExt.createWebview(viewType, title, showOptions, options, Uri.file(plugin.pluginPath)); + return webviewExt.createWebview(viewType, title, showOptions, options, plugin); }, registerWebviewPanelSerializer(viewType: string, serializer: theia.WebviewPanelSerializer): theia.Disposable { - return webviewExt.registerWebviewPanelSerializer(viewType, serializer, Uri.file(plugin.pluginPath)); + return webviewExt.registerWebviewPanelSerializer(viewType, serializer, plugin); }, get state(): theia.WindowState { return windowStateExt.getWindowState(); diff --git a/packages/plugin-ext/src/plugin/plugin-icon-path.ts b/packages/plugin-ext/src/plugin/plugin-icon-path.ts new file mode 100644 index 0000000000000..3c709ab31ce77 --- /dev/null +++ b/packages/plugin-ext/src/plugin/plugin-icon-path.ts @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. 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 * as path from 'path'; +import Uri from 'vscode-uri'; +import { IconUrl, PluginPackage } from '../common/plugin-protocol'; +import { Plugin } from '../common/plugin-api-rpc'; + +export type PluginIconPath = string | Uri | { + light: string | Uri, + dark: string | Uri +}; +export namespace PluginIconPath { + export function toUrl(iconPath: PluginIconPath | undefined, plugin: Plugin): IconUrl | undefined { + if (!iconPath) { + return undefined; + } + if (typeof iconPath === 'object' && 'light' in iconPath) { + return { + light: asString(iconPath.light, plugin), + dark: asString(iconPath.dark, plugin) + }; + } + return asString(iconPath, plugin); + } + export function asString(arg: string | Uri, plugin: Plugin): string { + arg = arg instanceof Uri && arg.scheme === 'file' ? arg.fsPath : arg; + if (typeof arg !== 'string') { + return arg.toString(true); + } + const { packagePath } = plugin.rawModel; + 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); + } +} diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index 81a79ef52b60d..1ecc7fd96a728 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -16,8 +16,6 @@ // tslint:disable:no-any -import * as path from 'path'; -import URI from 'vscode-uri'; import { TreeDataProvider, TreeView, TreeViewExpansionEvent, TreeItem2, TreeItemLabel, TreeViewSelectionChangeEvent, TreeViewVisibilityChangeEvent @@ -31,7 +29,7 @@ import { Plugin, PLUGIN_RPC_CONTEXT, TreeViewsExt, TreeViewsMain, TreeViewItem } import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl, CommandsConverter } from '../command-registry'; import { TreeViewSelection } from '../../common'; -import { PluginPackage } from '../../common/plugin-protocol'; +import { PluginIconPath } from '../plugin-icon-path'; export class TreeViewsExtImpl implements TreeViewsExt { @@ -279,31 +277,12 @@ class TreeViewExtImpl implements Disposable { let iconUrl; let themeIconId; const { iconPath } = treeItem; - if (iconPath) { - const toUrl = (arg: string | URI) => { - arg = arg instanceof URI && arg.scheme === 'file' ? arg.fsPath : arg; - if (typeof arg !== 'string') { - return arg.toString(true); - } - const { packagePath } = this.plugin.rawModel; - 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); - }; - if (typeof iconPath === 'string' && iconPath.indexOf('fa-') !== -1) { - icon = iconPath; - } else if (iconPath instanceof ThemeIcon) { - themeIconId = iconPath.id; - } else if (typeof iconPath === 'string' || iconPath instanceof URI) { - iconUrl = toUrl(iconPath); - } else { - const { light, dark } = iconPath as { light: string | URI, dark: string | URI }; - iconUrl = { - light: toUrl(light), - dark: toUrl(dark) - }; - } + if (typeof iconPath === 'string' && iconPath.indexOf('fa-') !== -1) { + icon = iconPath; + } else if (iconPath instanceof ThemeIcon) { + themeIconId = iconPath.id; + } else { + iconUrl = PluginIconPath.toUrl(iconPath, this.plugin); } const treeViewItem = { diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index d6bacec72a1ce..2d6412f815809 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -18,18 +18,20 @@ import { v4 } from 'uuid'; import { WebviewsExt, WebviewPanelViewState, WebviewsMain, PLUGIN_RPC_CONTEXT, WebviewInitData, /* WebviewsMain, PLUGIN_RPC_CONTEXT */ } from '../common/plugin-api-rpc'; import * as theia from '@theia/plugin'; import { RPCProtocol } from '../common/rpc-protocol'; +import { Plugin } from '../common/plugin-api-rpc'; import URI from 'vscode-uri'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type-converters'; import { Disposable, WebviewPanelTargetArea } from './types-impl'; import { WorkspaceExtImpl } from './workspace'; +import { PluginIconPath } from './plugin-icon-path'; export class WebviewsExtImpl implements WebviewsExt { private readonly proxy: WebviewsMain; private readonly webviewPanels = new Map(); private readonly serializers = new Map(); private initData: WebviewInitData | undefined; @@ -85,9 +87,9 @@ export class WebviewsExtImpl implements WebviewsExt { if (!entry) { return Promise.reject(new Error(`No serializer found for '${viewType}'`)); } - const { serializer, pluginLocation } = entry; + const { serializer, plugin } = entry; - const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, pluginLocation); + const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); const revivedPanel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, toViewColumn(position)!, options, webview); this.webviewPanels.set(viewId, revivedPanel); return serializer.deserializeWebviewPanel(revivedPanel, state); @@ -98,7 +100,7 @@ export class WebviewsExtImpl implements WebviewsExt { title: string, showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions, - pluginLocation: URI + plugin: Plugin ): theia.WebviewPanel { if (!this.initData) { throw new Error('Webviews are not initialized'); @@ -107,7 +109,7 @@ export class WebviewsExtImpl implements WebviewsExt { const viewId = v4(); this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, options); - const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, pluginLocation); + const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, webviewShowOptions, options, webview); this.webviewPanels.set(viewId, panel); return panel; @@ -116,13 +118,13 @@ export class WebviewsExtImpl implements WebviewsExt { registerWebviewPanelSerializer( viewType: string, serializer: theia.WebviewPanelSerializer, - pluginLocation: URI + plugin: Plugin ): theia.Disposable { if (this.serializers.has(viewType)) { throw new Error(`Serializer for '${viewType}' already registered`); } - this.serializers.set(viewType, { serializer, pluginLocation }); + this.serializers.set(viewType, { serializer, plugin }); this.proxy.$registerSerializer(viewType); return new Disposable(() => { @@ -156,7 +158,7 @@ export class WebviewImpl implements theia.Webview { options: theia.WebviewOptions, private readonly initData: WebviewInitData, private readonly workspace: WorkspaceExtImpl, - private readonly pluginLocation: URI + readonly plugin: Plugin ) { this._options = options; } @@ -206,7 +208,7 @@ export class WebviewImpl implements theia.Webview { ...newOptions, localResourceRoots: newOptions.localResourceRoots || [ ...(this.workspace.workspaceFolders || []).map(x => x.uri), - this.pluginLocation, + URI.file(this.plugin.pluginPath) ] }); this._options = newOptions; @@ -231,6 +233,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { private _active = true; private _visible = true; private _showOptions: theia.WebviewPanelShowOptions; + private _iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined; readonly onDisposeEmitter = new Emitter(); public readonly onDidDispose: Event = this.onDisposeEmitter.event; @@ -243,7 +246,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { private readonly _viewType: string, private _title: string, showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, - private readonly _options: theia.WebviewPanelOptions | undefined, + private readonly _options: theia.WebviewPanelOptions, private readonly _webview: WebviewImpl ) { this._showOptions = typeof showOptions === 'object' ? showOptions : { viewColumn: showOptions as theia.ViewColumn }; @@ -283,19 +286,15 @@ export class WebviewPanelImpl implements theia.WebviewPanel { } } - set iconPath(iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri }) { + get iconPath(): theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined { + return this._iconPath; + } + + set iconPath(iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined) { this.checkIsDisposed(); - if (URI.isUri(iconPath)) { - if ('http' === iconPath.scheme || 'https' === iconPath.scheme) { - this.proxy.$setIconPath(this.viewId, iconPath.toString()); - } else { - this.proxy.$setIconPath(this.viewId, (iconPath).path); - } - } else { - this.proxy.$setIconPath(this.viewId, { - light: (<{ light: theia.Uri; dark: theia.Uri }>iconPath).light.path, - dark: (<{ light: theia.Uri; dark: theia.Uri }>iconPath).dark.path - }); + if (this._iconPath !== iconPath) { + this._iconPath = iconPath; + this.proxy.$setIconPath(this.viewId, PluginIconPath.toUrl(iconPath, this._webview.plugin)); } } @@ -306,7 +305,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { get options(): theia.WebviewPanelOptions { this.checkIsDisposed(); - return this._options!; + return this._options; } get viewColumn(): theia.ViewColumn | undefined {