diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 8258c723fe428..0b852934409a6 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -102,6 +102,10 @@ "name": "vs/workbench/contrib/interactive", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/languageStatus", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/keybindings", "project": "vscode-workbench" diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 9e3b6888ef236..20f56038fd004 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -20,7 +20,7 @@ display: none; } -.monaco-hover .hover-contents { +.monaco-hover .hover-contents:not(.html-hover-contents) { padding: 4px 8px; } @@ -137,7 +137,7 @@ } /** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/ -.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span { +.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span { margin-bottom: 4px; display: inline-block; } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 7ce3fa0c95c65..b54b5f62bc665 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -23,7 +23,7 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelMarkdownString { - markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise); + markdown: IMarkdownString | string | HTMLElement | undefined | ((token: CancellationToken) => Promise); markdownNotSupportedFallback: string | undefined; } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts index 7f5ad71d04243..0f4b9674ed62d 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -25,7 +25,7 @@ export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIc } } -export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined { +export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | HTMLElement | undefined): IDisposable | undefined { if (!markdownTooltip) { return undefined; } @@ -79,7 +79,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM hoverWidget?.dispose(); hoverWidget = hoverDelegate.showHover(hoverOptions); - const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); + const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) && !(markdownTooltip instanceof HTMLElement) ? markdownTooltip.markdownNotSupportedFallback : undefined); hoverWidget?.dispose(); hoverWidget = undefined; @@ -119,8 +119,8 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM } -function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { - if (isString(markdownTooltip)) { +function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString | HTMLElement): (token: CancellationToken) => Promise { + if (isString(markdownTooltip) || markdownTooltip instanceof HTMLElement) { return async () => markdownTooltip; } else if (isFunction(markdownTooltip.markdown)) { return markdownTooltip.markdown; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 2a18e78075e76..b0fa92a5b2109 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2873,15 +2873,18 @@ declare module 'vscode' { } interface LanguageStatusItem { + readonly id: string; selector: DocumentSelector; - text: string; - detail: string | MarkdownString severity: LanguageStatusSeverity; + name: string | undefined; + text: string; + detail: string; + command: Command | undefined; dispose(): void; } namespace languages { - export function createLanguageStatusItem(selector: DocumentSelector): LanguageStatusItem; + export function createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; } //#endregion diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b7d91ad95b31a..32a4d036f8b23 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -186,7 +186,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService); const extHostDialogs = new ExtHostDialogs(rpcProtocol); const extHostStatusBar = new ExtHostStatusBar(rpcProtocol, extHostCommands.converter); - const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments); + const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments, extHostCommands.converter); // Register API-ish commands ExtHostApiCommands.register(extHostCommands); @@ -507,9 +507,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostLanguageFeatures.registerTypeHierarchyProvider(extension, selector, provider); }, - createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem { + createLanguageStatusItem(id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem { checkProposedApiEnabled(extension); - return extHostLanguages.createLanguageStatusItem(selector); + return extHostLanguages.createLanguageStatusItem(extension, id, selector); } }; diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index d246aa012b716..4470b431ddb26 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -10,19 +10,20 @@ import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { StandardTokenType, Range, Position, LanguageStatusSeverity } from 'vs/workbench/api/common/extHostTypes'; import Severity from 'vs/base/common/severity'; import { disposableTimeout } from 'vs/base/common/async'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; export class ExtHostLanguages { private readonly _proxy: MainThreadLanguagesShape; - private readonly _documents: ExtHostDocuments; constructor( mainContext: IMainContext, - documents: ExtHostDocuments + private readonly _documents: ExtHostDocuments, + private readonly _commands: CommandsConverter ) { this._proxy = mainContext.getProxy(MainContext.MainThreadLanguages); - this._documents = documents; } getLanguages(): Promise { @@ -67,32 +68,58 @@ export class ExtHostLanguages { private _handlePool: number = 0; - createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem { + createLanguageStatusItem(extension: IExtensionDescription, id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem { const handle = this._handlePool++; const proxy = this._proxy; - const data: { selector: any, text: string, detail: string | vscode.MarkdownString, severity: vscode.LanguageStatusSeverity } = { + const data: Omit = { selector, + id, + name: extension.displayName ?? extension.name, + severity: LanguageStatusSeverity.Information, + command: undefined, text: '', detail: '', - severity: LanguageStatusSeverity.Information, }; let soonHandle: IDisposable | undefined; + let commandDisposables = new DisposableStore(); const updateAsync = () => { soonHandle?.dispose(); soonHandle = disposableTimeout(() => { + + commandDisposables.clear(); + this._proxy.$setLanguageStatus(handle, { + id: `${extension.identifier.value}/${id}`, + name: data.name ?? extension.displayName ?? extension.name, + source: extension.displayName ?? extension.name, selector: data.selector, - text: data.text, - message: typeof data.detail === 'string' ? data.detail : typeConvert.MarkdownString.from(data.detail), - severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info + label: data.text, + detail: data.detail, + severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info, + command: data.command && this._commands.toInternal(data.command, commandDisposables) }); }, 0); }; const result: vscode.LanguageStatusItem = { + dispose() { + commandDisposables.dispose(); + soonHandle?.dispose(); + proxy.$removeLanguageStatus(handle); + }, + get id() { + return data.id; + }, + get name() { + return data.name; + }, + set name(value) { + data.name = value; + updateAsync(); + }, get selector() { return data.selector; }, @@ -121,9 +148,12 @@ export class ExtHostLanguages { data.severity = value; updateAsync(); }, - dispose() { - soonHandle?.dispose(); - proxy.$removeLanguageStatus(handle); + get command() { + return data.command; + }, + set command(value) { + data.command = value; + updateAsync(); } }; updateAsync(); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 361b41867e6f2..1bf93a3f0257b 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -37,7 +37,7 @@ import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorE import { ConfigurationChangedEvent, IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { deepClone, equals } from 'vs/base/common/objects'; +import { deepClone } from 'vs/base/common/objects'; import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Schemas } from 'vs/base/common/network'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -50,11 +50,10 @@ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessi import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers'; -import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERROR_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; -import { ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; import { AutomaticLanguageDetectionLikelyWrongClassification, AutomaticLanguageDetectionLikelyWrongId, IAutomaticLanguageDetectionLikelyWrongData, ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; class SideBySideEditorEncodingSupport implements IEncodingSupport { @@ -182,7 +181,6 @@ class StateChange { type StateDelta = ( { type: 'selectionStatus'; selectionStatus: string | undefined; } | { type: 'mode'; mode: string | undefined; } - | { type: 'languageStatus'; status: ILanguageStatus[] | undefined; } | { type: 'encoding'; encoding: string | undefined; } | { type: 'EOL'; EOL: string | undefined; } | { type: 'indentation'; indentation: string | undefined; } @@ -200,9 +198,6 @@ class State { private _mode: string | undefined; get mode(): string | undefined { return this._mode; } - private _status: ILanguageStatus[] | undefined; - get status(): ILanguageStatus[] | undefined { return this._status; } - private _encoding: string | undefined; get encoding(): string | undefined { return this._encoding; } @@ -248,13 +243,6 @@ class State { } } - if (update.type === 'languageStatus') { - if (!equals(this._status, update.status)) { - this._status = update.status; - change.languageStatus = true; - } - } - if (update.type === 'encoding') { if (this._encoding !== update.encoding) { this._encoding = update.encoding; @@ -318,7 +306,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private readonly encodingElement = this._register(new MutableDisposable()); private readonly eolElement = this._register(new MutableDisposable()); private readonly modeElement = this._register(new MutableDisposable()); - private readonly statusElement = this._register(new MutableDisposable()); private readonly metadataElement = this._register(new MutableDisposable()); private readonly currentProblemStatus: ShowCurrentMarkerInStatusbarContribution = this._register(this.instantiationService.createInstance(ShowCurrentMarkerInStatusbarContribution)); @@ -330,7 +317,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private promptedScreenReader: boolean = false; constructor( - @ILanguageStatusService private readonly languageStatusService: ILanguageStatusService, @IEditorService private readonly editorService: IEditorService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IModeService private readonly modeService: IModeService, @@ -559,36 +545,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateElement(this.modeElement, props, 'status.editor.mode', StatusbarAlignment.RIGHT, 100.1); } - private updateStatusElement(status: ILanguageStatus[] | undefined): void { - if (!status || status.length === 0) { - this.statusElement.clear(); - return; - } - - const [first] = status; - - let backgroundColor: ThemeColor | undefined; - let color: ThemeColor | undefined; - if (first.severity === Severity.Error) { - backgroundColor = themeColorFromId(STATUS_BAR_ERROR_ITEM_BACKGROUND); - color = themeColorFromId(STATUS_BAR_ERROR_ITEM_FOREGROUND); - } else if (first.severity === Severity.Warning) { - backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); - color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); - } - - const props: IStatusbarEntry = { - name: localize('status.editor.status', "Language Status"), - text: first.text, - ariaLabel: first.text, - tooltip: first.message, - backgroundColor, - color - }; - - this.updateElement(this.statusElement, props, 'status.editor.status', StatusbarAlignment.RIGHT, 100.05); - } - private updateMetadataElement(text: string | undefined): void { if (!text) { this.metadataElement.clear(); @@ -645,7 +601,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateEncodingElement(this.state.encoding); this.updateEOLElement(this.state.EOL ? this.state.EOL === '\r\n' ? nlsEOLCRLF : nlsEOLLF : undefined); this.updateModeElement(this.state.mode); - this.updateStatusElement(this.state.status); this.updateMetadataElement(this.state.metadata); } @@ -683,7 +638,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.onScreenReaderModeChange(activeCodeEditor); this.onSelectionChange(activeCodeEditor); this.onModeChange(activeCodeEditor, activeInput); - this.onLanguageStatusChange(activeCodeEditor); this.onEOLChange(activeCodeEditor); this.onEncodingChange(activeEditorPane, activeCodeEditor); this.onIndentationChange(activeCodeEditor); @@ -717,10 +671,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.onModeChange(activeCodeEditor, activeInput); })); - this.activeEditorListeners.add(this.languageStatusService.onDidChange(() => { - this.onLanguageStatusChange(activeCodeEditor); - })); - // Hook Listener for content changes this.activeEditorListeners.add(activeCodeEditor.onDidChangeModelContent((e) => { this.onEOLChange(activeCodeEditor); @@ -787,14 +737,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateState(info); } - private async onLanguageStatusChange(editorWidget: ICodeEditor | undefined): Promise { - const update: StateDelta = { type: 'languageStatus', status: undefined }; - if (editorWidget?.hasModel()) { - update.status = await this.languageStatusService.getLanguageStatus(editorWidget.getModel()); - } - this.updateState(update); - } - private onIndentationChange(editorWidget: ICodeEditor | undefined): void { const update: StateDelta = { type: 'indentation', indentation: undefined }; diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts new file mode 100644 index 0000000000000..4214053bae6ff --- /dev/null +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/languageStatus'; +import * as dom from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import Severity from 'vs/base/common/severity'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { NOTIFICATIONS_BORDER, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { Link } from 'vs/platform/opener/browser/link'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { equals } from 'vs/base/common/arrays'; + +class LanguageStatusViewModel { + + constructor( + readonly combined: readonly ILanguageStatus[], + readonly dedicated: readonly ILanguageStatus[] + ) { } + + isEqual(other: LanguageStatusViewModel) { + return equals(this.combined, other.combined) && equals(this.dedicated, other.dedicated); + } +} + +class EditorStatusContribution implements IWorkbenchContribution { + + private static readonly _id = 'status.languageStatus'; + + private static readonly _keyDedicatedItems = 'languageStatus.dedicated'; + + private readonly _disposables = new DisposableStore(); + + private _dedicated = new Set(); + + private _model?: LanguageStatusViewModel; + private _combinedEntry?: IStatusbarEntryAccessor; + private _dedicatedEntries = new Map(); + private _renderDisposables = new DisposableStore(); + + constructor( + @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, + @IStatusbarService private readonly _statusBarService: IStatusbarService, + @IEditorService private readonly _editorService: IEditorService, + @IOpenerService private readonly _openerService: IOpenerService, + @ICommandService private readonly _commandService: ICommandService, + @IStorageService private readonly _storageService: IStorageService, + ) { + _storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables); + this._restoreState(); + + _languageStatusService.onDidChange(this._update, this, this._disposables); + _editorService.onDidActiveEditorChange(this._update, this, this._disposables); + this._update(); + + _statusBarService.onDidChangeEntryVisibility(e => { + if (!e.visible && this._dedicated.has(e.id)) { + this._dedicated.delete(e.id); + this._update(); + this._storeState(); + } + }, this._disposables); + + } + + dispose(): void { + this._disposables.dispose(); + this._combinedEntry?.dispose(); + dispose(this._dedicatedEntries.values()); + this._renderDisposables.dispose(); + } + + // --- persisting dedicated items + + private _handleStorageChange(e: IStorageValueChangeEvent) { + if (e.key !== EditorStatusContribution._keyDedicatedItems) { + return; + } + this._restoreState(); + this._update(); + } + + private _restoreState(): void { + const raw = this._storageService.get(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL, '[]'); + try { + const ids = JSON.parse(raw); + this._dedicated = new Set(ids); + } catch { + this._dedicated.clear(); + } + } + + private _storeState(): void { + if (this._dedicated.size === 0) { + this._storageService.remove(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL); + } else { + const raw = JSON.stringify(Array.from(this._dedicated.keys())); + this._storageService.store(EditorStatusContribution._keyDedicatedItems, raw, StorageScope.GLOBAL, StorageTarget.USER); + } + } + + // --- language status model and UI + + private _createViewModel(): LanguageStatusViewModel { + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + if (!editor?.hasModel()) { + return new LanguageStatusViewModel([], []); + } + const all = this._languageStatusService.getLanguageStatus(editor.getModel()); + const combined: ILanguageStatus[] = []; + const dedicated: ILanguageStatus[] = []; + for (let item of all) { + if (this._dedicated.has(item.id)) { + dedicated.push(item); + } else { + combined.push(item); + } + } + return new LanguageStatusViewModel(combined, dedicated); + } + + private _update(): void { + + const model = this._createViewModel(); + + if (this._model?.isEqual(model)) { + return; + } + + this._model = model; + + this._renderDisposables.clear(); + + // combined status bar item is a single item which hover shows + // each status item + if (model.combined.length === 0) { + // nothing + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + + } else { + const [first] = model.combined; + let text: string = '$(info)'; + if (first.severity === Severity.Error) { + text = '$(error)'; + } else if (first.severity === Severity.Warning) { + text = '$(warning)'; + } + const element = document.createElement('div'); + for (const status of model.combined) { + element.appendChild(this._renderStatus(status, this._renderDisposables)); + } + const props: IStatusbarEntry = { + name: localize('status.editor.status', "Editor Language Status"), + ariaLabel: localize('status.editor.status', "Editor Language Status"), + tooltip: element, + text, + }; + if (!this._combinedEntry) { + this._combinedEntry = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.11); + } else { + this._combinedEntry.update(props); + } + } + + // dedicated status bar items are shows as-is in the status bar + const newDedicatedEntries = new Map(); + for (const status of model.dedicated) { + const props = EditorStatusContribution._asStatusbarEntry(status); + let entry = this._dedicatedEntries.get(status.id); + if (!entry) { + entry = this._statusBarService.addEntry(props, status.id, StatusbarAlignment.RIGHT, 100.09999); + } else { + entry.update(props); + this._dedicatedEntries.delete(status.id); + } + newDedicatedEntries.set(status.id, entry); + } + dispose(this._dedicatedEntries.values()); + this._dedicatedEntries = newDedicatedEntries; + } + + private _renderStatus(status: ILanguageStatus, store: DisposableStore): HTMLElement { + + const node = document.createElement('div'); + node.classList.add('hover-language-status-element'); + + const left = document.createElement('div'); + left.classList.add('left'); + node.appendChild(left); + + const label = document.createElement('span'); + label.classList.add('label'); + dom.append(label, ...renderLabelWithIcons(status.label)); + left.appendChild(label); + + const detail = document.createElement('span'); + detail.classList.add('detail'); + this._renderTextPlus(detail, status.detail, store); + left.appendChild(detail); + + const right = document.createElement('div'); + right.classList.add('right'); + node.appendChild(right); + + const { command } = status; + if (command) { + const btn = new Button(right, { title: command.tooltip }); + btn.label = command.title; + btn.onDidClick(_e => { + if (command.arguments) { + this._commandService.executeCommand(command.id, ...command.arguments); + } else { + this._commandService.executeCommand(command.id); + } + }); + store.add(btn); + } + + // -- pin + const action = new Action('pin', localize('pin', "Pin to Status Bar"), Codicon.pin.classNames, true, () => { + this._dedicated.add(status.id); + this._statusBarService.updateEntryVisibility(status.id, true); + this._update(); + this._storeState(); + }); + const actionBar = new ActionBar(right, {}); + actionBar.push(action, { icon: true, label: false }); + store.add(action); + store.add(actionBar); + + return node; + } + + private _renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { + for (let node of parseLinkedText(text).nodes) { + if (typeof node === 'string') { + const parts = renderLabelWithIcons(node); + dom.append(target, ...parts); + } else { + const link = new Link(node, undefined, this._openerService); + store.add(link); + dom.append(target, link.el); + } + } + } + + // --- + + private static _asStatusbarEntry(item: ILanguageStatus): IStatusbarEntry { + return { + name: item.name, + text: item.label, + ariaLabel: item.label, + tooltip: new MarkdownString(item.detail, true), + command: item.command + }; + } +} + +registerThemingParticipant((theme, collector) => { + collector.addRule(`:root { + --code-notifications-border: ${theme.getColor(NOTIFICATIONS_BORDER)}; + --code-language-status-item-active-background: ${theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND)?.darken(.8)}; + }`); +}); + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css new file mode 100644 index 0000000000000..825cdced79cb2 --- /dev/null +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .hover-language-status-element { + display: flex; + justify-content: space-between; + padding: 4px 8px; + vertical-align: middle; +} + +.monaco-workbench .hover-language-status-element:not(:last-child) { + border-bottom: 1px solid var(--code-notifications-border); +} + +.monaco-workbench .hover-language-status-element > .left > .label::after { + content: '–'; + padding: 0 4px; + opacity: 0.6; +} + +.monaco-workbench .hover-language-status-element > .left > .label:empty { + display: none; +} + +.monaco-workbench .hover-language-status-element .right { + margin: auto 0; + display: flex; +} + +.monaco-workbench .hover-language-status-element .right:not(:empty) { + padding-left: 16px; +} + +.monaco-workbench .hover-language-status-element .right .monaco-button.monaco-text-button { + font-size: smaller; + padding: 2px 6px +} + +.monaco-workbench .hover-language-status-element .right .monaco-action-bar:not(:first-child) { + padding-left: 8px; +} + +/* todo@jrieken - unsets the default padding, something like friend/parent status bar items is needed */ +.monaco-workbench .part.statusbar > .items-container > DIV#status\.languageStatus.statusbar-item a { + padding: 0 4px; + margin: 0; +} + +.monaco-workbench .part.statusbar > .items-container > DIV#status\.languageStatus.statusbar-item a:hover { + background-color: var(--code-language-status-item-active-background); +} + +.monaco-workbench .part.statusbar > .items-container > DIV#status\.editor\.mode.statusbar-item:hover + DIV#status\.languageStatus.statusbar-item { + background-color: var(--code-language-status-item-active-background); +} + +.monaco-workbench .part.statusbar > .items-container > DIV#status\.editor\.mode.statusbar-item A { + margin-left: 0; +} diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index f730994aecb75..4e30614322b42 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -114,6 +114,7 @@ export class HoverWidget extends Widget { } else if (options.content instanceof HTMLElement) { contentsElement.appendChild(options.content); + contentsElement.classList.add('html-hover-contents'); } else { const markdown = options.content; diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index affbfc6ffbc48..99d34137596f7 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -5,10 +5,10 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { ITextModel } from 'vs/editor/common/model'; +import { Command } from 'vs/editor/common/modes'; import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry'; import { LanguageSelector } from 'vs/editor/common/modes/languageSelector'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -16,10 +16,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ILanguageStatus { - selector: LanguageSelector, - severity: Severity; - text: string; - message: string | IMarkdownString; + readonly id: string; + readonly name: string; + + readonly selector: LanguageSelector; + readonly severity: Severity; + readonly label: string; + readonly detail: string; + readonly source: string; + readonly command: Command | undefined; } export interface ILanguageStatusProvider { @@ -36,7 +41,7 @@ export interface ILanguageStatusService { addStatus(status: ILanguageStatus): IDisposable; - getLanguageStatus(model: ITextModel): Promise; + getLanguageStatus(model: ITextModel): ILanguageStatus[]; } @@ -52,7 +57,7 @@ class LanguageStatusServiceImpl implements ILanguageStatusService { return this._provider.register(status.selector, status); } - async getLanguageStatus(model: ITextModel): Promise { + getLanguageStatus(model: ITextModel): ILanguageStatus[] { return this._provider.ordered(model).sort((a, b) => b.severity - a.severity); } } diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 4c700b5e38323..a6424fc127222 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -49,7 +49,7 @@ export interface IStatusbarEntry { /** * An optional tooltip text to show when you hover over the entry */ - readonly tooltip?: string | IMarkdownString; + readonly tooltip?: string | IMarkdownString | HTMLElement; /** * An optional color to use for the entry diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index d738cca8fccc7..ca38847039850 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -300,6 +300,9 @@ import 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution'; import 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline'; import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Language Status +import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; + // Experiments import 'vs/workbench/contrib/experiments/browser/experiments.contribution';