From 5d8d47ff3af1749cc9e0ea534cccbb9be3f959ab Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 22 Jul 2021 15:32:18 +0200 Subject: [PATCH] fine tune hover and extension status --- .../extensions/browser/extensionEditor.ts | 97 ++++---- .../extensions/browser/extensionsActions.ts | 215 +++++++----------- .../extensions/browser/extensionsList.ts | 9 +- .../extensions/browser/extensionsWidgets.ts | 86 ++++--- .../browser/media/extensionEditor.css | 51 +++-- 5 files changed, 221 insertions(+), 237 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index e537233723f..e72e09419b5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -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); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0044da14edc..3477d716221 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -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 { - 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()); + 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 { - 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}; }`); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 625cb1cfde0..87d252ac3f2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -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 { 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 { 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, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 22fd3e8d219..1a9c2b18778 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -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}** _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}) `); - } - markdown.appendMarkdown(`${toolTip}`); - if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) { - markdown.appendMarkdown(` [${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}) `); + } + 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(` [${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}) `); + 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}; }`); + } }); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index c0c364966c9..5d867b4e4c6 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -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 {