From 8a1195432117e50e4aaf0e7eba17b75948fad04c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 17 Aug 2021 12:08:37 +0200 Subject: [PATCH 01/17] move language status service to workbench --- src/vs/workbench/api/browser/mainThreadLanguages.ts | 2 +- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/browser/parts/editor/editorStatus.ts | 2 +- .../services/languageStatus/common}/languageStatusService.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/vs/{editor/common/services => workbench/services/languageStatus/common}/languageStatusService.ts (100%) diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index f4636bb3896..78986edd1f6 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -12,7 +12,7 @@ import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { StandardTokenType } from 'vs/editor/common/modes'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ILanguageStatus, ILanguageStatusService } from 'vs/editor/common/services/languageStatusService'; +import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; import { IDisposable } from 'vs/base/common/lifecycle'; @extHostNamedCustomer(MainContext.MainThreadLanguages) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index eaf0a2e9c08..5bcf5dda003 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -24,7 +24,7 @@ import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model' import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import * as modes from 'vs/editor/common/modes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; -import { ILanguageStatus } from 'vs/editor/common/services/languageStatusService'; +import { ILanguageStatus } from 'vs/workbench/services/languageStatus/common/languageStatusService'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 9155121d2ca..925f7369f59 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -54,7 +54,7 @@ import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERROR_ITEM_FOREGROUND, STA import { ThemeColor, 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/editor/common/services/languageStatusService'; +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 { diff --git a/src/vs/editor/common/services/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts similarity index 100% rename from src/vs/editor/common/services/languageStatusService.ts rename to src/vs/workbench/services/languageStatus/common/languageStatusService.ts From 5055ed7c0daa1318b2c077d1e5314611a97973fc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 17 Aug 2021 16:02:46 +0200 Subject: [PATCH 02/17] remove language status from editorStatus.ts --- .../browser/parts/editor/editorStatus.ts | 64 +------------------ 1 file changed, 3 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 925f7369f59..b60a974a455 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/common/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 }; From 4222c512c2ad56bdb04a0995918f5c48a012ee21 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 17 Aug 2021 16:58:36 +0200 Subject: [PATCH 03/17] add fly-out UI for language status items, tweak API proposal --- build/lib/i18n.resources.json | 4 + src/vs/vscode.proposed.d.ts | 3 +- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHostLanguages.ts | 16 +- .../browser/languageStatus.contribution.ts | 137 +++++++++++++++ .../browser/languageStatusList.ts | 165 ++++++++++++++++++ .../browser/media/languageStatus.css | 22 +++ .../common/languageStatusService.ts | 9 +- src/vs/workbench/workbench.common.main.ts | 3 + 9 files changed, 342 insertions(+), 19 deletions(-) create mode 100644 src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts create mode 100644 src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts create mode 100644 src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 8258c723fe4..0b852934409 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/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5b664caa6cd..c7c0cfd3131 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2829,8 +2829,7 @@ declare module 'vscode' { interface LanguageStatusItem { selector: DocumentSelector; - text: string; - detail: string | MarkdownString + detail: string; severity: LanguageStatusSeverity; dispose(): void; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b093f1fdf84..9fd85f03e28 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -509,7 +509,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem { checkProposedApiEnabled(extension); - return extHostLanguages.createLanguageStatusItem(selector); + return extHostLanguages.createLanguageStatusItem(extension, selector); } }; diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index d246aa012b7..1dc9f2d0067 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -11,6 +11,7 @@ import { StandardTokenType, Range, Position, LanguageStatusSeverity } from 'vs/w import Severity from 'vs/base/common/severity'; import { disposableTimeout } from 'vs/base/common/async'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostLanguages { @@ -67,12 +68,12 @@ export class ExtHostLanguages { private _handlePool: number = 0; - createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem { + createLanguageStatusItem(extension: IExtensionDescription, 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: { selector: any, text: string, detail: string, severity: vscode.LanguageStatusSeverity } = { selector, text: '', detail: '', @@ -84,9 +85,9 @@ export class ExtHostLanguages { soonHandle?.dispose(); soonHandle = disposableTimeout(() => { this._proxy.$setLanguageStatus(handle, { + source: extension.displayName ?? extension.name, selector: data.selector, - text: data.text, - message: typeof data.detail === 'string' ? data.detail : typeConvert.MarkdownString.from(data.detail), + message: data.detail, severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info }); }, 0); @@ -100,13 +101,6 @@ export class ExtHostLanguages { data.selector = value; updateAsync(); }, - get text() { - return data.text; - }, - set text(value) { - data.text = value; - updateAsync(); - }, get detail() { return data.detail; }, 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 00000000000..58c40fc853b --- /dev/null +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { DisposableStore, MutableDisposable, toDisposable } 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 { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +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_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; +import { LanguageStatusDetailsWidget } from 'vs/workbench/contrib/languageStatus/browser/languageStatusList'; +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/common/statusbar'; + +class EditorStatusContribution implements IWorkbenchContribution { + + private static readonly _id = 'status.languageStatus'; + + private readonly _entry = new MutableDisposable(); + private readonly _disposables = new DisposableStore(); + + private _status: ILanguageStatus[] = []; + private _showingDetails: boolean = false; + + constructor( + @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, + @IStatusbarService private readonly _statusBarService: IStatusbarService, + @IEditorService private readonly _editorService: IEditorService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + this._disposables.add(CommandsRegistry.registerCommand(EditorStatusContribution._id, () => this._toggleDetails())); + + _languageStatusService.onDidChange(this._update, this, this._disposables); + _editorService.onDidActiveEditorChange(this._update, this, this._disposables); + this._update(); + } + + dispose(): void { + this._entry.dispose(); + this._disposables.dispose(); + } + + private _updateStatus(): void { + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + if (editor?.hasModel()) { + this._status = this._languageStatusService.getLanguageStatus(editor.getModel()); + } else { + this._status = []; + } + } + + private _update(): void { + + this._updateStatus(); + if (this._status.length === 0) { + this._entry.clear(); + return; + } + + const [first] = this._status; + let backgroundColor: ThemeColor | undefined; + let color: ThemeColor | undefined; + if (first.severity === Severity.Error) { + backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); + color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); + } else if (first.severity === Severity.Warning) { + color = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); + } + + const props: IStatusbarEntry = { + name: localize('status.editor.status', "Language Status"), + text: '$(circle-large-outline)', + ariaLabel: localize('status.editor.status', "Language Status"), + backgroundColor, + color, + command: EditorStatusContribution._id, + showBeak: this._showingDetails + }; + + if (!this._entry.value) { + this._entry.value = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.06); + } else { + this._entry.value.update(props); + } + } + + private _toggleDetails(): void { + if (!this._entry.value) { + return; + } + + if (this._showingDetails) { + this._contextViewService.hideContextView(); + // this._showingDetails = false; // happens in onHide + return; + } + + const anchor = document.getElementById(EditorStatusContribution._id); + if (!anchor) { + return; + } + + let widget: LanguageStatusDetailsWidget | undefined; + + this._contextViewService.showContextView({ + getAnchor: () => anchor, + render: container => { + widget = this._instantiationService.createInstance(LanguageStatusDetailsWidget, this._status, container); + return toDisposable(() => { + widget?.dispose(); + widget = undefined; + }); + + }, + onHide: () => { + this._showingDetails = false; + this._update(); + } + }); + this._showingDetails = true; + this._update(); + } +} + + + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts new file mode 100644 index 00000000000..336f115a83d --- /dev/null +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_BORDER } from 'vs/workbench/common/theme'; +import { ILanguageStatus } from 'vs/workbench/services/languageStatus/common/languageStatusService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Link } from 'vs/platform/opener/browser/link'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +class LanguageStatusTemplate { + + readonly firstLine: HTMLDivElement; + readonly secondLine: HTMLDivElement; + + constructor( + readonly container: HTMLElement, + @IOpenerService private readonly _openerService: IOpenerService + ) { + container.classList.add('status-element'); + this.firstLine = document.createElement('div'); + this.secondLine = document.createElement('div'); + this.secondLine.classList.add('detail'); + + container.appendChild(this.firstLine); + container.appendChild(this.secondLine); + } + + dispose() { + + } + + set(element: ILanguageStatus): void { + // message + // source + dom.clearNode(this.firstLine); + for (let node of parseLinkedText(element.message).nodes) { + if (typeof node === 'string') { + const parts = renderLabelWithIcons(node); + dom.append(this.firstLine, ...parts); + } else { + dom.append(this.firstLine, new Link(node, undefined, this._openerService).el); + } + } + + dom.reset(this.secondLine, element.source); + } +} + +class Renderer implements IListRenderer, IListVirtualDelegate { + + static templateId: string = 'languageStatus'; + + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { } + + // delegate + + getHeight(element: ILanguageStatus): number { + return 56; + } + getTemplateId(element: ILanguageStatus): string { + return Renderer.templateId; + } + + // renderer + + templateId: string = Renderer.templateId; + + renderTemplate(container: HTMLElement): LanguageStatusTemplate { + return this._instantiationService.createInstance(LanguageStatusTemplate, container); + } + renderElement(element: ILanguageStatus, index: number, templateData: LanguageStatusTemplate, height: number | undefined): void { + templateData.set(element); + } + disposeTemplate(templateData: LanguageStatusTemplate): void { + templateData.dispose(); + } + +} + +export class LanguageStatusDetailsWidget { + + private readonly _disposables = new DisposableStore(); + + private readonly _container: HTMLDivElement; + private readonly _list: WorkbenchList; + + constructor( + status: ILanguageStatus[], + parent: HTMLElement, + @IThemeService themeService: IThemeService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILayoutService private readonly _layoutService: ILayoutService, + ) { + parent.style.setProperty('--code-widget-shadow', themeService.getColorTheme().getColor(widgetShadow, true)?.toString() ?? 'inherit'); + parent.style.setProperty('--code-notifications-border', themeService.getColorTheme().getColor(NOTIFICATIONS_BORDER, true)?.toString() ?? 'inherit'); + parent.classList.add('language-status'); + + this._container = document.createElement('div'); + this._container.classList.add('list-container'); + parent.appendChild(this._container); + + const renderer = _instantiationService.createInstance(Renderer); + this._list = >this._instantiationService.createInstance( + WorkbenchList, + 'LanguageStatusList', + this._container, + renderer, + [renderer], + { + // ...this.options, + setRowLineHeight: false, + horizontalScrolling: false, + overrideStyles: { + listBackground: NOTIFICATIONS_BACKGROUND, + // listInactiveSelectionBackground: NOTIFICATIONS_BACKGROUND, + // listActiveSelectionBackground: NOTIFICATIONS_BACKGROUND, + }, + accessibilityProvider: { + getAriaLabel(element: ILanguageStatus): string { + return element.message; + }, + getWidgetAriaLabel(): string { + return localize('language status', "Language Status List"); + }, + getRole(): string { + return 'dialog'; // https://github.com/microsoft/vscode/issues/82728 + } + } + } + ); + + // no selections + this._disposables.add(this._list.onDidChangeSelection(e => { + if (e.indexes.length > 0) { + this._list.setSelection([]); + } + })); + + this._list.splice(0, this._list.length, status); + this.layout(); + } + + dispose(): void { + this._list.dispose(); + } + + layout() { + const width = Math.max(200, this._layoutService.dimension.width * 0.6); + this._container.style.width = `${width}px`; + this._list.layout(); + } +} 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 00000000000..5888810413e --- /dev/null +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .language-status { + margin: 8px; + cursor: default; + box-shadow: var(--code-widget-shadow) 0 0 8px 2px; +} + +.monaco-workbench .language-status .list-container .status-element { + padding: 10px 5px; +} + +.monaco-workbench .language-status .list-container .status-element div.detail { + font-size: smaller; +} + +.monaco-workbench .language-status .list-container .monaco-list-row[data-last-element="false"].status-element { + border-bottom: 1px solid var(--code-notifications-border); +} diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index affbfc6ffbc..9cdb7202b97 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -5,7 +5,6 @@ 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'; @@ -18,8 +17,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ILanguageStatus { selector: LanguageSelector, severity: Severity; - text: string; - message: string | IMarkdownString; + message: string; + source: string; } export interface ILanguageStatusProvider { @@ -36,7 +35,7 @@ export interface ILanguageStatusService { addStatus(status: ILanguageStatus): IDisposable; - getLanguageStatus(model: ITextModel): Promise; + getLanguageStatus(model: ITextModel): ILanguageStatus[]; } @@ -52,7 +51,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/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index d738cca8fcc..ca388470398 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'; From f801025e2b2c1d4a5880c2d2283a7bb299afb6fb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 18 Aug 2021 10:39:54 +0200 Subject: [PATCH 04/17] small API extension, experiment with pin-command --- src/vs/vscode.proposed.d.ts | 6 +- .../workbench/api/common/extHostLanguages.ts | 8 +++ .../browser/languageStatus.contribution.ts | 7 ++- .../browser/languageStatusList.ts | 62 ++++++++++++++----- .../browser/media/languageStatus.css | 8 +-- .../common/languageStatusService.ts | 1 + 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c7c0cfd3131..9d9f9c57f48 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2829,7 +2829,11 @@ declare module 'vscode' { interface LanguageStatusItem { selector: DocumentSelector; - detail: string; + + text: string; + detail: string; // tooltip! + // command: string | Command | undefined; + severity: LanguageStatusSeverity; dispose(): void; } diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index 1dc9f2d0067..cf2ced19fe0 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -87,6 +87,7 @@ export class ExtHostLanguages { this._proxy.$setLanguageStatus(handle, { source: extension.displayName ?? extension.name, selector: data.selector, + text: data.text, message: data.detail, severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info }); @@ -101,6 +102,13 @@ export class ExtHostLanguages { data.selector = value; updateAsync(); }, + get text() { + return data.text; + }, + set text(value) { + data.text = value; + updateAsync(); + }, get detail() { return data.detail; }, diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 58c40fc853b..5baf1a039a9 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti 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_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; +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 { LanguageStatusDetailsWidget } from 'vs/workbench/contrib/languageStatus/browser/languageStatusList'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; @@ -71,10 +71,11 @@ class EditorStatusContribution implements IWorkbenchContribution { 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); - } else if (first.severity === Severity.Warning) { - color = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); } const props: IStatusbarEntry = { diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts index 336f115a83d..e2fbb7a0a48 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts @@ -12,50 +12,80 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_BORDER } from 'vs/workbench/common/theme'; import { ILanguageStatus } from 'vs/workbench/services/languageStatus/common/languageStatusService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Link } from 'vs/platform/opener/browser/link'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +// import Severity from 'vs/base/common/severity'; class LanguageStatusTemplate { - readonly firstLine: HTMLDivElement; - readonly secondLine: HTMLDivElement; + readonly text: HTMLDivElement; + readonly toolbar: ToolBar; constructor( readonly container: HTMLElement, - @IOpenerService private readonly _openerService: IOpenerService + @IContextMenuService contextMenuService: IContextMenuService, + @IOpenerService private readonly _openerService: IOpenerService, ) { container.classList.add('status-element'); - this.firstLine = document.createElement('div'); - this.secondLine = document.createElement('div'); - this.secondLine.classList.add('detail'); - container.appendChild(this.firstLine); - container.appendChild(this.secondLine); + this.text = document.createElement('div'); + container.appendChild(this.text); + + const toolbarContainer = document.createElement('div'); + container.appendChild(toolbarContainer); + + this.toolbar = new ToolBar(toolbarContainer, contextMenuService, {}); } dispose() { - + this.toolbar.dispose(); } set(element: ILanguageStatus): void { // message // source - dom.clearNode(this.firstLine); + dom.clearNode(this.text); + + // switch (element.severity) { + // case Severity.Error: + // dom.append(this.text, ...renderLabelWithIcons('$(error)')); + // break; + // case Severity.Warning: + // dom.append(this.text, ...renderLabelWithIcons('$(warning)')); + // break; + // case Severity.Info: + // default: + // dom.append(this.text, ...renderLabelWithIcons('$(info)')); + // break; + // } + for (let node of parseLinkedText(element.message).nodes) { if (typeof node === 'string') { const parts = renderLabelWithIcons(node); - dom.append(this.firstLine, ...parts); + dom.append(this.text, ...parts); } else { - dom.append(this.firstLine, new Link(node, undefined, this._openerService).el); + dom.append(this.text, new Link(node, undefined, this._openerService).el); } } - dom.reset(this.secondLine, element.source); + this.toolbar.setActions([new Action( + 'dd', + localize('label.pin', 'Pin'), + ThemeIcon.asClassName(Codicon.pin), + true, + () => { + console.log(element); + } + )]); } } @@ -68,7 +98,7 @@ class Renderer implements IListRenderer // delegate getHeight(element: ILanguageStatus): number { - return 56; + return 42; } getTemplateId(element: ILanguageStatus): string { return Renderer.templateId; @@ -158,7 +188,7 @@ export class LanguageStatusDetailsWidget { } layout() { - const width = Math.max(200, this._layoutService.dimension.width * 0.6); + const width = Math.min(460, Math.max(200, this._layoutService.dimension.width * 0.6)); this._container.style.width = `${width}px`; this._list.layout(); } diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 5888810413e..b3a4baf0505 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -4,17 +4,15 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .language-status { - margin: 8px; + margin: 7px; cursor: default; box-shadow: var(--code-widget-shadow) 0 0 8px 2px; } .monaco-workbench .language-status .list-container .status-element { padding: 10px 5px; -} - -.monaco-workbench .language-status .list-container .status-element div.detail { - font-size: smaller; + display: flex; + justify-content: space-between; } .monaco-workbench .language-status .list-container .monaco-list-row[data-last-element="false"].status-element { diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index 9cdb7202b97..6a709fb59c0 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -17,6 +17,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ILanguageStatus { selector: LanguageSelector, severity: Severity; + text: string; message: string; source: string; } From b582cd4182171f30381ae65207c4b73951555bd0 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 18 Aug 2021 11:34:21 +0200 Subject: [PATCH 05/17] use hover instead of flyout --- .../browser/languageStatus.contribution.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 5baf1a039a9..681099725fd 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -78,14 +79,23 @@ class EditorStatusContribution implements IWorkbenchContribution { color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); } + // todo@jrieken is is NOT OK because all md-strings are now trusted even though + // they are from different extensions + const tooltip = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + for (let status of this._status) { + tooltip.appendMarkdown(status.message); + tooltip.appendMarkdown('\n\n---\n\n'); + } + const props: IStatusbarEntry = { name: localize('status.editor.status', "Language Status"), text: '$(circle-large-outline)', ariaLabel: localize('status.editor.status', "Language Status"), backgroundColor, color, - command: EditorStatusContribution._id, - showBeak: this._showingDetails + // command: EditorStatusContribution._id, + showBeak: this._showingDetails, + tooltip }; if (!this._entry.value) { From d6a31ab3b0f7ec7b9321a3e4578a5bd9a805e097 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Aug 2021 17:11:52 +0200 Subject: [PATCH 06/17] no hover padding when showing html element contents, fyi @Tyriar --- src/vs/base/browser/ui/hover/hover.css | 2 +- src/vs/workbench/services/hover/browser/hoverWidget.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 9e3b6888ef2..4de24334292 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; } diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index f730994aecb..4e30614322b 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; From 222214eddf462aa1675aab006fe6be0b1bda385e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Aug 2021 17:12:39 +0200 Subject: [PATCH 07/17] WIP use html element as hover element, needs status bar adoption, pin in noop --- .../workbench/api/common/extHostLanguages.ts | 4 +- .../browser/languageStatus.contribution.ts | 110 +++++----- .../browser/languageStatusList.ts | 195 ------------------ .../browser/media/languageStatus.css | 16 +- .../common/languageStatusService.ts | 4 +- 5 files changed, 68 insertions(+), 261 deletions(-) delete mode 100644 src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index cf2ced19fe0..9aa3dda450c 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -87,8 +87,8 @@ export class ExtHostLanguages { this._proxy.$setLanguageStatus(handle, { source: extension.displayName ?? extension.name, selector: data.selector, - text: data.text, - message: data.detail, + label: data.text, + detail: data.detail, severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info }); }, 0); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 681099725fd..933a235d418 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -3,24 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +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 Severity from 'vs/base/common/severity'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeColor, themeColorFromId, ThemeIcon } 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 { LanguageStatusDetailsWidget } from 'vs/workbench/contrib/languageStatus/browser/languageStatusList'; +import { NOTIFICATIONS_BORDER, 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, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/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 { 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 { @@ -30,17 +33,13 @@ class EditorStatusContribution implements IWorkbenchContribution { private readonly _disposables = new DisposableStore(); private _status: ILanguageStatus[] = []; - private _showingDetails: boolean = false; constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @IStatusbarService private readonly _statusBarService: IStatusbarService, @IEditorService private readonly _editorService: IEditorService, - @IContextViewService private readonly _contextViewService: IContextViewService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IOpenerService private readonly _openerService: IOpenerService, ) { - this._disposables.add(CommandsRegistry.registerCommand(EditorStatusContribution._id, () => this._toggleDetails())); - _languageStatusService.onDidChange(this._update, this, this._disposables); _editorService.onDidActiveEditorChange(this._update, this, this._disposables); this._update(); @@ -79,12 +78,9 @@ class EditorStatusContribution implements IWorkbenchContribution { color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); } - // todo@jrieken is is NOT OK because all md-strings are now trusted even though - // they are from different extensions - const tooltip = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - for (let status of this._status) { - tooltip.appendMarkdown(status.message); - tooltip.appendMarkdown('\n\n---\n\n'); + const element = document.createElement('div'); + for (const status of this._status) { + element.appendChild(this._renderStatus(status)); } const props: IStatusbarEntry = { @@ -93,9 +89,7 @@ class EditorStatusContribution implements IWorkbenchContribution { ariaLabel: localize('status.editor.status', "Language Status"), backgroundColor, color, - // command: EditorStatusContribution._id, - showBeak: this._showingDetails, - tooltip + tooltip: element }; if (!this._entry.value) { @@ -105,44 +99,54 @@ class EditorStatusContribution implements IWorkbenchContribution { } } - private _toggleDetails(): void { - if (!this._entry.value) { - return; - } + private _renderStatus(status: ILanguageStatus): HTMLElement { - if (this._showingDetails) { - this._contextViewService.hideContextView(); - // this._showingDetails = false; // happens in onHide - return; - } + const node = document.createElement('div'); + node.classList.add('hover-language-status-element'); - const anchor = document.getElementById(EditorStatusContribution._id); - if (!anchor) { - return; - } + const left = document.createElement('div'); + node.appendChild(left); - let widget: LanguageStatusDetailsWidget | undefined; + const detail = document.createElement('div'); + detail.classList.add('detail'); + this._renderTextPlus(detail, status.detail); + left.appendChild(detail); - this._contextViewService.showContextView({ - getAnchor: () => anchor, - render: container => { - widget = this._instantiationService.createInstance(LanguageStatusDetailsWidget, this._status, container); - return toDisposable(() => { - widget?.dispose(); - widget = undefined; - }); + const label = document.createElement('div'); + label.classList.add('label'); + this._renderTextPlus(label, status.label); + left.appendChild(label); - }, - onHide: () => { - this._showingDetails = false; - this._update(); + const right = document.createElement('div'); + node.appendChild(right); + + const actions = new ActionBar(right, {}); + actions.push(new Action( + 'pin', + localize('label.pin', 'Pin'), + ThemeIcon.asClassName(Codicon.pin), + true, + () => { + console.log(status); } - }); - this._showingDetails = true; - this._update(); + ), { icon: true, label: false }); + return node; + } + + private _renderTextPlus(target: HTMLElement, text: string): void { + for (let node of parseLinkedText(text).nodes) { + if (typeof node === 'string') { + const parts = renderLabelWithIcons(node); + dom.append(target, ...parts); + } else { + dom.append(target, new Link(node, undefined, this._openerService).el); + } + } } } - +registerThemingParticipant((theme, collector) => { + collector.addRule(`:root { --code-notifications-border: ${theme.getColor(NOTIFICATIONS_BORDER)}}`); +}); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts deleted file mode 100644 index e2fbb7a0a48..00000000000 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatusList.ts +++ /dev/null @@ -1,195 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_BORDER } from 'vs/workbench/common/theme'; -import { ILanguageStatus } from 'vs/workbench/services/languageStatus/common/languageStatusService'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { parseLinkedText } from 'vs/base/common/linkedText'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { Link } from 'vs/platform/opener/browser/link'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Action } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -// import Severity from 'vs/base/common/severity'; - -class LanguageStatusTemplate { - - readonly text: HTMLDivElement; - readonly toolbar: ToolBar; - - constructor( - readonly container: HTMLElement, - @IContextMenuService contextMenuService: IContextMenuService, - @IOpenerService private readonly _openerService: IOpenerService, - ) { - container.classList.add('status-element'); - - this.text = document.createElement('div'); - container.appendChild(this.text); - - const toolbarContainer = document.createElement('div'); - container.appendChild(toolbarContainer); - - this.toolbar = new ToolBar(toolbarContainer, contextMenuService, {}); - } - - dispose() { - this.toolbar.dispose(); - } - - set(element: ILanguageStatus): void { - // message - // source - dom.clearNode(this.text); - - // switch (element.severity) { - // case Severity.Error: - // dom.append(this.text, ...renderLabelWithIcons('$(error)')); - // break; - // case Severity.Warning: - // dom.append(this.text, ...renderLabelWithIcons('$(warning)')); - // break; - // case Severity.Info: - // default: - // dom.append(this.text, ...renderLabelWithIcons('$(info)')); - // break; - // } - - for (let node of parseLinkedText(element.message).nodes) { - if (typeof node === 'string') { - const parts = renderLabelWithIcons(node); - dom.append(this.text, ...parts); - } else { - dom.append(this.text, new Link(node, undefined, this._openerService).el); - } - } - - this.toolbar.setActions([new Action( - 'dd', - localize('label.pin', 'Pin'), - ThemeIcon.asClassName(Codicon.pin), - true, - () => { - console.log(element); - } - )]); - } -} - -class Renderer implements IListRenderer, IListVirtualDelegate { - - static templateId: string = 'languageStatus'; - - constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { } - - // delegate - - getHeight(element: ILanguageStatus): number { - return 42; - } - getTemplateId(element: ILanguageStatus): string { - return Renderer.templateId; - } - - // renderer - - templateId: string = Renderer.templateId; - - renderTemplate(container: HTMLElement): LanguageStatusTemplate { - return this._instantiationService.createInstance(LanguageStatusTemplate, container); - } - renderElement(element: ILanguageStatus, index: number, templateData: LanguageStatusTemplate, height: number | undefined): void { - templateData.set(element); - } - disposeTemplate(templateData: LanguageStatusTemplate): void { - templateData.dispose(); - } - -} - -export class LanguageStatusDetailsWidget { - - private readonly _disposables = new DisposableStore(); - - private readonly _container: HTMLDivElement; - private readonly _list: WorkbenchList; - - constructor( - status: ILanguageStatus[], - parent: HTMLElement, - @IThemeService themeService: IThemeService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILayoutService private readonly _layoutService: ILayoutService, - ) { - parent.style.setProperty('--code-widget-shadow', themeService.getColorTheme().getColor(widgetShadow, true)?.toString() ?? 'inherit'); - parent.style.setProperty('--code-notifications-border', themeService.getColorTheme().getColor(NOTIFICATIONS_BORDER, true)?.toString() ?? 'inherit'); - parent.classList.add('language-status'); - - this._container = document.createElement('div'); - this._container.classList.add('list-container'); - parent.appendChild(this._container); - - const renderer = _instantiationService.createInstance(Renderer); - this._list = >this._instantiationService.createInstance( - WorkbenchList, - 'LanguageStatusList', - this._container, - renderer, - [renderer], - { - // ...this.options, - setRowLineHeight: false, - horizontalScrolling: false, - overrideStyles: { - listBackground: NOTIFICATIONS_BACKGROUND, - // listInactiveSelectionBackground: NOTIFICATIONS_BACKGROUND, - // listActiveSelectionBackground: NOTIFICATIONS_BACKGROUND, - }, - accessibilityProvider: { - getAriaLabel(element: ILanguageStatus): string { - return element.message; - }, - getWidgetAriaLabel(): string { - return localize('language status', "Language Status List"); - }, - getRole(): string { - return 'dialog'; // https://github.com/microsoft/vscode/issues/82728 - } - } - } - ); - - // no selections - this._disposables.add(this._list.onDidChangeSelection(e => { - if (e.indexes.length > 0) { - this._list.setSelection([]); - } - })); - - this._list.splice(0, this._list.length, status); - this.layout(); - } - - dispose(): void { - this._list.dispose(); - } - - layout() { - const width = Math.min(460, Math.max(200, this._layoutService.dimension.width * 0.6)); - this._container.style.width = `${width}px`; - this._list.layout(); - } -} diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index b3a4baf0505..ba4ab75e7ad 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -3,18 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .language-status { - margin: 7px; - cursor: default; - box-shadow: var(--code-widget-shadow) 0 0 8px 2px; -} - -.monaco-workbench .language-status .list-container .status-element { - padding: 10px 5px; +.monaco-workbench .hover-language-status-element { display: flex; justify-content: space-between; + padding: 4px 8px; } -.monaco-workbench .language-status .list-container .monaco-list-row[data-last-element="false"].status-element { +.monaco-workbench .hover-language-status-element:not(:last-child) { border-bottom: 1px solid var(--code-notifications-border); } + +.monaco-workbench .hover-language-status-element .monaco-action-bar { + padding-left: 8px; +} diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index 6a709fb59c0..2fc2a423646 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -17,8 +17,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ILanguageStatus { selector: LanguageSelector, severity: Severity; - text: string; - message: string; + label: string; + detail: string; source: string; } From 6ef61f0d4465402c8dfd9e266e233c2fd9acfe50 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Aug 2021 11:30:11 +0200 Subject: [PATCH 08/17] Allow HTMLElement as status bar tooltip --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 2 +- src/vs/base/browser/ui/iconLabel/iconLabelHover.ts | 8 ++++---- .../languageStatus/browser/languageStatus.contribution.ts | 2 +- src/vs/workbench/services/statusbar/browser/statusbar.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 7ce3fa0c95c..b54b5f62bc6 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 7f5ad71d042..0f4b9674ed6 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/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 933a235d418..bd1fdd457a8 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -17,7 +17,7 @@ import { NOTIFICATIONS_BORDER, STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERRO 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/common/statusbar'; +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'; diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 4c700b5e383..a6424fc1272 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 From 1fd14441235f07a4b246eccd0bd7f2aaa61bc484 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Aug 2021 13:04:41 +0200 Subject: [PATCH 09/17] support language status command, no more pin for now --- src/vs/vscode.proposed.d.ts | 6 ++-- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHostLanguages.ts | 34 +++++++++++++------ .../browser/languageStatus.contribution.ts | 30 ++++++++-------- .../browser/media/languageStatus.css | 5 +-- .../common/languageStatusService.ts | 2 ++ 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7712c9887b8..2b32b890461 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2865,12 +2865,10 @@ declare module 'vscode' { interface LanguageStatusItem { selector: DocumentSelector; - + severity: LanguageStatusSeverity; text: string; detail: string; // tooltip! - // command: string | Command | undefined; - - severity: LanguageStatusSeverity; + command: Command | undefined; dispose(): void; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8a401cd930c..1499d0d83e8 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); diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index 9aa3dda450c..296fd17dc65 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -10,20 +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 { @@ -73,28 +73,39 @@ export class ExtHostLanguages { const handle = this._handlePool++; const proxy = this._proxy; - const data: { selector: any, text: string, detail: string, severity: vscode.LanguageStatusSeverity } = { + const data: Omit = { selector, + 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, { source: extension.displayName ?? extension.name, selector: data.selector, label: data.text, detail: data.detail, - severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info + 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 selector() { return data.selector; }, @@ -123,9 +134,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/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index bd1fdd457a8..52fb84ab510 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -11,7 +11,7 @@ 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, ThemeColor, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { NOTIFICATIONS_BORDER, 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'; @@ -21,9 +21,8 @@ import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarA import { parseLinkedText } from 'vs/base/common/linkedText'; import { Link } from 'vs/platform/opener/browser/link'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Action } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { ICommandService } from 'vs/platform/commands/common/commands'; class EditorStatusContribution implements IWorkbenchContribution { @@ -39,6 +38,7 @@ class EditorStatusContribution implements IWorkbenchContribution { @IStatusbarService private readonly _statusBarService: IStatusbarService, @IEditorService private readonly _editorService: IEditorService, @IOpenerService private readonly _openerService: IOpenerService, + @ICommandService private readonly _commandService: ICommandService, ) { _languageStatusService.onDidChange(this._update, this, this._disposables); _editorService.onDidActiveEditorChange(this._update, this, this._disposables); @@ -120,16 +120,18 @@ class EditorStatusContribution implements IWorkbenchContribution { const right = document.createElement('div'); node.appendChild(right); - const actions = new ActionBar(right, {}); - actions.push(new Action( - 'pin', - localize('label.pin', 'Pin'), - ThemeIcon.asClassName(Codicon.pin), - true, - () => { - console.log(status); - } - ), { icon: true, label: false }); + 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); + } + }); + } return node; } diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index ba4ab75e7ad..7ddf6836dcf 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -13,6 +13,7 @@ border-bottom: 1px solid var(--code-notifications-border); } -.monaco-workbench .hover-language-status-element .monaco-action-bar { - padding-left: 8px; +.monaco-workbench .hover-language-status-element .monaco-button.monaco-text-button { + font-size: smaller; + padding: 2px 4px } diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index 2fc2a423646..bd02bf7b3a9 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; 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'; @@ -20,6 +21,7 @@ export interface ILanguageStatus { label: string; detail: string; source: string; + command: Command | undefined; } export interface ILanguageStatusProvider { From 3fbb432eae3854a4c4323042bcf12b5b3d631171 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Aug 2021 13:11:22 +0200 Subject: [PATCH 10/17] tweak padding of button --- .../languageStatus/browser/languageStatus.contribution.ts | 2 ++ .../contrib/languageStatus/browser/media/languageStatus.css | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 52fb84ab510..63a3e1aa8c6 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -105,6 +105,7 @@ class EditorStatusContribution implements IWorkbenchContribution { node.classList.add('hover-language-status-element'); const left = document.createElement('div'); + left.classList.add('left'); node.appendChild(left); const detail = document.createElement('div'); @@ -118,6 +119,7 @@ class EditorStatusContribution implements IWorkbenchContribution { left.appendChild(label); const right = document.createElement('div'); + right.classList.add('right'); node.appendChild(right); const { command } = status; diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 7ddf6836dcf..583fc143bb2 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -13,7 +13,11 @@ border-bottom: 1px solid var(--code-notifications-border); } -.monaco-workbench .hover-language-status-element .monaco-button.monaco-text-button { +.monaco-workbench .hover-language-status-element .right:not(:empty) { + padding-left: 24px; +} + +.monaco-workbench .hover-language-status-element .right .monaco-button.monaco-text-button { font-size: smaller; padding: 2px 4px } From 9652c1aeee670acaab204b5258bda5660fab7e41 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Aug 2021 13:18:25 +0200 Subject: [PATCH 11/17] tweak padding and position of button --- .../languageStatus/browser/media/languageStatus.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 583fc143bb2..e0158b1512f 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -7,17 +7,21 @@ 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 .right { + margin: auto 0; +} .monaco-workbench .hover-language-status-element .right:not(:empty) { - padding-left: 24px; + padding-left: 16px; } .monaco-workbench .hover-language-status-element .right .monaco-button.monaco-text-button { font-size: smaller; - padding: 2px 4px + padding: 2px 6px } From 61e2a76157a367284bc1cc996763fe89237f30bc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Aug 2021 16:48:17 +0200 Subject: [PATCH 12/17] render text and details, only detail supports mini-md --- src/vs/base/browser/ui/hover/hover.css | 2 +- .../browser/languageStatus.contribution.ts | 12 ++++++------ .../languageStatus/browser/media/languageStatus.css | 11 +++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 4de24334292..20f56038fd0 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -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/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 63a3e1aa8c6..4b4ca958723 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -108,16 +108,16 @@ class EditorStatusContribution implements IWorkbenchContribution { left.classList.add('left'); node.appendChild(left); - const detail = document.createElement('div'); + 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); left.appendChild(detail); - const label = document.createElement('div'); - label.classList.add('label'); - this._renderTextPlus(label, status.label); - left.appendChild(label); - const right = document.createElement('div'); right.classList.add('right'); node.appendChild(right); diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index e0158b1512f..5572d0d1c5d 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -13,6 +13,17 @@ .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; } From aa4f2d50516a5ab0965a331fe4064c37a3bf5540 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 25 Aug 2021 15:52:18 +0200 Subject: [PATCH 13/17] use info, warning, error icons, no colors for now, remove padding (hack) --- .../browser/languageStatus.contribution.ts | 9 ++++++--- .../languageStatus/browser/media/languageStatus.css | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 4b4ca958723..a43916fa335 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -70,12 +70,15 @@ class EditorStatusContribution implements IWorkbenchContribution { const [first] = this._status; let backgroundColor: ThemeColor | undefined; let color: ThemeColor | undefined; + let text: string = '$(info)'; if (first.severity === Severity.Error) { backgroundColor = themeColorFromId(STATUS_BAR_ERROR_ITEM_BACKGROUND); color = themeColorFromId(STATUS_BAR_ERROR_ITEM_FOREGROUND); + text = '$(error)'; } else if (first.severity === Severity.Warning) { backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); + text = '$(warning)'; } const element = document.createElement('div'); @@ -85,10 +88,10 @@ class EditorStatusContribution implements IWorkbenchContribution { const props: IStatusbarEntry = { name: localize('status.editor.status', "Language Status"), - text: '$(circle-large-outline)', ariaLabel: localize('status.editor.status', "Language Status"), - backgroundColor, - color, + text, + // backgroundColor, + // color, tooltip: element }; diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 5572d0d1c5d..b41ea3507b9 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -36,3 +36,8 @@ font-size: smaller; padding: 2px 6px } + +/* todo@jrieken - unsets the default padding, something like friend/parent status bar items is needed */ +.monaco-workbench .part.statusbar > .items-container > #status\.languageStatus.statusbar-item a { + padding-left: 0; +} From 258ca706a7932efc26c8659b8252971b4ad7bd51 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 26 Aug 2021 11:43:37 +0200 Subject: [PATCH 14/17] more CSS trickery to get linked status bar items to work --- .../browser/languageStatus.contribution.ts | 17 +++++++---------- .../browser/media/languageStatus.css | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index a43916fa335..82dce2636f3 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -11,9 +11,9 @@ 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, ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { NOTIFICATIONS_BORDER, 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 { 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'; @@ -68,16 +68,10 @@ class EditorStatusContribution implements IWorkbenchContribution { } const [first] = this._status; - let backgroundColor: ThemeColor | undefined; - let color: ThemeColor | undefined; let text: string = '$(info)'; if (first.severity === Severity.Error) { - backgroundColor = themeColorFromId(STATUS_BAR_ERROR_ITEM_BACKGROUND); - color = themeColorFromId(STATUS_BAR_ERROR_ITEM_FOREGROUND); text = '$(error)'; } else if (first.severity === Severity.Warning) { - backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); - color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); text = '$(warning)'; } @@ -96,7 +90,7 @@ class EditorStatusContribution implements IWorkbenchContribution { }; if (!this._entry.value) { - this._entry.value = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.06); + this._entry.value = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.11); } else { this._entry.value.update(props); } @@ -153,7 +147,10 @@ class EditorStatusContribution implements IWorkbenchContribution { } registerThemingParticipant((theme, collector) => { - collector.addRule(`:root { --code-notifications-border: ${theme.getColor(NOTIFICATIONS_BORDER)}}`); + 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 index b41ea3507b9..977f8ffa3e9 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -38,6 +38,19 @@ } /* todo@jrieken - unsets the default padding, something like friend/parent status bar items is needed */ -.monaco-workbench .part.statusbar > .items-container > #status\.languageStatus.statusbar-item a { - padding-left: 0; +.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; } From f9b95de0a9e33e33a21ca65c0f7cc98aea98a64d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 27 Aug 2021 17:02:15 +0200 Subject: [PATCH 15/17] further alignments between LanguageStatusItem and StatusBarItem, add pin command that makes language status become a status bar item --- src/vs/vscode.proposed.d.ts | 6 +- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHostLanguages.ts | 16 +- .../browser/languageStatus.contribution.ts | 137 +++++++++++++----- .../browser/media/languageStatus.css | 5 + .../common/languageStatusService.ts | 15 +- 6 files changed, 134 insertions(+), 49 deletions(-) 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 { From 1c8d83f71923cfc644ccdc6d3a212c2968947dbc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 27 Aug 2021 17:18:12 +0200 Subject: [PATCH 16/17] dispose things that are created on render --- .../browser/languageStatus.contribution.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index f2e89eb27e0..62a7d48050d 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -34,9 +34,11 @@ class EditorStatusContribution implements IWorkbenchContribution { private readonly _disposables = new DisposableStore(); + private readonly _dedicated = new Set(); + private _combinedEntry?: IStatusbarEntryAccessor; private _dedicatedEntries = new Map(); - private _dedicated = new Set(); + private _renderDisposables = new DisposableStore(); constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @@ -61,6 +63,7 @@ class EditorStatusContribution implements IWorkbenchContribution { this._disposables.dispose(); this._combinedEntry?.dispose(); dispose(this._dedicatedEntries.values()); + this._renderDisposables.dispose(); } private _getLanguageStatus(): [combined: ILanguageStatus[], dedicated: ILanguageStatus[]] { @@ -85,6 +88,8 @@ class EditorStatusContribution implements IWorkbenchContribution { const [combined, dedicated] = this._getLanguageStatus(); + this._renderDisposables.clear(); + // combined status bar item is a single item which hover shows // each status item if (combined.length === 0) { @@ -102,7 +107,7 @@ class EditorStatusContribution implements IWorkbenchContribution { } const element = document.createElement('div'); for (const status of combined) { - element.appendChild(this._renderStatus(status)); + element.appendChild(this._renderStatus(status, this._renderDisposables)); } const props: IStatusbarEntry = { name: localize('status.editor.status', "Editor Language Status"), @@ -118,7 +123,6 @@ class EditorStatusContribution implements IWorkbenchContribution { } // 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); @@ -135,7 +139,7 @@ class EditorStatusContribution implements IWorkbenchContribution { this._dedicatedEntries = newDedicatedEntries; } - private _renderStatus(status: ILanguageStatus): HTMLElement { + private _renderStatus(status: ILanguageStatus, store: DisposableStore): HTMLElement { const node = document.createElement('div'); node.classList.add('hover-language-status-element'); @@ -151,7 +155,7 @@ class EditorStatusContribution implements IWorkbenchContribution { const detail = document.createElement('span'); detail.classList.add('detail'); - this._renderTextPlus(detail, status.detail); + this._renderTextPlus(detail, status.detail, store); left.appendChild(detail); const right = document.createElement('div'); @@ -169,6 +173,7 @@ class EditorStatusContribution implements IWorkbenchContribution { this._commandService.executeCommand(command.id); } }); + store.add(btn); } // -- pin @@ -177,19 +182,23 @@ class EditorStatusContribution implements IWorkbenchContribution { this._statusBarService.updateEntryVisibility(status.id, true); this._update(); }); - const actions = new ActionBar(right, {}); - actions.push(action, { icon: true, label: false }); + 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): void { + 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 { - dom.append(target, new Link(node, undefined, this._openerService).el); + const link = new Link(node, undefined, this._openerService); + store.add(link); + dom.append(target, link.el); } } } From 5f3b7e1ef91051f7847f6cee9409cb4847c16484 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 30 Aug 2021 11:26:47 +0200 Subject: [PATCH 17/17] persist pinned language status items --- .../browser/languageStatus.contribution.ts | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 62a7d48050d..4214053bae6 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -27,15 +27,32 @@ 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 readonly _dedicated = new Set(); + private _dedicated = new Set(); + private _model?: LanguageStatusViewModel; private _combinedEntry?: IStatusbarEntryAccessor; private _dedicatedEntries = new Map(); private _renderDisposables = new DisposableStore(); @@ -46,7 +63,11 @@ class EditorStatusContribution implements IWorkbenchContribution { @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(); @@ -55,8 +76,10 @@ class EditorStatusContribution implements IWorkbenchContribution { if (!e.visible && this._dedicated.has(e.id)) { this._dedicated.delete(e.id); this._update(); + this._storeState(); } }, this._disposables); + } dispose(): void { @@ -66,10 +89,41 @@ class EditorStatusContribution implements IWorkbenchContribution { this._renderDisposables.dispose(); } - private _getLanguageStatus(): [combined: ILanguageStatus[], dedicated: ILanguageStatus[]] { + // --- 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 [[], []]; + return new LanguageStatusViewModel([], []); } const all = this._languageStatusService.getLanguageStatus(editor.getModel()); const combined: ILanguageStatus[] = []; @@ -81,24 +135,30 @@ class EditorStatusContribution implements IWorkbenchContribution { combined.push(item); } } - return [combined, dedicated]; + return new LanguageStatusViewModel(combined, dedicated); } private _update(): void { - const [combined, dedicated] = this._getLanguageStatus(); + 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 (combined.length === 0) { + if (model.combined.length === 0) { // nothing this._combinedEntry?.dispose(); this._combinedEntry = undefined; } else { - const [first] = combined; + const [first] = model.combined; let text: string = '$(info)'; if (first.severity === Severity.Error) { text = '$(error)'; @@ -106,7 +166,7 @@ class EditorStatusContribution implements IWorkbenchContribution { text = '$(warning)'; } const element = document.createElement('div'); - for (const status of combined) { + for (const status of model.combined) { element.appendChild(this._renderStatus(status, this._renderDisposables)); } const props: IStatusbarEntry = { @@ -124,7 +184,7 @@ class EditorStatusContribution implements IWorkbenchContribution { // dedicated status bar items are shows as-is in the status bar const newDedicatedEntries = new Map(); - for (const status of dedicated) { + for (const status of model.dedicated) { const props = EditorStatusContribution._asStatusbarEntry(status); let entry = this._dedicatedEntries.get(status.id); if (!entry) { @@ -181,6 +241,7 @@ class EditorStatusContribution implements IWorkbenchContribution { 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 });