diff --git a/src/vs/editor/common/services/languageStatusService.ts b/src/vs/editor/common/services/languageStatusService.ts new file mode 100644 index 0000000000000..e9152560dc9d4 --- /dev/null +++ b/src/vs/editor/common/services/languageStatusService.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Emitter, 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 { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { LanguageSelector } from 'vs/editor/common/modes/languageSelector'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + + +export interface ILanguageStatus { + severity: Severity; + text: string; + message: string | IMarkdownString; +} + +export interface ILanguageStatusProvider { + provideLanguageStatus(langId: string, token: CancellationToken): Promise +} + +export const ILanguageStatusService = createDecorator('ILanguageStatusService'); + +export interface ILanguageStatusService { + + _serviceBrand: undefined; + + onDidChange: Event; + + registerLanguageStatusProvider(selector: LanguageSelector, provider: ILanguageStatusProvider): IDisposable; + + getLanguageStatus(model: ITextModel): Promise; +} + + +class LanguageStatusServiceImpl implements ILanguageStatusService { + declare _serviceBrand: undefined; + + private readonly _provider = new LanguageFeatureRegistry(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = Event.any(this._onDidChange.event, this._provider.onDidChange); + + dispose() { + this._onDidChange.dispose(); + } + + registerLanguageStatusProvider(selector: LanguageSelector, provider: ILanguageStatusProvider): IDisposable { + return this._provider.register(selector, provider); + } + + async getLanguageStatus(model: ITextModel): Promise { + const all: ILanguageStatus[] = []; + for (const provider of this._provider.ordered(model)) { + try { + const status = await provider.provideLanguageStatus(model.getLanguageIdentifier().language, CancellationToken.None); + if (status) { + all.push(status); + } + } catch (err) { + onUnexpectedExternalError(err); + } + } + return all.sort((a, b) => b.severity - a.severity); + } +} + +registerSingleton(ILanguageStatusService, LanguageStatusServiceImpl, true); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index fef188943c452..20466643ba7d2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3158,4 +3158,28 @@ declare module 'vscode' { } //#endregion + //#region https://github.com/microsoft/vscode/issues/129037 + + enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2 + } + + class LanguageStatus { + text: string; + detail: string | MarkdownString; + severity: LanguageStatusSeverity; + constructor(text: string); + } + + export interface LanguageStatusProvider { + provideLanguageStatus(token: CancellationToken): ProviderResult; + } + + namespace languages { + export function registerLanguageStatusProvider(selector: DocumentSelector, provider: LanguageStatusProvider): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 45ee8e795fb7e..c706f83678299 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -23,6 +23,7 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { mixin } from 'vs/base/common/objects'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; +import { ILanguageStatus, ILanguageStatusService } from 'vs/editor/common/services/languageStatusService'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -34,6 +35,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha constructor( extHostContext: IExtHostContext, @IModeService modeService: IModeService, + @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures); this._modeService = modeService; @@ -157,6 +159,16 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha //#endregion + // --- language status + + $registerLanguageStatusProvider(handle: number, selector: IDocumentFilterDto[]): void { + this._registrations.set(handle, this._languageStatusService.registerLanguageStatusProvider(selector, { + provideLanguageStatus: (_langId: string, token: CancellationToken): Promise => { + return this._proxy.$provideLanguageStatus(handle, token); + } + })); + } + // --- outline $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8bf20cf4917f3..75eafe516dd0b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -506,6 +506,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerTypeHierarchyProvider(selector: vscode.DocumentSelector, provider: vscode.TypeHierarchyProvider): vscode.Disposable { checkProposedApiEnabled(extension); return extHostLanguageFeatures.registerTypeHierarchyProvider(extension, selector, provider); + }, + registerLanguageStatusProvider(selector: vscode.DocumentSelector, provider: vscode.LanguageStatusProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerLanguageStatusProvider(extension, selector, provider); } }; @@ -1279,7 +1283,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I StatementCoverage: extHostTypes.StatementCoverage, BranchCoverage: extHostTypes.BranchCoverage, FunctionCoverage: extHostTypes.FunctionCoverage, - WorkspaceTrustState: extHostTypes.WorkspaceTrustState + WorkspaceTrustState: extHostTypes.WorkspaceTrustState, + LanguageStatus: extHostTypes.LanguageStatus, + LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f776086b153fb..feeca2223667a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -67,6 +67,7 @@ import { createExtHostContextProxyIdentifier as createExtId, createMainContextPr import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import * as search from 'vs/workbench/services/search/common/search'; import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; +import { ILanguageStatus } from 'vs/editor/common/services/languageStatusService'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -382,6 +383,7 @@ export interface IdentifiableInlineCompletion extends modes.InlineCompletion { export interface MainThreadLanguageFeaturesShape extends IDisposable { $unregister(handle: number): void; + $registerLanguageStatusProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], label: string): void; $registerCodeLensSupport(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; $emitCodeLensEvent(eventHandle: number, event?: any): void; @@ -1639,6 +1641,7 @@ export interface IInlineValueContextDto { export type ITypeHierarchyItemDto = Dto; export interface ExtHostLanguageFeaturesShape { + $provideLanguageStatus(handle: number, token: CancellationToken): Promise; $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveCodeLens(handle: number, symbol: ICodeLensDto, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b021f9c03f721..a1d9be8d8df0a 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import type * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, LanguageStatusSeverity } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -33,9 +33,37 @@ import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; +import { ILanguageStatus } from 'vs/editor/common/services/languageStatusService'; +import Severity from 'vs/base/common/severity'; // --- adapter +class LanguageStatusAdapter { + + constructor(private readonly _provider: vscode.LanguageStatusProvider) { } + + async provideLanguageStatus(token: CancellationToken): Promise { + + const value = await this._provider.provideLanguageStatus(token); + if (!value) { + return; + } + + let severity = Severity.Info; + if (value.severity === LanguageStatusSeverity.Error) { + severity = Severity.Error; + } else if (value.severity === LanguageStatusSeverity.Warning) { + severity = Severity.Warning; + } + + return { + text: value.text, + message: typeConvert.MarkdownString.from(value.detail), + severity + }; + } +} + class DocumentSymbolAdapter { private _documents: ExtHostDocuments; @@ -1492,7 +1520,7 @@ class TypeHierarchyAdapter { return map?.get(itemId); } } -type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter +type Adapter = LanguageStatusAdapter | DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter @@ -1624,6 +1652,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ext.displayName || ext.name; } + // --- language status + + registerLanguageStatusProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.LanguageStatusProvider): vscode.Disposable { + const handle = this._addNewAdapter(new LanguageStatusAdapter(provider), extension); + this._proxy.$registerLanguageStatusProvider(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideLanguageStatus(handle: number, token: CancellationToken): Promise { + return this._withAdapter(handle, LanguageStatusAdapter, adapter => adapter.provideLanguageStatus(token), undefined); + } + // --- outline registerDocumentSymbolProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, metadata?: vscode.DocumentSymbolProviderMetadata): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 54b63602bc115..97fd0e9a03409 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1280,6 +1280,24 @@ export class CallHierarchyOutgoingCall { } } +export enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2 +} + +export class LanguageStatus { + + text: string; + detail: string | MarkdownString; + severity: LanguageStatusSeverity; + + constructor(text: string) { + this.text = text; + this.detail = ''; + this.severity = LanguageStatusSeverity.Information; + } +} @es5ClassCompat export class CodeLens { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index cd87db7f04d3e..345eb49851290 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 } from 'vs/base/common/objects'; +import { deepClone, equals } 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'; @@ -51,9 +51,10 @@ 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_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +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'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -142,6 +143,7 @@ class StateChange { indentation: boolean = false; selectionStatus: boolean = false; mode: boolean = false; + languageStatus: boolean = false; encoding: boolean = false; EOL: boolean = false; tabFocusMode: boolean = false; @@ -153,6 +155,7 @@ class StateChange { this.indentation = this.indentation || other.indentation; this.selectionStatus = this.selectionStatus || other.selectionStatus; this.mode = this.mode || other.mode; + this.languageStatus = this.languageStatus || other.languageStatus; this.encoding = this.encoding || other.encoding; this.EOL = this.EOL || other.EOL; this.tabFocusMode = this.tabFocusMode || other.tabFocusMode; @@ -165,6 +168,7 @@ class StateChange { return this.indentation || this.selectionStatus || this.mode + || this.languageStatus || this.encoding || this.EOL || this.tabFocusMode @@ -177,6 +181,7 @@ 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; } @@ -194,6 +199,9 @@ 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; } @@ -239,6 +247,13 @@ 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; @@ -302,6 +317,7 @@ 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)); @@ -313,6 +329,7 @@ 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, @@ -541,6 +558,32 @@ 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; + if (first.severity === Severity.Error) { + backgroundColor = { id: 'statusBarItem.errorBackground' }; + } else if (first.severity === Severity.Warning) { + backgroundColor = { id: 'statusBarItem.warningBackground' }; + } + + const props: IStatusbarEntry = { + name: localize('status.editor.status', "Language Status"), + text: first.text, + ariaLabel: first.text, + backgroundColor, + tooltip: first.message, + }; + + this.updateElement(this.statusElement, props, 'status.editor.status', StatusbarAlignment.RIGHT, 100.05); + } + private updateMetadataElement(text: string | undefined): void { if (!text) { this.metadataElement.clear(); @@ -597,6 +640,7 @@ 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); } @@ -634,6 +678,7 @@ 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); @@ -667,6 +712,10 @@ 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); @@ -733,6 +782,14 @@ 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 };