mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-24 17:31:37 +01:00
Merge pull request #131264 from microsoft/joh/langStatus
Language status items
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents {
|
||||
.monaco-hover .hover-contents:not(.html-hover-contents) {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
|
||||
/** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span {
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span {
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface IIconLabelCreationOptions {
|
||||
}
|
||||
|
||||
export interface IIconLabelMarkdownString {
|
||||
markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
|
||||
markdown: IMarkdownString | string | HTMLElement | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
|
||||
markdownNotSupportedFallback: string | undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string | IMarkdownString | undefined> {
|
||||
if (isString(markdownTooltip)) {
|
||||
function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString | HTMLElement): (token: CancellationToken) => Promise<string | IMarkdownString | HTMLElement | undefined> {
|
||||
if (isString(markdownTooltip) || markdownTooltip instanceof HTMLElement) {
|
||||
return async () => markdownTooltip;
|
||||
} else if (isFunction(markdownTooltip.markdown)) {
|
||||
return markdownTooltip.markdown;
|
||||
|
||||
Vendored
+6
-3
@@ -2873,15 +2873,18 @@ declare module 'vscode' {
|
||||
}
|
||||
|
||||
interface LanguageStatusItem {
|
||||
readonly id: string;
|
||||
selector: DocumentSelector;
|
||||
text: string;
|
||||
detail: string | MarkdownString
|
||||
severity: LanguageStatusSeverity;
|
||||
name: string | undefined;
|
||||
text: string;
|
||||
detail: string;
|
||||
command: Command | undefined;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
namespace languages {
|
||||
export function createLanguageStatusItem(selector: DocumentSelector): LanguageStatusItem;
|
||||
export function createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -186,7 +186,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService);
|
||||
const extHostDialogs = new ExtHostDialogs(rpcProtocol);
|
||||
const extHostStatusBar = new ExtHostStatusBar(rpcProtocol, extHostCommands.converter);
|
||||
const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments);
|
||||
const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments, extHostCommands.converter);
|
||||
|
||||
// Register API-ish commands
|
||||
ExtHostApiCommands.register(extHostCommands);
|
||||
@@ -507,9 +507,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostLanguageFeatures.registerTypeHierarchyProvider(extension, selector, provider);
|
||||
},
|
||||
createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem {
|
||||
createLanguageStatusItem(id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostLanguages.createLanguageStatusItem(selector);
|
||||
return extHostLanguages.createLanguageStatusItem(extension, id, selector);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,19 +10,20 @@ import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { StandardTokenType, Range, Position, LanguageStatusSeverity } from 'vs/workbench/api/common/extHostTypes';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { disposableTimeout } from 'vs/base/common/async';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
|
||||
|
||||
export class ExtHostLanguages {
|
||||
|
||||
private readonly _proxy: MainThreadLanguagesShape;
|
||||
private readonly _documents: ExtHostDocuments;
|
||||
|
||||
constructor(
|
||||
mainContext: IMainContext,
|
||||
documents: ExtHostDocuments
|
||||
private readonly _documents: ExtHostDocuments,
|
||||
private readonly _commands: CommandsConverter
|
||||
) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadLanguages);
|
||||
this._documents = documents;
|
||||
}
|
||||
|
||||
getLanguages(): Promise<string[]> {
|
||||
@@ -67,32 +68,58 @@ export class ExtHostLanguages {
|
||||
|
||||
private _handlePool: number = 0;
|
||||
|
||||
createLanguageStatusItem(selector: vscode.DocumentSelector): vscode.LanguageStatusItem {
|
||||
createLanguageStatusItem(extension: IExtensionDescription, id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem {
|
||||
|
||||
const handle = this._handlePool++;
|
||||
const proxy = this._proxy;
|
||||
|
||||
const data: { selector: any, text: string, detail: string | vscode.MarkdownString, severity: vscode.LanguageStatusSeverity } = {
|
||||
const data: Omit<vscode.LanguageStatusItem, 'dispose'> = {
|
||||
selector,
|
||||
id,
|
||||
name: extension.displayName ?? extension.name,
|
||||
severity: LanguageStatusSeverity.Information,
|
||||
command: undefined,
|
||||
text: '',
|
||||
detail: '',
|
||||
severity: LanguageStatusSeverity.Information,
|
||||
};
|
||||
|
||||
let soonHandle: IDisposable | undefined;
|
||||
let commandDisposables = new DisposableStore();
|
||||
const updateAsync = () => {
|
||||
soonHandle?.dispose();
|
||||
soonHandle = disposableTimeout(() => {
|
||||
|
||||
commandDisposables.clear();
|
||||
|
||||
this._proxy.$setLanguageStatus(handle, {
|
||||
id: `${extension.identifier.value}/${id}`,
|
||||
name: data.name ?? extension.displayName ?? extension.name,
|
||||
source: extension.displayName ?? extension.name,
|
||||
selector: data.selector,
|
||||
text: data.text,
|
||||
message: typeof data.detail === 'string' ? data.detail : typeConvert.MarkdownString.from(data.detail),
|
||||
severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info
|
||||
label: data.text,
|
||||
detail: data.detail,
|
||||
severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info,
|
||||
command: data.command && this._commands.toInternal(data.command, commandDisposables)
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const result: vscode.LanguageStatusItem = {
|
||||
dispose() {
|
||||
commandDisposables.dispose();
|
||||
soonHandle?.dispose();
|
||||
proxy.$removeLanguageStatus(handle);
|
||||
},
|
||||
get id() {
|
||||
return data.id;
|
||||
},
|
||||
get name() {
|
||||
return data.name;
|
||||
},
|
||||
set name(value) {
|
||||
data.name = value;
|
||||
updateAsync();
|
||||
},
|
||||
get selector() {
|
||||
return data.selector;
|
||||
},
|
||||
@@ -121,9 +148,12 @@ export class ExtHostLanguages {
|
||||
data.severity = value;
|
||||
updateAsync();
|
||||
},
|
||||
dispose() {
|
||||
soonHandle?.dispose();
|
||||
proxy.$removeLanguageStatus(handle);
|
||||
get command() {
|
||||
return data.command;
|
||||
},
|
||||
set command(value) {
|
||||
data.command = value;
|
||||
updateAsync();
|
||||
}
|
||||
};
|
||||
updateAsync();
|
||||
|
||||
@@ -37,7 +37,7 @@ import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorE
|
||||
import { ConfigurationChangedEvent, IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { deepClone, equals } from 'vs/base/common/objects';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
@@ -50,11 +50,10 @@ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessi
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar';
|
||||
import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers';
|
||||
import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERROR_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme';
|
||||
import { ThemeColor, themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
|
||||
import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService';
|
||||
import { AutomaticLanguageDetectionLikelyWrongClassification, AutomaticLanguageDetectionLikelyWrongId, IAutomaticLanguageDetectionLikelyWrongData, ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
|
||||
|
||||
class SideBySideEditorEncodingSupport implements IEncodingSupport {
|
||||
@@ -182,7 +181,6 @@ class StateChange {
|
||||
type StateDelta = (
|
||||
{ type: 'selectionStatus'; selectionStatus: string | undefined; }
|
||||
| { type: 'mode'; mode: string | undefined; }
|
||||
| { type: 'languageStatus'; status: ILanguageStatus[] | undefined; }
|
||||
| { type: 'encoding'; encoding: string | undefined; }
|
||||
| { type: 'EOL'; EOL: string | undefined; }
|
||||
| { type: 'indentation'; indentation: string | undefined; }
|
||||
@@ -200,9 +198,6 @@ class State {
|
||||
private _mode: string | undefined;
|
||||
get mode(): string | undefined { return this._mode; }
|
||||
|
||||
private _status: ILanguageStatus[] | undefined;
|
||||
get status(): ILanguageStatus[] | undefined { return this._status; }
|
||||
|
||||
private _encoding: string | undefined;
|
||||
get encoding(): string | undefined { return this._encoding; }
|
||||
|
||||
@@ -248,13 +243,6 @@ class State {
|
||||
}
|
||||
}
|
||||
|
||||
if (update.type === 'languageStatus') {
|
||||
if (!equals(this._status, update.status)) {
|
||||
this._status = update.status;
|
||||
change.languageStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (update.type === 'encoding') {
|
||||
if (this._encoding !== update.encoding) {
|
||||
this._encoding = update.encoding;
|
||||
@@ -318,7 +306,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
|
||||
private readonly encodingElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
|
||||
private readonly eolElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
|
||||
private readonly modeElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
|
||||
private readonly statusElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
|
||||
private readonly metadataElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
|
||||
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<void> {
|
||||
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 };
|
||||
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/languageStatus';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { DisposableStore, dispose } from 'vs/base/common/lifecycle';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { NOTIFICATIONS_BORDER, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
|
||||
import { parseLinkedText } from 'vs/base/common/linkedText';
|
||||
import { Link } from 'vs/platform/opener/browser/link';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
|
||||
class LanguageStatusViewModel {
|
||||
|
||||
constructor(
|
||||
readonly combined: readonly ILanguageStatus[],
|
||||
readonly dedicated: readonly ILanguageStatus[]
|
||||
) { }
|
||||
|
||||
isEqual(other: LanguageStatusViewModel) {
|
||||
return equals(this.combined, other.combined) && equals(this.dedicated, other.dedicated);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorStatusContribution implements IWorkbenchContribution {
|
||||
|
||||
private static readonly _id = 'status.languageStatus';
|
||||
|
||||
private static readonly _keyDedicatedItems = 'languageStatus.dedicated';
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _dedicated = new Set<string>();
|
||||
|
||||
private _model?: LanguageStatusViewModel;
|
||||
private _combinedEntry?: IStatusbarEntryAccessor;
|
||||
private _dedicatedEntries = new Map<string, IStatusbarEntryAccessor>();
|
||||
private _renderDisposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
@ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService,
|
||||
@IStatusbarService private readonly _statusBarService: IStatusbarService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
_storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables);
|
||||
this._restoreState();
|
||||
|
||||
_languageStatusService.onDidChange(this._update, this, this._disposables);
|
||||
_editorService.onDidActiveEditorChange(this._update, this, this._disposables);
|
||||
this._update();
|
||||
|
||||
_statusBarService.onDidChangeEntryVisibility(e => {
|
||||
if (!e.visible && this._dedicated.has(e.id)) {
|
||||
this._dedicated.delete(e.id);
|
||||
this._update();
|
||||
this._storeState();
|
||||
}
|
||||
}, this._disposables);
|
||||
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._combinedEntry?.dispose();
|
||||
dispose(this._dedicatedEntries.values());
|
||||
this._renderDisposables.dispose();
|
||||
}
|
||||
|
||||
// --- persisting dedicated items
|
||||
|
||||
private _handleStorageChange(e: IStorageValueChangeEvent) {
|
||||
if (e.key !== EditorStatusContribution._keyDedicatedItems) {
|
||||
return;
|
||||
}
|
||||
this._restoreState();
|
||||
this._update();
|
||||
}
|
||||
|
||||
private _restoreState(): void {
|
||||
const raw = this._storageService.get(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL, '[]');
|
||||
try {
|
||||
const ids = <string[]>JSON.parse(raw);
|
||||
this._dedicated = new Set(ids);
|
||||
} catch {
|
||||
this._dedicated.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private _storeState(): void {
|
||||
if (this._dedicated.size === 0) {
|
||||
this._storageService.remove(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL);
|
||||
} else {
|
||||
const raw = JSON.stringify(Array.from(this._dedicated.keys()));
|
||||
this._storageService.store(EditorStatusContribution._keyDedicatedItems, raw, StorageScope.GLOBAL, StorageTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
// --- language status model and UI
|
||||
|
||||
private _createViewModel(): LanguageStatusViewModel {
|
||||
const editor = getCodeEditor(this._editorService.activeTextEditorControl);
|
||||
if (!editor?.hasModel()) {
|
||||
return new LanguageStatusViewModel([], []);
|
||||
}
|
||||
const all = this._languageStatusService.getLanguageStatus(editor.getModel());
|
||||
const combined: ILanguageStatus[] = [];
|
||||
const dedicated: ILanguageStatus[] = [];
|
||||
for (let item of all) {
|
||||
if (this._dedicated.has(item.id)) {
|
||||
dedicated.push(item);
|
||||
} else {
|
||||
combined.push(item);
|
||||
}
|
||||
}
|
||||
return new LanguageStatusViewModel(combined, dedicated);
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
|
||||
const model = this._createViewModel();
|
||||
|
||||
if (this._model?.isEqual(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._model = model;
|
||||
|
||||
this._renderDisposables.clear();
|
||||
|
||||
// combined status bar item is a single item which hover shows
|
||||
// each status item
|
||||
if (model.combined.length === 0) {
|
||||
// nothing
|
||||
this._combinedEntry?.dispose();
|
||||
this._combinedEntry = undefined;
|
||||
|
||||
} else {
|
||||
const [first] = model.combined;
|
||||
let text: string = '$(info)';
|
||||
if (first.severity === Severity.Error) {
|
||||
text = '$(error)';
|
||||
} else if (first.severity === Severity.Warning) {
|
||||
text = '$(warning)';
|
||||
}
|
||||
const element = document.createElement('div');
|
||||
for (const status of model.combined) {
|
||||
element.appendChild(this._renderStatus(status, this._renderDisposables));
|
||||
}
|
||||
const props: IStatusbarEntry = {
|
||||
name: localize('status.editor.status', "Editor Language Status"),
|
||||
ariaLabel: localize('status.editor.status', "Editor Language Status"),
|
||||
tooltip: element,
|
||||
text,
|
||||
};
|
||||
if (!this._combinedEntry) {
|
||||
this._combinedEntry = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, 100.11);
|
||||
} else {
|
||||
this._combinedEntry.update(props);
|
||||
}
|
||||
}
|
||||
|
||||
// dedicated status bar items are shows as-is in the status bar
|
||||
const newDedicatedEntries = new Map<string, IStatusbarEntryAccessor>();
|
||||
for (const status of model.dedicated) {
|
||||
const props = EditorStatusContribution._asStatusbarEntry(status);
|
||||
let entry = this._dedicatedEntries.get(status.id);
|
||||
if (!entry) {
|
||||
entry = this._statusBarService.addEntry(props, status.id, StatusbarAlignment.RIGHT, 100.09999);
|
||||
} else {
|
||||
entry.update(props);
|
||||
this._dedicatedEntries.delete(status.id);
|
||||
}
|
||||
newDedicatedEntries.set(status.id, entry);
|
||||
}
|
||||
dispose(this._dedicatedEntries.values());
|
||||
this._dedicatedEntries = newDedicatedEntries;
|
||||
}
|
||||
|
||||
private _renderStatus(status: ILanguageStatus, store: DisposableStore): HTMLElement {
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.classList.add('hover-language-status-element');
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.classList.add('left');
|
||||
node.appendChild(left);
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.classList.add('label');
|
||||
dom.append(label, ...renderLabelWithIcons(status.label));
|
||||
left.appendChild(label);
|
||||
|
||||
const detail = document.createElement('span');
|
||||
detail.classList.add('detail');
|
||||
this._renderTextPlus(detail, status.detail, store);
|
||||
left.appendChild(detail);
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.classList.add('right');
|
||||
node.appendChild(right);
|
||||
|
||||
const { command } = status;
|
||||
if (command) {
|
||||
const btn = new Button(right, { title: command.tooltip });
|
||||
btn.label = command.title;
|
||||
btn.onDidClick(_e => {
|
||||
if (command.arguments) {
|
||||
this._commandService.executeCommand(command.id, ...command.arguments);
|
||||
} else {
|
||||
this._commandService.executeCommand(command.id);
|
||||
}
|
||||
});
|
||||
store.add(btn);
|
||||
}
|
||||
|
||||
// -- pin
|
||||
const action = new Action('pin', localize('pin', "Pin to Status Bar"), Codicon.pin.classNames, true, () => {
|
||||
this._dedicated.add(status.id);
|
||||
this._statusBarService.updateEntryVisibility(status.id, true);
|
||||
this._update();
|
||||
this._storeState();
|
||||
});
|
||||
const actionBar = new ActionBar(right, {});
|
||||
actionBar.push(action, { icon: true, label: false });
|
||||
store.add(action);
|
||||
store.add(actionBar);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private _renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void {
|
||||
for (let node of parseLinkedText(text).nodes) {
|
||||
if (typeof node === 'string') {
|
||||
const parts = renderLabelWithIcons(node);
|
||||
dom.append(target, ...parts);
|
||||
} else {
|
||||
const link = new Link(node, undefined, this._openerService);
|
||||
store.add(link);
|
||||
dom.append(target, link.el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private static _asStatusbarEntry(item: ILanguageStatus): IStatusbarEntry {
|
||||
return {
|
||||
name: item.name,
|
||||
text: item.label,
|
||||
ariaLabel: item.label,
|
||||
tooltip: new MarkdownString(item.detail, true),
|
||||
command: item.command
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
collector.addRule(`:root {
|
||||
--code-notifications-border: ${theme.getColor(NOTIFICATIONS_BORDER)};
|
||||
--code-language-status-item-active-background: ${theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND)?.darken(.8)};
|
||||
}`);
|
||||
});
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored);
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .hover-language-status-element {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element:not(:last-child) {
|
||||
border-bottom: 1px solid var(--code-notifications-border);
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element > .left > .label::after {
|
||||
content: '–';
|
||||
padding: 0 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element > .left > .label:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element .right {
|
||||
margin: auto 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element .right:not(:empty) {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element .right .monaco-button.monaco-text-button {
|
||||
font-size: smaller;
|
||||
padding: 2px 6px
|
||||
}
|
||||
|
||||
.monaco-workbench .hover-language-status-element .right .monaco-action-bar:not(:first-child) {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* todo@jrieken - unsets the default padding, something like friend/parent status bar items is needed */
|
||||
.monaco-workbench .part.statusbar > .items-container > DIV#status\.languageStatus.statusbar-item a {
|
||||
padding: 0 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.statusbar > .items-container > DIV#status\.languageStatus.statusbar-item a:hover {
|
||||
background-color: var(--code-language-status-item-active-background);
|
||||
}
|
||||
|
||||
.monaco-workbench .part.statusbar > .items-container > DIV#status\.editor\.mode.statusbar-item:hover + DIV#status\.languageStatus.statusbar-item {
|
||||
background-color: var(--code-language-status-item-active-background);
|
||||
}
|
||||
|
||||
.monaco-workbench .part.statusbar > .items-container > DIV#status\.editor\.mode.statusbar-item A {
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
import { LanguageSelector } from 'vs/editor/common/modes/languageSelector';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
@@ -16,10 +16,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
|
||||
|
||||
export interface ILanguageStatus {
|
||||
selector: LanguageSelector,
|
||||
severity: Severity;
|
||||
text: string;
|
||||
message: string | IMarkdownString;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
|
||||
readonly selector: LanguageSelector;
|
||||
readonly severity: Severity;
|
||||
readonly label: string;
|
||||
readonly detail: string;
|
||||
readonly source: string;
|
||||
readonly command: Command | undefined;
|
||||
}
|
||||
|
||||
export interface ILanguageStatusProvider {
|
||||
@@ -36,7 +41,7 @@ export interface ILanguageStatusService {
|
||||
|
||||
addStatus(status: ILanguageStatus): IDisposable;
|
||||
|
||||
getLanguageStatus(model: ITextModel): Promise<ILanguageStatus[]>;
|
||||
getLanguageStatus(model: ITextModel): ILanguageStatus[];
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +57,7 @@ class LanguageStatusServiceImpl implements ILanguageStatusService {
|
||||
return this._provider.register(status.selector, status);
|
||||
}
|
||||
|
||||
async getLanguageStatus(model: ITextModel): Promise<ILanguageStatus[]> {
|
||||
getLanguageStatus(model: ITextModel): ILanguageStatus[] {
|
||||
return this._provider.ordered(model).sort((a, b) => b.severity - a.severity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user