fine tune hover and extension status

This commit is contained in:
Sandeep Somavarapu 2021-07-22 15:32:18 +02:00
parent 2feb2fc6b5
commit 5d8d47ff3a
No known key found for this signature in database
GPG key ID: 1FED25EC4646638B
5 changed files with 221 additions and 237 deletions

View file

@ -13,7 +13,7 @@ import { Cache, CacheResult } from 'vs/base/common/cache';
import { Action, IAction } from 'vs/base/common/actions';
import { getErrorMessage, isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { append, $, finalHandler, join, hide, show, addDisposableListener, EventType, setParentFlowTo, reset } from 'vs/base/browser/dom';
import { append, $, finalHandler, join, addDisposableListener, EventType, setParentFlowTo, reset } from 'vs/base/browser/dom';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -28,7 +28,7 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import {
UpdateAction, ReloadAction, EnableDropDownAction, DisableDropDownAction, ExtensionStatusLabelAction, SetFileIconThemeAction, SetColorThemeAction,
RemoteInstallAction, ExtensionToolTipAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction,
RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction,
ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction,
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
@ -49,7 +49,7 @@ import { KeybindingParser } from 'vs/base/common/keybindingParser';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry';
import { isUndefined } from 'vs/base/common/types';
import { isString, isUndefined } from 'vs/base/common/types';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
@ -70,7 +70,8 @@ import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { errorIcon, infoIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { errorIcon, infoIcon, starEmptyIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { MarkdownString } from 'vs/base/common/htmlContent';
class NavBar extends Disposable {
@ -140,11 +141,10 @@ interface IExtensionEditorTemplate {
rating: HTMLElement;
description: HTMLElement;
extensionActionBar: ActionBar;
status: HTMLElement;
recommendation: HTMLElement;
navbar: NavBar;
content: HTMLElement;
subtextContainer: HTMLElement;
subtext: HTMLElement;
ignoreActionbar: ActionBar;
header: HTMLElement;
}
@ -227,8 +227,8 @@ export class ExtensionEditor extends EditorPane {
const description = append(details, $('.description'));
const extensionActions = append(details, $('.actions'));
const extensionActionBar = this._register(new ActionBar(extensionActions, {
const extensionActionsContainer = append(details, $('.actions'));
const extensionActionBar = this._register(new ActionBar(extensionActionsContainer, {
animated: false,
actionViewItemProvider: (action: IAction) => {
if (action instanceof ExtensionDropDownAction) {
@ -242,20 +242,14 @@ export class ExtensionEditor extends EditorPane {
focusOnlyEnabledItems: true
}));
const subtextContainer = append(details, $('.subtext-container'));
const subtext = append(subtextContainer, $('.subtext'));
const ignoreActionbar = this._register(new ActionBar(subtextContainer, { animated: false }));
const status = append(extensionActionsContainer, $('.status'));
const recommendation = append(details, $('.recommendation'));
this._register(Event.chain(extensionActionBar.onDidRun)
.map(({ error }) => error)
.filter(error => !!error)
.on(this.onError, this));
this._register(Event.chain(ignoreActionbar.onDidRun)
.map(({ error }) => error)
.filter(error => !!error)
.on(this.onError, this));
const body = append(root, $('.body'));
const navbar = new NavBar(body);
@ -266,20 +260,19 @@ export class ExtensionEditor extends EditorPane {
builtin,
content,
description,
extensionActionBar,
header,
icon,
iconContainer,
version,
ignoreActionbar,
installCount,
name,
navbar,
preview,
publisher,
rating,
subtext,
subtextContainer
extensionActionBar,
status,
recommendation
};
}
@ -381,7 +374,6 @@ export class ExtensionEditor extends EditorPane {
];
const reloadAction = this.instantiationService.createInstance(ReloadAction);
const combinedInstallAction = this.instantiationService.createInstance(InstallDropdownAction);
const systemDisabledWarningAction = this.instantiationService.createInstance(ExtensionStatusAction);
const actions = [
reloadAction,
this.instantiationService.createInstance(ExtensionStatusLabelAction),
@ -403,10 +395,9 @@ export class ExtensionEditor extends EditorPane {
]),
this.instantiationService.createInstance(ToggleSyncExtensionAction),
this.instantiationService.createInstance(ExtensionEditorManageExtensionAction),
systemDisabledWarningAction,
this.instantiationService.createInstance(ExtensionToolTipAction, systemDisabledWarningAction, reloadAction),
];
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]);
const extensionStatus = this.instantiationService.createInstance(ExtensionStatusAction);
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, extensionStatus]);
extensionContainers.extension = extension;
template.extensionActionBar.clear();
@ -416,7 +407,9 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.add(disposable);
}
this.setSubText(extension, template);
this.setStatus(extensionStatus, template);
this.setRecommendationText(extension, template);
template.content.innerText = ''; // Clear content before setting navbar actions.
template.navbar.clear();
@ -462,24 +455,40 @@ export class ExtensionEditor extends EditorPane {
this.editorLoadComplete = true;
}
private setSubText(extension: IExtension, template: IExtensionEditorTemplate): void {
hide(template.subtextContainer);
const updateRecommendationFn = () => {
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText;
show(template.subtextContainer);
} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(extension.identifier.id.toLowerCase()) !== -1) {
template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
show(template.subtextContainer);
} else {
template.subtext.textContent = '';
hide(template.subtextContainer);
private setStatus(extensionStatus: ExtensionStatusAction, template: IExtensionEditorTemplate): void {
const updateStatus = () => {
reset(template.status);
const status = extensionStatus.status;
if (status) {
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
if (status.icon) {
markdown.appendMarkdown(`$(${status.icon.id}) `);
}
if (isString(status.message)) {
markdown.appendText(status.message);
} else {
markdown.appendMarkdown(status.message.value);
}
append(template.status, renderMarkdown(markdown));
}
};
updateRecommendationFn();
this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationFn()));
updateStatus();
this.transientDisposables.add(extensionStatus.onDidChangeStatus(() => updateStatus()));
}
private setRecommendationText(extension: IExtension, template: IExtensionEditorTemplate): void {
const updateRecommendationText = () => {
reset(template.recommendation);
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
append(template.recommendation, $(`span${ThemeIcon.asCSSSelector(starEmptyIcon)}`));
append(template.recommendation, $(`span.recommendation-text`, undefined, extRecommendations[extension.identifier.id.toLowerCase()].reasonText));
} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(extension.identifier.id.toLowerCase()) !== -1) {
append(template.recommendation, $(`span.recommendation-text`, undefined, localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.")));
}
};
updateRecommendationText();
this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationText()));
}
override clearInput(): void {
@ -1615,12 +1624,16 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
const link = theme.getColor(textLinkForeground);
if (link) {
collector.addRule(`.monaco-workbench .extension-editor .content a { color: ${link}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions > .status a { color: ${link}; }`);
}
const activeLink = theme.getColor(textLinkActiveForeground);
if (activeLink) {
collector.addRule(`.monaco-workbench .extension-editor .content a:hover,
.monaco-workbench .extension-editor .content a:active { color: ${activeLink}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions > .status a:hover,
.monaco-workbench .extension-editor > .header > .details > .actions > .status a:active { color: ${activeLink}; }`);
}
const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);

View file

@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions';
import { Delayer, Promises } from 'vs/base/common/async';
import * as DOM from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import * as json from 'vs/base/common/json';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { dispose } from 'vs/base/common/lifecycle';
@ -62,6 +62,7 @@ import { isIOS, isWeb } from 'vs/base/common/platform';
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
function getRelativeDateLabel(date: Date): string {
const delta = new Date().getTime() - date.getTime();
@ -1957,82 +1958,7 @@ export class ToggleSyncExtensionAction extends ExtensionDropDownAction {
}
}
export class ExtensionToolTipAction extends ExtensionAction {
private static readonly Class = `${ExtensionAction.TEXT_ACTION_CLASS} disable-status`;
updateWhenCounterExtensionChanges: boolean = true;
private _runningExtensions: IExtensionDescription[] | null = null;
constructor(
private readonly extensionStatusIconAction: ExtensionStatusAction,
private readonly reloadAction: ReloadAction,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IExtensionService private readonly extensionService: IExtensionService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService
) {
super('extensions.tooltip', extensionStatusIconAction.statusMessage, `${ExtensionToolTipAction.Class} hide`, false);
this._register(extensionStatusIconAction.onDidChange(() => this.update(), this));
this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this));
this.updateRunningExtensions();
}
private updateRunningExtensions(): void {
this.extensionService.getExtensions().then(runningExtensions => { this._runningExtensions = runningExtensions; this.update(); });
}
update(): void {
this.label = this.getTooltip();
this.class = ExtensionToolTipAction.Class;
if (!this.label) {
this.class = `${ExtensionToolTipAction.Class} hide`;
}
}
private getTooltip(): string {
if (!this.extension) {
return '';
}
if (this.reloadAction.enabled) {
return this.reloadAction.tooltip;
}
if (this.extensionStatusIconAction.statusMessage) {
return this.extensionStatusIconAction.statusMessage;
}
if (this.extension && this.extension.local && this.extension.state === ExtensionState.Installed && this._runningExtensions) {
const isRunning = this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier));
const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local);
if (isEnabled && isRunning) {
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) {
return localize('extension enabled on remote', "Extension is enabled on '{0}'", this.extension.server.label);
}
}
if (this.extension.enablementState === EnablementState.EnabledGlobally) {
return localize('globally enabled', "This extension is enabled globally.");
}
if (this.extension.enablementState === EnablementState.EnabledWorkspace) {
return localize('workspace enabled', "This extension is enabled for this workspace by the user.");
}
}
if (!isEnabled && !isRunning) {
if (this.extension.enablementState === EnablementState.DisabledGlobally) {
return localize('globally disabled', "This extension is disabled globally by the user.");
}
if (this.extension.enablementState === EnablementState.DisabledWorkspace) {
return localize('workspace disabled', "This extension is disabled for this workspace by the user.");
}
}
}
return '';
}
override run(): Promise<any> {
return Promise.resolve(null);
}
}
export type ExtensionStatus = { readonly message: IMarkdownString | string, readonly icon?: ThemeIcon };
export class ExtensionStatusAction extends ExtensionAction {
@ -2041,11 +1967,11 @@ export class ExtensionStatusAction extends ExtensionAction {
updateWhenCounterExtensionChanges: boolean = true;
private _runningExtensions: IExtensionDescription[] | null = null;
private _statusIcon: ThemeIcon | undefined;
get statusIcon(): ThemeIcon | undefined { return this._statusIcon; }
private _status: ExtensionStatus | undefined;
get status(): ExtensionStatus | undefined { return this._status; }
private _statusMessage: string | undefined;
get statusMessage(): string | undefined { return this._statusMessage; }
private readonly _onDidChangeStatus = this._register(new Emitter<void>());
readonly onDidChangeStatus = this._onDidChangeStatus.event;
constructor(
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@ -2060,7 +1986,7 @@ export class ExtensionStatusAction extends ExtensionAction {
@IProductService private readonly productService: IProductService,
@IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService,
) {
super('extensions.install', '', `${ExtensionStatusAction.CLASS} hide`, false);
super('extensions.status', '', `${ExtensionStatusAction.CLASS} hide`, false);
this._register(this.labelService.onDidChangeFormatters(() => this.update(), this));
this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this));
this.updateRunningExtensions();
@ -2072,9 +1998,7 @@ export class ExtensionStatusAction extends ExtensionAction {
}
update(): void {
this.class = `${ExtensionStatusAction.CLASS} hide`;
this._statusIcon = undefined;
this.updateStatusMessage('');
this.updateStatus(undefined, true);
this.enabled = false;
if (!this.extension) {
@ -2083,20 +2007,20 @@ export class ExtensionStatusAction extends ExtensionAction {
if (this.extension.state === ExtensionState.Uninstalled && !this.extensionsWorkbenchService.canInstall(this.extension) && this.extension.gallery) {
if (this.extension.isMalicious) {
this.updateStatusIcon(warningIcon);
this.updateStatusMessage(localize('malicious tooltip', "This extension was reported to be problematic."));
this.updateStatus({ icon: warningIcon, message: localize('malicious tooltip', "This extension was reported to be problematic.") }, true);
return;
}
if (this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer
&& !this.extensionManagementServerService.remoteExtensionManagementServer) {
const productName = isWeb ? localize({ key: 'vscode web', comment: ['VS Code Web is the name of the product'] }, "VS Code Web") : this.productService.nameLong;
this.updateStatusIcon(infoIcon);
let message;
if (this.extension.gallery.webExtension) {
this.updateStatusMessage(localize('user disabled', "You have configured the '{0}' extension to be disabled in {1}. To enable it, please open user settings and remove it from `remote.extensionKind` setting.", this.extension.displayName || this.extension.identifier.id, productName));
message = localize('user disabled', "You have configured the '{0}' extension to be disabled in {1}. To enable it, please open user settings and remove it from `remote.extensionKind` setting.", this.extension.displayName || this.extension.identifier.id, productName);
} else {
this.updateStatusMessage(localize('not web tooltip', "The '{0}' extension is not available in {1}.", this.extension.displayName || this.extension.identifier.id, productName));
message = new MarkdownString(`${localize('not web tooltip', "The '{0}' extension is not available in {1}. Click {2} to learn more.", this.extension.displayName || this.extension.identifier.id, productName, `[${localize('more info', "More Information")}](https://aka.ms/vscode-remote-codespaces#_why-is-an-extension-not-installable-in-the-browser)`)}`);
}
this.updateStatus({ icon: infoIcon, message }, true);
return;
}
}
@ -2111,21 +2035,20 @@ export class ExtensionStatusAction extends ExtensionAction {
// Extension is disabled by environment
if (this.extension.enablementState === EnablementState.DisabledByEnvironment) {
this.updateStatusMessage(localize('disabled by environment', "This extension is disabled by the environment."));
this.updateStatus({ message: localize('disabled by environment', "This extension is disabled by the environment.") }, true);
return;
}
// Extension is enabled by environment
if (this.extension.enablementState === EnablementState.EnabledByEnvironment) {
this.updateStatusMessage(localize('enabled by environment', "This extension is enabled because it is required in the current environment."));
this.updateStatus({ message: localize('enabled by environment', "This extension is enabled because it is required in the current environment.") }, true);
return;
}
// Extension is disabled by virtual workspace
if (this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace) {
const details = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.virtualWorkspaces);
this.updateStatusIcon(infoIcon);
this.updateStatusMessage(details || localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces."));
this.updateStatus({ icon: infoIcon, message: details || localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces.") }, true);
return;
}
@ -2134,8 +2057,7 @@ export class ExtensionStatusAction extends ExtensionAction {
const virtualSupportType = this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(this.extension.local.manifest);
const details = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.virtualWorkspaces);
if (virtualSupportType === 'limited' || details) {
this.updateStatusIcon(infoIcon);
this.updateStatusMessage(details || localize('extension limited because of virtual workspace', "This extension has limited features because the current workspace is virtual."));
this.updateStatus({ icon: infoIcon, message: details || localize('extension limited because of virtual workspace', "This extension has limited features because the current workspace is virtual.") }, true);
return;
}
}
@ -2145,9 +2067,8 @@ export class ExtensionStatusAction extends ExtensionAction {
// All disabled dependencies of the extension are disabled by untrusted workspace
(this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.workbenchExtensionEnablementService.getDependenciesEnablementStates(this.extension.local).every(([, enablementState]) => this.workbenchExtensionEnablementService.isEnabledEnablementState(enablementState) || enablementState === EnablementState.DisabledByTrustRequirement))) {
this.enabled = true;
this.updateStatusIcon(trustIcon);
const untrustedDetails = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces);
this.updateStatusMessage(untrustedDetails || localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted."));
this.updateStatus({ icon: trustIcon, message: untrustedDetails || localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted.") }, true);
return;
}
@ -2157,8 +2078,7 @@ export class ExtensionStatusAction extends ExtensionAction {
const untrustedDetails = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces);
if (untrustedSupportType === 'limited' || untrustedDetails) {
this.enabled = true;
this.updateStatusIcon(trustIcon);
this.updateStatusMessage(untrustedDetails || localize('extension limited because of trust requirement', "This extension has limited features because the current workspace is not trusted."));
this.updateStatus({ icon: trustIcon, message: untrustedDetails || localize('extension limited because of trust requirement', "This extension has limited features because the current workspace is not trusted.") }, true);
return;
}
}
@ -2167,12 +2087,13 @@ export class ExtensionStatusAction extends ExtensionAction {
if (this.extension.enablementState === EnablementState.DisabledByExtensionKind) {
if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)) {
const server = this.extensionManagementServerService.localExtensionManagementServer === this.extension.server ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer;
this.updateStatusIcon(warningIcon);
let message;
if (server) {
this.updateStatusMessage(localize('Install in other server to enable', "Install the extension on '{0}' to enable.", server.label));
message = localize('Install in other server to enable', "Install the extension on '{0}' to enable.", server.label);
} else {
this.updateStatusMessage(localize('disabled because of extension kind', "This extension has defined that it cannot run on the remote server"));
message = localize('disabled because of extension kind', "This extension has defined that it cannot run on the remote server");
}
this.updateStatus({ icon: warningIcon, message }, true);
return;
}
}
@ -2181,10 +2102,10 @@ export class ExtensionStatusAction extends ExtensionAction {
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
if (isLanguagePackExtension(this.extension.local.manifest)) {
if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)) {
this.updateStatusIcon(infoIcon);
this.updateStatusMessage(this.extension.server === this.extensionManagementServerService.localExtensionManagementServer
const message = this.extension.server === this.extensionManagementServerService.localExtensionManagementServer
? localize('Install language pack also in remote server', "Install the language pack extension on '{0}' to enable it there also.", this.extensionManagementServerService.remoteExtensionManagementServer.label)
: localize('Install language pack also locally', "Install the language pack extension locally to enable it there also."));
: localize('Install language pack also locally', "Install the language pack extension locally to enable it there also.");
this.updateStatus({ icon: infoIcon, message }, true);
}
return;
}
@ -2193,16 +2114,14 @@ export class ExtensionStatusAction extends ExtensionAction {
const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)) : null;
if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) {
if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) {
this.updateStatusIcon(infoIcon);
this.updateStatusMessage(localize('disabled locally', "Extension is enabled on '{0}' and disabled locally.", this.extensionManagementServerService.remoteExtensionManagementServer.label));
this.updateStatus({ icon: infoIcon, message: localize('disabled locally', "Extension is enabled on '{0}' and disabled locally.", this.extensionManagementServerService.remoteExtensionManagementServer.label) }, true);
}
return;
}
if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) {
if (this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) {
this.updateStatusIcon(infoIcon);
this.updateStatusMessage(localize('disabled remotely', "Extension is enabled locally and disabled on '{0}'.", this.extensionManagementServerService.remoteExtensionManagementServer.label));
this.updateStatus({ icon: infoIcon, message: localize('disabled remotely', "Extension is enabled locally and disabled on '{0}'.", this.extensionManagementServerService.remoteExtensionManagementServer.label) }, true);
}
return;
}
@ -2210,36 +2129,67 @@ export class ExtensionStatusAction extends ExtensionAction {
// Extension is disabled by its dependency
if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency) {
this.updateStatusIcon(warningIcon);
this.updateStatusMessage(localize('extension disabled because of dependency', "This extension has been disabled because it depends on an extension that is disabled."));
this.updateStatus({ icon: warningIcon, message: localize('extension disabled because of dependency', "This extension has been disabled because it depends on an extension that is disabled.") }, true);
return;
}
const isEnabled = this.workbenchExtensionEnablementService.isEnabled(this.extension.local);
const isRunning = this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier));
if (isEnabled && isRunning) {
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) {
this.updateStatus({ message: localize('extension enabled on remote', "Extension is enabled on '{0}'", this.extension.server.label) }, true);
return;
}
}
if (this.extension.enablementState === EnablementState.EnabledGlobally) {
this.updateStatus({ message: localize('globally enabled', "This extension is enabled globally.") }, true);
return;
}
if (this.extension.enablementState === EnablementState.EnabledWorkspace) {
this.updateStatus({ message: localize('workspace enabled', "This extension is enabled for this workspace by the user.") }, true);
return;
}
}
if (!isEnabled && !isRunning) {
if (this.extension.enablementState === EnablementState.DisabledGlobally) {
this.updateStatus({ message: localize('globally disabled', "This extension is disabled globally by the user.") }, true);
return;
}
if (this.extension.enablementState === EnablementState.DisabledWorkspace) {
this.updateStatus({ message: localize('workspace disabled', "This extension is disabled for this workspace by the user.") }, true);
return;
}
}
}
private updateStatusIcon(statusIcon: ThemeIcon): void {
this._statusIcon = statusIcon;
if (this.statusIcon === errorIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-error ${ThemeIcon.asClassName(errorIcon)}`;
private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void {
this._status = status;
if (updateClass) {
if (this._status?.icon === errorIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-error ${ThemeIcon.asClassName(errorIcon)}`;
}
else if (this._status?.icon === warningIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-warning ${ThemeIcon.asClassName(warningIcon)}`;
}
else if (this._status?.icon === infoIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-info ${ThemeIcon.asClassName(infoIcon)}`;
}
else if (this._status?.icon === trustIcon) {
this.class = `${ExtensionStatusAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`;
}
else {
this.class = `${ExtensionStatusAction.CLASS} hide`;
}
}
if (this.statusIcon === warningIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-warning ${ThemeIcon.asClassName(warningIcon)}`;
}
if (this.statusIcon === infoIcon) {
this.class = `${ExtensionStatusAction.CLASS} extension-status-info ${ThemeIcon.asClassName(infoIcon)}`;
}
if (this.statusIcon === trustIcon) {
this.class = `${ExtensionStatusAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`;
}
}
private updateStatusMessage(message: string | undefined): void {
this._statusMessage = message;
this._onDidChange.fire({});
this._onDidChangeStatus.fire();
}
override async run(): Promise<any> {
if (this.statusIcon === trustIcon) {
if (this._status?.icon === trustIcon) {
return this.commandService.executeCommand('workbench.trust.manage');
}
}
@ -2687,6 +2637,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-error { color: ${errorColor}; }`);
collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-error { color: ${errorColor}; }`);
collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions > .status ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`);
collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`);
}
@ -2695,6 +2646,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { color: ${warningColor}; }`);
collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { color: ${warningColor}; }`);
collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions > .status ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`);
collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`);
}
@ -2703,6 +2655,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-info { color: ${infoColor}; }`);
collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-info { color: ${infoColor}; }`);
collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions > .status ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`);
collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`);
}
});

View file

@ -13,7 +13,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging';
import { Event } from 'vs/base/common/event';
import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
@ -125,8 +125,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
extensionStatusIconAction,
this.instantiationService.createInstance(ManageExtensionAction)
];
const extensionTooltipAction = this.instantiationService.createInstance(ExtensionToolTipAction, extensionStatusIconAction, reloadAction);
const extensionHoverWidget = this.instantiationService.createInstance(ExtensionHoverWidget, { target: root, position: this.options.hoverOptions.position }, extensionStatusIconAction, extensionTooltipAction);
const extensionHoverWidget = this.instantiationService.createInstance(ExtensionHoverWidget, { target: root, position: this.options.hoverOptions.position }, extensionStatusIconAction, reloadAction);
const widgets = [
recommendationWidget,
@ -139,10 +138,10 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
this.instantiationService.createInstance(InstallCountWidget, installCount, true),
this.instantiationService.createInstance(RatingsWidget, ratings, true),
];
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, extensionTooltipAction]);
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]);
actionbar.push(actions, actionOptions);
const disposable = combinedDisposable(...actions, ...widgets, actionbar, extensionContainers, extensionTooltipAction);
const disposable = combinedDisposable(...actions, ...widgets, actionbar, extensionContainers);
return {
root, element, icon, name, installCount, ratings, description, author, disposables: [disposable], actionbar,

View file

@ -12,8 +12,8 @@ import { localize } from 'vs/nls';
import { EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ILabelService } from 'vs/platform/label/common/label';
import { extensionButtonProminentBackground, extensionButtonProminentForeground, ExtensionStatusAction, ExtensionToolTipAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IThemeService, IColorTheme, ThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { extensionButtonProminentBackground, extensionButtonProminentForeground, ExtensionStatusAction, ReloadAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IThemeService, ThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EXTENSION_BADGE_REMOTE_BACKGROUND, EXTENSION_BADGE_REMOTE_FOREGROUND } from 'vs/workbench/common/theme';
import { Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@ -31,6 +31,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
import Severity from 'vs/base/common/severity';
import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
import { Color } from 'vs/base/common/color';
import { isString } from 'vs/base/common/types';
export abstract class ExtensionWidget extends Disposable implements IExtensionContainer {
private _extension: IExtension | null = null;
@ -165,7 +166,6 @@ export class RecommendationWidget extends ExtensionWidget {
constructor(
private parent: HTMLElement,
@IThemeService private readonly themeService: IThemeService,
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService
) {
super();
@ -192,14 +192,6 @@ export class RecommendationWidget extends ExtensionWidget {
this.element = append(this.parent, $('div.extension-bookmark'));
const recommendation = append(this.element, $('.recommendation'));
append(recommendation, $('span' + ThemeIcon.asCSSSelector(ratingIcon)));
const applyBookmarkStyle = (theme: IColorTheme) => {
const bgColor = theme.getColor(extensionButtonProminentBackground);
const fgColor = theme.getColor(extensionButtonProminentForeground);
recommendation.style.borderTopColor = bgColor ? bgColor.toString() : 'transparent';
recommendation.style.color = fgColor ? fgColor.toString() : 'white';
};
applyBookmarkStyle(this.themeService.getColorTheme());
this.themeService.onDidColorThemeChange(applyBookmarkStyle, this, this.disposables);
}
}
@ -389,8 +381,8 @@ export class ExtensionHoverWidget extends ExtensionWidget {
constructor(
private readonly options: ExtensionHoverOptions,
private readonly extensionStatusIconAction: ExtensionStatusAction,
private readonly tooltipAction: ExtensionToolTipAction,
private readonly extensionStatusAction: ExtensionStatusAction,
private readonly reloadAction: ReloadAction,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IHoverService private readonly hoverService: IHoverService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ -426,32 +418,22 @@ export class ExtensionHoverWidget extends ExtensionWidget {
markdown.appendMarkdown(`**${this.extension.displayName}**&nbsp;_v${this.extension.version}_`);
markdown.appendText(`\n`);
const toolTip = this.tooltipAction.label;
const extensionStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension);
const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension);
const extensionStatus = this.extensionStatusAction.status;
const reloadRequiredMessage = this.reloadAction.enabled ? this.reloadAction.tooltip : '';
if (toolTip || extensionStatus) {
if (toolTip) {
if (this.extensionStatusIconAction.statusIcon) {
markdown.appendMarkdown(`$(${this.extensionStatusIconAction.statusIcon.id})&nbsp;`);
}
markdown.appendMarkdown(`${toolTip}`);
if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) {
markdown.appendMarkdown(`&nbsp;[${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`);
}
markdown.appendText(`\n`);
}
if (extensionStatus) {
if (extensionStatus.activationTimes) {
const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime;
markdown.appendMarkdown(`${localize('activation', "Activation time")}${extensionStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''} : \`${activationTime}ms\``);
if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage) {
if (extensionRuntimeStatus) {
if (extensionRuntimeStatus.activationTimes) {
const activationTime = extensionRuntimeStatus.activationTimes.codeLoadingTime + extensionRuntimeStatus.activationTimes.activateCallTime;
markdown.appendMarkdown(`${localize('activation', "Activation time")}${extensionRuntimeStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''} : \`${activationTime}ms\``);
markdown.appendText(`\n`);
}
if (extensionStatus.runtimeErrors.length || extensionStatus.messages.length) {
const hasErrors = extensionStatus.runtimeErrors.length || extensionStatus.messages.some(message => message.type === Severity.Error);
const hasWarnings = extensionStatus.messages.some(message => message.type === Severity.Warning);
const errorsLink = extensionStatus.runtimeErrors.length ? `[${extensionStatus.runtimeErrors.length === 1 ? localize('uncaught error', '1 uncaught error') : localize('uncaught errors', '{0} uncaught errors', extensionStatus.runtimeErrors.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.RuntimeStatus]))}`)})` : undefined;
const messageLink = extensionStatus.messages.length ? `[${extensionStatus.messages.length === 1 ? localize('message', '1 message') : localize('messages', '{0} messages', extensionStatus.messages.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.RuntimeStatus]))}`)})` : undefined;
if (extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.length) {
const hasErrors = extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.some(message => message.type === Severity.Error);
const hasWarnings = extensionRuntimeStatus.messages.some(message => message.type === Severity.Warning);
const errorsLink = extensionRuntimeStatus.runtimeErrors.length ? `[${extensionRuntimeStatus.runtimeErrors.length === 1 ? localize('uncaught error', '1 uncaught error') : localize('uncaught errors', '{0} uncaught errors', extensionRuntimeStatus.runtimeErrors.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.RuntimeStatus]))}`)})` : undefined;
const messageLink = extensionRuntimeStatus.messages.length ? `[${extensionRuntimeStatus.messages.length === 1 ? localize('message', '1 message') : localize('messages', '{0} messages', extensionRuntimeStatus.messages.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.RuntimeStatus]))}`)})` : undefined;
markdown.appendMarkdown(`$(${hasErrors ? errorIcon.id : hasWarnings ? warningIcon.id : infoIcon.id}) This extension has reported `);
if (errorsLink && messageLink) {
markdown.appendMarkdown(`${errorsLink} and ${messageLink}`);
@ -462,6 +444,27 @@ export class ExtensionHoverWidget extends ExtensionWidget {
}
}
if (extensionStatus) {
if (extensionStatus.icon) {
markdown.appendMarkdown(`$(${extensionStatus.icon.id})&nbsp;`);
}
if (isString(extensionStatus.message)) {
markdown.appendText(extensionStatus.message);
} else {
markdown.appendMarkdown(extensionStatus.message.value);
}
if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) {
markdown.appendMarkdown(`&nbsp;[${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`);
}
markdown.appendText(`\n`);
}
if (reloadRequiredMessage) {
markdown.appendMarkdown(`$(${infoIcon.id})&nbsp;`);
markdown.appendMarkdown(`${reloadRequiredMessage}`);
markdown.appendText(`\n`);
}
markdown.appendMarkdown(`---`);
markdown.appendText(`\n`);
}
@ -499,4 +502,15 @@ registerThemingParticipant((theme, collector) => {
if (extensionRatingIcon) {
collector.addRule(`.extension-ratings .codicon-extensions-star-full, .extension-ratings .codicon-extensions-star-half { color: ${extensionRatingIcon}; }`);
}
const fgColor = theme.getColor(extensionButtonProminentForeground);
if (fgColor) {
collector.addRule(`.extension-bookmark .recommendation { color: ${fgColor}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .recommendation .codicon { color: ${fgColor}; }`);
}
const bgColor = theme.getColor(extensionButtonProminentBackground);
if (bgColor) {
collector.addRule(`.extension-bookmark .recommendation { border-top-color: ${bgColor}; }`);
}
});

