333 lines
12 KiB
TypeScript
333 lines
12 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 { ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService';
|
|
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
|
import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERROR_ITEM_FOREGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } 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, ShowTooltipCommand, 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 { 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';
|
|
import { URI } from 'vs/base/common/uri';
|
|
|
|
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<string>();
|
|
|
|
private _model?: LanguageStatusViewModel;
|
|
private _combinedEntry?: IStatusbarEntryAccessor;
|
|
private _dedicatedEntries = new Map<string, IStatusbarEntryAccessor>();
|
|
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,
|
|
@IStorageService private readonly _storageService: IStorageService,
|
|
) {
|
|
_storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables);
|
|
this._restoreState();
|
|
|
|
_editorService.onDidActiveEditorChange(() => this._update(), this, this._disposables);
|
|
_languageStatusService.onDidChange(() => this._update(), this, this._disposables);
|
|
_languageStatusService.onDidChangeBusy(() => this._update(true), 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 = <string[]>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(force?: boolean): void {
|
|
|
|
const model = this._createViewModel();
|
|
|
|
if (this._model?.isEqual(model) && !force) {
|
|
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;
|
|
const showSeverity = first.severity >= Severity.Warning;
|
|
const text = EditorStatusContribution._severityToComboCodicon(first.severity);
|
|
|
|
let isBusy = false;
|
|
const ariaLabels: string[] = [];
|
|
const element = document.createElement('div');
|
|
for (const status of model.combined) {
|
|
const thisIsBusy = this._languageStatusService.isBusy(status);
|
|
element.appendChild(this._renderStatus(status, showSeverity, thisIsBusy, this._renderDisposables));
|
|
ariaLabels.push(this._asAriaLabel(status));
|
|
isBusy = isBusy || thisIsBusy;
|
|
}
|
|
const props: IStatusbarEntry = {
|
|
name: localize('langStatus.name', "Editor Language Status"),
|
|
ariaLabel: localize('langStatus.aria', "Editor Language Status: {0}", ariaLabels.join(', next: ')),
|
|
tooltip: element,
|
|
command: ShowTooltipCommand,
|
|
text: isBusy ? `${text}\u00A0\u00A0$(loading~spin)` : text,
|
|
};
|
|
if (!this._combinedEntry) {
|
|
this._combinedEntry = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.LEFT, compact: true });
|
|
} else {
|
|
this._combinedEntry.update(props);
|
|
}
|
|
}
|
|
|
|
// dedicated status bar items are shows as-is in the status bar
|
|
const newDedicatedEntries = new Map<string, IStatusbarEntryAccessor>();
|
|
for (const status of model.dedicated) {
|
|
const props = EditorStatusContribution._asStatusbarEntry(status, this._languageStatusService.isBusy(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, showSeverity: boolean, isBusy: boolean, store: DisposableStore): HTMLElement {
|
|
|
|
const parent = document.createElement('div');
|
|
parent.classList.add('hover-language-status');
|
|
|
|
const severity = document.createElement('div');
|
|
severity.classList.add('severity', `sev${status.severity}`);
|
|
severity.classList.toggle('show', showSeverity);
|
|
const severityText = EditorStatusContribution._severityToSingleCodicon(status.severity);
|
|
dom.append(severity, ...renderLabelWithIcons(severityText));
|
|
parent.appendChild(severity);
|
|
|
|
const element = document.createElement('div');
|
|
element.classList.add('element');
|
|
parent.appendChild(element);
|
|
|
|
const left = document.createElement('div');
|
|
left.classList.add('left');
|
|
element.appendChild(left);
|
|
|
|
const label = document.createElement('span');
|
|
label.classList.add('label');
|
|
if (isBusy) {
|
|
dom.append(label, ...renderLabelWithIcons('$(loading~spin)\u00A0\u00A0'));
|
|
}
|
|
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');
|
|
element.appendChild(right);
|
|
|
|
// -- command (if available)
|
|
const { command } = status;
|
|
if (command) {
|
|
store.add(new Link(right, {
|
|
label: command.title,
|
|
title: command.tooltip,
|
|
href: URI.from({
|
|
scheme: 'command', path: command.id, query: command.arguments && JSON.stringify(command.arguments)
|
|
}).toString()
|
|
}, undefined, this._openerService));
|
|
}
|
|
|
|
// -- pin
|
|
const actionBar = new ActionBar(right, {});
|
|
store.add(actionBar);
|
|
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();
|
|
});
|
|
actionBar.push(action, { icon: true, label: false });
|
|
store.add(action);
|
|
|
|
return parent;
|
|
}
|
|
|
|
private static _severityToComboCodicon(sev: Severity): string {
|
|
switch (sev) {
|
|
case Severity.Error: return '$(bracket-error)';
|
|
case Severity.Warning: return '$(bracket-dot)';
|
|
default: return '$(bracket)';
|
|
}
|
|
}
|
|
|
|
private static _severityToSingleCodicon(sev: Severity): string {
|
|
switch (sev) {
|
|
case Severity.Error: return '$(error)';
|
|
case Severity.Warning: return '$(info)';
|
|
default: return '$(check)';
|
|
}
|
|
}
|
|
|
|
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 {
|
|
store.add(new Link(target, node, undefined, this._openerService));
|
|
}
|
|
}
|
|
}
|
|
|
|
private _asAriaLabel(status: ILanguageStatus): string {
|
|
if (status.accessibilityInfo) {
|
|
return status.accessibilityInfo.label;
|
|
} else if (status.detail) {
|
|
return localize('aria.1', '{0}, {1}', status.label, status.detail);
|
|
} else {
|
|
return localize('aria.2', '{0}', status.label);
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
private static _asStatusbarEntry(item: ILanguageStatus, isBusy: boolean): IStatusbarEntry {
|
|
|
|
let color: ThemeColor | undefined;
|
|
let backgroundColor: ThemeColor | undefined;
|
|
if (item.severity === Severity.Warning) {
|
|
color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND);
|
|
backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND);
|
|
} else if (item.severity === Severity.Error) {
|
|
color = themeColorFromId(STATUS_BAR_ERROR_ITEM_FOREGROUND);
|
|
backgroundColor = themeColorFromId(STATUS_BAR_ERROR_ITEM_BACKGROUND);
|
|
}
|
|
|
|
return {
|
|
name: localize('name.pattern', '{0} (Language Status)', item.name),
|
|
text: isBusy ? `${item.label}\u00A0\u00A0$(loading~spin)` : item.label,
|
|
ariaLabel: item.accessibilityInfo?.label ?? item.label,
|
|
role: item.accessibilityInfo?.role,
|
|
tooltip: item.command?.tooltip || new MarkdownString(item.detail, true),
|
|
color,
|
|
backgroundColor,
|
|
command: item.command
|
|
};
|
|
}
|
|
}
|
|
|
|
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored);
|