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

Support using theme icons in webview iconPath #92122

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class HighlightedLabel {
private highlights: IHighlight[] = [];
private didEverRender: boolean = false;

constructor(container: HTMLElement, private supportCodicons: boolean) {
constructor(container: HTMLElement, private readonly supportCodicons: boolean) {
this.domNode = document.createElement('span');
this.domNode.className = 'monaco-highlighted-label';

Expand Down
11 changes: 6 additions & 5 deletions src/vs/base/browser/ui/iconLabel/iconLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { IMatch } from 'vs/base/common/filters';
import { Disposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
import { equals } from 'vs/base/common/objects';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';

export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
Expand Down Expand Up @@ -112,7 +114,7 @@ export class IconLabel extends Disposable {
if (options?.supportHighlights) {
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
} else {
this.nameNode = new Label(nameContainer);
this.nameNode = new Label(nameContainer, options?.supportCodicons);
}

if (options?.supportDescriptionHighlights) {
Expand Down Expand Up @@ -170,7 +172,7 @@ class Label {
private singleLabel: HTMLElement | undefined = undefined;
private options: IIconLabelValueOptions | undefined;

constructor(private container: HTMLElement) { }
constructor(private readonly container: HTMLElement, private readonly supportCodicons = false) { }

setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
Expand All @@ -186,8 +188,7 @@ class Label {
dom.removeClass(this.container, 'multiple');
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
}

this.singleLabel.textContent = label;
this.singleLabel.innerHTML = this.supportCodicons ? renderCodicons(escape(label)) : escape(label);
} else {
this.container.innerHTML = '';
dom.addClass(this.container, 'multiple');
Expand Down Expand Up @@ -233,7 +234,7 @@ class LabelWithHighlights {
private singleLabel: HighlightedLabel | undefined = undefined;
private options: IIconLabelValueOptions | undefined;

constructor(private container: HTMLElement, private supportCodicons: boolean) { }
constructor(private readonly container: HTMLElement, private readonly supportCodicons: boolean) { }

setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6430,7 +6430,7 @@ declare module 'vscode' {
/**
* Icon for the panel shown in UI.
*/
iconPath?: Uri | { light: Uri; dark: Uri };
iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon;

/**
* Webview belonging to the panel.
Expand Down
23 changes: 16 additions & 7 deletions src/vs/workbench/api/browser/mainThreadWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, IReference, dispose } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { basename } from 'vs/base/common/path';
import { isWeb } from 'vs/base/common/platform';
Expand All @@ -21,6 +21,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
Expand Down Expand Up @@ -196,9 +197,9 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
webview.setName(value);
}

public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | extHostProtocol.ThemeIconDto | undefined): void {
const webview = this.getWebviewInput(handle);
webview.iconPath = reviveWebviewIcon(value);
webview.webviewIconPath = reviveWebviewIcon(value);
}

public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
Expand Down Expand Up @@ -505,11 +506,19 @@ function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptio
}

function reviveWebviewIcon(
value: { light: UriComponents, dark: UriComponents; } | undefined
value: extHostProtocol.WebviewUriIconDto | extHostProtocol.ThemeIconDto | undefined
): WebviewIcons | undefined {
return value
? { light: URI.revive(value.light), dark: URI.revive(value.dark) }
: undefined;
if (!value) {
return undefined;
}
if ((value as extHostProtocol.ThemeIconDto).id) {
return value as ThemeIcon;
}

return {
light: URI.revive((value as extHostProtocol.WebviewUriIconDto).light),
dark: URI.revive((value as extHostProtocol.WebviewUriIconDto).dark),
};
}

namespace HotExitState {
Expand Down
15 changes: 12 additions & 3 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ export interface TransferQuickPickItems extends quickInput.IQuickPickItem {

export interface TransferQuickInputButton {
handle: number;
iconPath: { dark: URI; light?: URI; } | { id: string; };
iconPath: { dark: URI; light?: URI; } | ThemeIconDto;
tooltip?: string;
}

Expand Down Expand Up @@ -573,12 +573,17 @@ export interface WebviewExtensionDescription {
readonly location: UriComponents;
}

export interface WebviewUriIconDto {
readonly light: UriComponents;
readonly dark: UriComponents;
}

export interface MainThreadWebviewsShape extends IDisposable {
$createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void;
$disposeWebview(handle: WebviewPanelHandle): void;
$reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void;
$setTitle(handle: WebviewPanelHandle, value: string): void;
$setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void;
$setIconPath(handle: WebviewPanelHandle, value: WebviewUriIconDto | ThemeIconDto | undefined): void;

$setHtml(handle: WebviewPanelHandle, value: string): void;
$setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void;
Expand Down Expand Up @@ -1102,7 +1107,7 @@ export interface IWorkspaceEditEntryMetadataDto {
needsConfirmation: boolean;
label: string;
description?: string;
iconPath?: { id: string } | UriComponents | { light: UriComponents, dark: UriComponents };
iconPath?: ThemeIconDto | UriComponents | { light: UriComponents, dark: UriComponents };
}

export interface IWorkspaceFileEditDto {
Expand Down Expand Up @@ -1288,6 +1293,10 @@ export interface ITerminalDimensionsDto {
rows: number;
}

export interface ThemeIconDto {
readonly id: string;
}

export interface ExtHostTerminalServiceShape {
$acceptTerminalClosed(id: number, exitCode: number | undefined): void;
$acceptTerminalOpened(id: number, name: string, shellLaunchConfig: IShellLaunchConfigDto): void;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHostWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
import type * as vscode from 'vscode';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewExtensionDescription, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol';
import { Disposable as VSCodeDisposable } from './extHostTypes';
import { Disposable as VSCodeDisposable, ThemeIcon } from './extHostTypes';

type IconPath = URI | { light: URI, dark: URI };
type IconPath = URI | { readonly light: URI; readonly dark: URI } | ThemeIcon;

export class ExtHostWebview implements vscode.Webview {
private _html: string = '';
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/browser/parts/editor/media/titlecontrol.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
cursor: pointer;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .codicon {
vertical-align: middle;
}

/* Title Actions */

.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label,
Expand Down
10 changes: 7 additions & 3 deletions src/vs/workbench/browser/parts/editor/tabsTitleControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { basenameOrAuthority } from 'vs/base/common/resources';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
import { IPath, win32, posix } from 'vs/base/common/path';
import { escapeCodicons } from 'vs/base/common/codicons';

interface IEditorInputLabel {
name?: string;
Expand Down Expand Up @@ -493,7 +494,9 @@ export class TabsTitleControl extends TitleControl {
tabContainer.appendChild(tabBorderTopContainer);

// Tab Editor Label
const editorLabel = this.tabResourceLabels.create(tabContainer);
const editorLabel = this.tabResourceLabels.create(tabContainer, {
supportCodicons: true,
});

// Tab Close Button
const tabCloseContainer = document.createElement('div');
Expand Down Expand Up @@ -954,7 +957,7 @@ export class TabsTitleControl extends TitleControl {
}

private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void {
const name = tabLabel.name;
const name = tabLabel.name || '';
const description = tabLabel.description || '';
const title = tabLabel.title || '';

Expand All @@ -963,8 +966,9 @@ export class TabsTitleControl extends TitleControl {
tabContainer.title = title;

// Label
const displayName = editor.iconPath ? `$(${editor.iconPath.id}) ${escapeCodicons(name)}` : escapeCodicons(name);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This logic is ugly and has to duplicated. We should look into pushing the icon logic into ResourceLabel

const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER });
tabLabelWidget.setResource({ name, description, resource }, { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) });
tabLabelWidget.setResource({ name: displayName, description, resource }, { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) });

// Tests helper
if (resource) {
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/common/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { createMemoizer } from 'vs/base/common/decorators';
import { ILabelService } from 'vs/platform/label/common/label';
import { Schemas } from 'vs/base/common/network';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';

export const DirtyWorkingCopiesContext = new RawContextKey<boolean>('dirtyWorkingCopies', false);
export const ActiveEditorContext = new RawContextKey<string | null>('activeEditor', null);
Expand Down Expand Up @@ -363,6 +364,11 @@ export interface IEditorInput extends IDisposable {
*/
readonly resource: URI | undefined;

/**
* Custom icon displayed for this resource.
*/
readonly iconPath?: ThemeIcon;

/**
* Unique type identifier for this inpput.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/w
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { escapeCodicons } from 'vs/base/common/codicons';

const $ = dom.$;

Expand Down Expand Up @@ -585,7 +586,7 @@ class OpenEditorRenderer implements IListRenderer<OpenEditor, IOpenEditorTemplat
const key = this.keybindingService.lookupKeybinding(closeEditorAction.id);
editorTemplate.actionBar.push(closeEditorAction, { icon: true, label: false, keybinding: key ? key.getLabel() : undefined });

editorTemplate.root = this.labels.create(container);
editorTemplate.root = this.labels.create(container, { supportCodicons: true });

return editorTemplate;
}
Expand All @@ -594,9 +595,10 @@ class OpenEditorRenderer implements IListRenderer<OpenEditor, IOpenEditorTemplat
const editor = openedEditor.editor;
templateData.actionRunner.editor = openedEditor;
editor.isDirty() && !editor.isSaving() ? dom.addClass(templateData.container, 'dirty') : dom.removeClass(templateData.container, 'dirty');
const displayName = editor.iconPath ? `$(${editor.iconPath.id}) ${escapeCodicons(editor.getName())}` : escapeCodicons(editor.getName());
templateData.root.setResource({
resource: toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }),
name: editor.getName(),
name: displayName,
description: editor.getDescription(Verbosity.MEDIUM)
}, {
italic: openedEditor.isPreview(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class ReleaseNotesManager {
this._currentReleaseNotes.onDispose(() => { this._currentReleaseNotes = undefined; });

const iconPath = URI.parse(require.toUrl('./media/code-icon.svg'));
this._currentReleaseNotes.iconPath = {
this._currentReleaseNotes.webviewIconPath = {
light: iconPath,
dark: iconPath
};
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/contrib/webview/browser/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Dimension } from 'vs/base/browser/dom';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
Expand All @@ -12,7 +13,7 @@ import * as nls from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';

/**
* Set when the find widget in a webview is visible.
Expand All @@ -25,10 +26,10 @@ export const webviewHasOwnEditFunctionsContext = new RawContextKey<boolean>(webv

export const IWebviewService = createDecorator<IWebviewService>('webviewService');

export interface WebviewIcons {
export type WebviewIcons = ThemeIcon | {
readonly light: URI;
readonly dark: URI;
}
};

/**
* Handles the creation of webview elements.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Lazy } from 'vs/base/common/lazy';
import { URI } from 'vs/base/common/uri';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { EditorInput, GroupIdentifier, IEditorInput, Verbosity } from 'vs/workbench/common/editor';
import { IWebviewService, WebviewIcons, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';

Expand Down Expand Up @@ -78,11 +79,15 @@ export class WebviewInput extends EditorInput {
return this.webview.extension;
}

public get iconPath() {
get iconPath(): ThemeIcon | undefined {
return ThemeIcon.isThemeIcon(this._iconPath) ? this._iconPath : undefined;
}

public get webviewIconPath() {
return this._iconPath;
}

public set iconPath(value: WebviewIcons | undefined) {
public set webviewIconPath(value: WebviewIcons | undefined) {
this._iconPath = value;
this._webviewService.setIcons(this.id, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IEditorInputFactory } from 'vs/workbench/common/editor';
import { WebviewInput } from './webviewEditorInput';
import { IWebviewWorkbenchService, WebviewInputOptions } from './webviewWorkbenchService';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';

interface SerializedIconPath {
light: string | UriComponents;
dark: string | UriComponents;
}
type SerializedIconPath = ThemeIcon | {
readonly light: string | UriComponents;
readonly dark: string | UriComponents;
};

interface SerializedWebview {
readonly id?: string;
Expand Down Expand Up @@ -84,17 +86,23 @@ export class WebviewEditorInputFactory implements IEditorInputFactory {
extensionLocation: input.extension ? input.extension.location : undefined,
extensionId: input.extension && input.extension.id ? input.extension.id.value : undefined,
state: input.webview.state,
iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined,
iconPath: input.webviewIconPath
? ThemeIcon.isThemeIcon(input.webviewIconPath) ? input.webviewIconPath : { light: input.webviewIconPath.light, dark: input.webviewIconPath.dark, }
: undefined,
group: input.group
};
}
}

function reviveIconPath(data: SerializedIconPath | undefined) {
function reviveIconPath(data: SerializedIconPath | undefined): WebviewIcons | undefined {
if (!data) {
return undefined;
}

if (ThemeIcon.isThemeIcon(data)) {
return data;
}

const light = reviveUri(data.light);
const dark = reviveUri(data.dark);
return light && dark ? { light, dark } : undefined;
Expand Down
Loading