View file

@ -144,6 +144,7 @@
.extension-editor > .header > .details > .actions {
margin-top: 10px;
display: flex;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar {
@ -198,44 +199,48 @@
margin-right: 0;
}
.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label,
.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status {
.extension-editor > .header > .details > .actions > .monaco-action-bar .action-item .action-label.extension-status-label,
.extension-editor > .header > .details > .actions > .monaco-action-bar .action-item .action-label.disable-status {
font-weight: normal;
}
.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label:hover,
.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status:hover {
.extension-editor > .header > .details > .actions > .monaco-action-bar .action-item .action-label.extension-status-label:hover,
.extension-editor > .header > .details > .actions > .monaco-action-bar .action-item .action-label.disable-status:hover {
opacity: 0.9;
}
.extension-editor > .header > .details > .subtext-container {
display: block;
float: left;
margin-top: 0;
font-size: 13px;
font-style: italic;
.extension-editor > .header > .details > .actions > .status {
line-height: 22px;
font-size: 90%;
margin-top: 3px;
}
.extension-editor > .header > .details > .subtext-container > .monaco-action-bar {
float: left;
margin-top: 2px;
font-style: normal;
.extension-editor > .header > .details > .actions > .status p {
margin-top: 0px;
margin-bottom: 0px;
}
.extension-editor > .header > .details > .subtext-container > .subtext {
float:left;
.extension-editor > .header > .details > .actions > .status a:hover {
text-decoration: underline;
}
.extension-editor > .header > .details > .actions > .status span {
vertical-align: middle;
margin-bottom: 4px;
}
.extension-editor > .header > .details > .recommendation {
margin-top: 5px;
margin-right: 4px;
}
.extension-editor > .header > .details > .subtext-container > .monaco-action-bar .action-label {
margin-top: 4px;
margin-left: 4px;
padding-bottom: 1px;
.extension-editor > .header > .details > .recommendation .codicon {
font-size: inherit;
padding-right: 3px;
}
.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container {
justify-content: flex-start;
.extension-editor > .header > .details > .recommendation .recommendation-text {
vertical-align: text-bottom;
font-size: 90%;
}
.extension-editor > .body {