diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index fb5297add66..b0fa92a5b21 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2873,16 +2873,18 @@ declare module 'vscode' { } interface LanguageStatusItem { + readonly id: string; selector: DocumentSelector; severity: LanguageStatusSeverity; + name: string | undefined; text: string; - detail: string; // tooltip! + 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 3a414cd6e1e..9ac7620b8f7 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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(extension, 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 296fd17dc65..4470b431ddb 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -68,13 +68,15 @@ export class ExtHostLanguages { private _handlePool: number = 0; - createLanguageStatusItem(extension: IExtensionDescription, 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: Omit = { selector, + id, + name: extension.displayName ?? extension.name, severity: LanguageStatusSeverity.Information, command: undefined, text: '', @@ -90,6 +92,8 @@ export class ExtHostLanguages { 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, label: data.text, @@ -106,6 +110,16 @@ export class ExtHostLanguages { 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; }, diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 82dce2636f3..f2e89eb27e0 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/languageStatus'; import * as dom from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +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'; @@ -23,15 +23,20 @@ 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'; class EditorStatusContribution implements IWorkbenchContribution { private static readonly _id = 'status.languageStatus'; - private readonly _entry = new MutableDisposable(); private readonly _disposables = new DisposableStore(); - private _status: ILanguageStatus[] = []; + private _combinedEntry?: IStatusbarEntryAccessor; + private _dedicatedEntries = new Map(); + private _dedicated = new Set(); constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @@ -43,57 +48,91 @@ class EditorStatusContribution implements IWorkbenchContribution { _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._disposables); } dispose(): void { - this._entry.dispose(); this._disposables.dispose(); + this._combinedEntry?.dispose(); + dispose(this._dedicatedEntries.values()); } - private _updateStatus(): void { + private _getLanguageStatus(): [combined: ILanguageStatus[], dedicated: ILanguageStatus[]] { const editor = getCodeEditor(this._editorService.activeTextEditorControl); - if (editor?.hasModel()) { - this._status = this._languageStatusService.getLanguageStatus(editor.getModel()); - } else { - this._status = []; + if (!editor?.hasModel()) { + return [[], []]; } + 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 [combined, dedicated]; } private _update(): void { - this._updateStatus(); - if (this._status.length === 0) { - this._entry.clear(); - return; - } + const [combined, dedicated] = this._getLanguageStatus(); - const [first] = this._status; - let text: string = '$(info)'; - if (first.severity === Severity.Error) { - text = '$(error)'; - } else if (first.severity === Severity.Warning) { - text = '$(warning)'; - } + // combined status bar item is a single item which hover shows + // each status item + if (combined.length === 0) { + // nothing + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; - const element = document.createElement('div'); - for (const status of this._status) { - element.appendChild(this._renderStatus(status)); - } - - const props: IStatusbarEntry = { - name: localize('status.editor.status', "Language Status"), - ariaLabel: localize('status.editor.status', "Language Status"), - text, - // backgroundColor, - // color, - tooltip: element - }; - - if (!this._entry.value) { - this._entry.value = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.11); } else { - this._entry.value.update(props); + const [first] = 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 combined) { + element.appendChild(this._renderStatus(status)); + } + 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 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): HTMLElement { @@ -131,6 +170,16 @@ class EditorStatusContribution implements IWorkbenchContribution { } }); } + + // -- 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(); + }); + const actions = new ActionBar(right, {}); + actions.push(action, { icon: true, label: false }); + return node; } @@ -144,6 +193,18 @@ class EditorStatusContribution implements IWorkbenchContribution { } } } + + // --- + + 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) => { diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 977f8ffa3e9..825cdced79c 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -26,6 +26,7 @@ .monaco-workbench .hover-language-status-element .right { margin: auto 0; + display: flex; } .monaco-workbench .hover-language-status-element .right:not(:empty) { @@ -37,6 +38,10 @@ 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; diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index bd02bf7b3a9..99d34137596 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -16,12 +16,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ILanguageStatus { - selector: LanguageSelector, - severity: Severity; - label: string; - detail: string; - source: string; - command: Command | undefined; + 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 {