Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow extensions to contribute custom icons for webview panels #49657

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
"onView:markdown.preview"
],
"contributes": {
"webviews": [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially tried putting this new point under views, but now that we support custom view containers it does not seem like a great fit there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by 'custom view containers'?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom activity bar sections that we added last iteration. I see two concerns with reusing views for webview contributions:

  • Webview contributions would not have the same properties as the other items in views.
  • As we now support contributing a custom view container with any name, there's nothing to stop someone from creating a view container named webviews. Pretty unlikely this will be a real problem so the first point is the main concern

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it has to be merged into the views extension point, then editor is the right location for web views instead of webviews. IMO Web view and Tree view are types of views. I see that at present Tree views and Web views do not have common schema, for e.g. Tree views do not have icons where as web views do. We can have either schema by type or schema by location depends on how they emerge in future. Looking at present scenario, it looks to me that the location drives the properties. I mean, views registered in activity bar containers do not need icons but icons are needed for views registered under editor. In future, if we want to be flexible in moving views across, then all views can define icons optionally. Not sure what is viewType property in web views. May be this can be used to differentiate tree views vs web views?

I think the second point you mentioned is a valid concern. May be we should think of restricting some default locations like editor or panel.

Or other approach would be to use viewsContainersextension point? Say, introduce a new locationeditor` and create a new view container for web views. It means a view container inside editor can contain multiple views(web or tree).

{
"viewType": "markdown.preview",
"icon": {
"light": "./media/Preview.svg",
"dark": "./media/Preview_inverse.svg"
}
}
],
"commands": [
{
"command": "markdown.showPreview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWe
import { WebviewEditor } from './webviewEditor';
import { WebviewEditorInput } from './webviewEditorInput';
import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService';
import './webviewExtensionPoint';

(Registry.as<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
WebviewEditor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export class WebviewEditorInput extends EditorInput {
}

public getResource(): URI {
return null;
return URI.from({
scheme: 'webview-panel',
path: this.state ? `webview-panel/${this.state.viewType}` : ''
});
}

public getName(): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { join } from 'path';
import * as dom from 'vs/base/browser/dom';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';

namespace schema {

export interface IUserFriendlyWebviewDescriptor {
viewType: string;
icon?: {
light: string;
dark: string;
};
}

export function isValidViewDescriptors(viewDescriptors: IUserFriendlyWebviewDescriptor[], collector: ExtensionMessageCollector): boolean {
if (!Array.isArray(viewDescriptors)) {
collector.error(localize('requirearray', "views must be an array"));
return false;
}

for (let descriptor of viewDescriptors) {
if (typeof descriptor.viewType !== 'string') {
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'viewType'));
return false;
}

if (descriptor.icon) {
if (typeof descriptor.icon.dark !== 'string') {
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon.dark'));
return false;
}
if (typeof descriptor.icon.light !== 'string') {
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon.light'));
return false;
}
}
}

return true;
}

const webviewDescriptor: IJSONSchema = {
type: 'object',
properties: {
viewType: {
description: localize('vscode.extension.contributes.webview.viewType', 'The unique identifier of the view.'),
type: 'string'
},
icon: {
type: 'object',
properties: {
light: {
type: 'string'
},
dark: {
type: 'string'
}
}
}
}
};

export const webviewsContribution: IJSONSchema = {
description: localize('vscode.extension.contributes.webviews', "Contributes webviews to the editor"),
type: 'array',
items: webviewDescriptor,
default: []
};
}


ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyWebviewDescriptor[]>('webviews', [], schema.webviewsContribution)
.setHandler((extensions) => {
for (let extension of extensions) {
const { value, collector } = extension;

if (!schema.isValidViewDescriptors(value, collector)) {
return;
}

const viewIds: string[] = [];
const viewDescriptors: IWebviewDescriptor[] = value.map(item => {
const viewDescriptor = <IWebviewDescriptor>{
viewType: item.viewType,
icon: item.icon ? {
light: join(extension.description.extensionFolderPath, item.icon.light),
dark: join(extension.description.extensionFolderPath, item.icon.dark),
} : undefined
};

// validate
if (viewIds.indexOf(viewDescriptor.viewType) !== -1) {
collector.error(localize('duplicateView1', "Cannot register multiple webview with same viewtype `{0}`", viewDescriptor.viewType));
return null;
}
// if (registeredViews.some(v => v.id === viewDescriptor.id)) {
// collector.error(localize('duplicateView2', "A view with id `{0}` is already registered in the location `{1}`", viewDescriptor.id, viewDescriptor.location.id));
// return null;
// }

viewIds.push(viewDescriptor.viewType);
return viewDescriptor;
});

WebviewsRegistry.registerViews(viewDescriptors);
}
});

export interface IWebviewDescriptor {
viewType: string;
icon?: {
light: string;
dark: string;
};
}

export const WebviewsRegistry = new class {
readonly _webviews = new Map<string, IWebviewDescriptor>();
_styleElement: HTMLStyleElement;

constructor() {
this._styleElement = dom.createStyleSheet();
this._styleElement.className = 'webview-icons';
}

public get(viewType: string): IWebviewDescriptor | undefined {
return this._webviews.get(viewType);
}

public registerViews(views: IWebviewDescriptor[]) {
const cssRules: string[] = [];
for (const view of views) {
this._webviews.set(view.viewType, view);
if (view.icon) {
cssRules.push(`.show-file-icons .${escapeCSS(view.viewType)}-name-file-icon::before { background-image: url(${view.icon.light}); }`);
}
}
this._styleElement.innerHTML += cssRules.join('\n');
}
};

function escapeCSS(str: string) {
return window['CSS'].escape(str);
}