first cut for language status API proposal, https://github.com/microsoft/vscode/issues/129037

This commit is contained in:
Johannes Rieken 2021-07-21 10:22:36 +02:00
parent 5fb3fbf755
commit 4508c29d35
No known key found for this signature in database
GPG key ID: 96634B5AF12F8798
8 changed files with 240 additions and 5 deletions

View file

@ -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<ILanguageStatus | undefined>
}
export const ILanguageStatusService = createDecorator<ILanguageStatusService>('ILanguageStatusService');
export interface ILanguageStatusService {
_serviceBrand: undefined;
onDidChange: Event<void>;
registerLanguageStatusProvider(selector: LanguageSelector, provider: ILanguageStatusProvider): IDisposable;
getLanguageStatus(model: ITextModel): Promise<ILanguageStatus[]>;
}
class LanguageStatusServiceImpl implements ILanguageStatusService {
declare _serviceBrand: undefined;
private readonly _provider = new LanguageFeatureRegistry<ILanguageStatusProvider>();
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = 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<ILanguageStatus[]> {
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);

View file

@ -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<LanguageStatus>;
}
namespace languages {
export function registerLanguageStatusProvider(selector: DocumentSelector, provider: LanguageStatusProvider): Disposable;
}
//#endregion
}

View file

@ -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<ILanguageStatus | undefined> => {
return this._proxy.$provideLanguageStatus(handle, token);
}
}));
}
// --- outline
$registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void {

View file

@ -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,
};
};
}

View file

@ -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<TypeHierarchyItem>;
export interface ExtHostLanguageFeaturesShape {
$provideLanguageStatus(handle: number, token: CancellationToken): Promise<ILanguageStatus | undefined>;
$provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise<modes.DocumentSymbol[] | undefined>;
$provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise<ICodeLensListDto | undefined>;
$resolveCodeLens(handle: number, symbol: ICodeLensDto, token: CancellationToken): Promise<ICodeLensDto | undefined>;

View file

@ -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<ILanguageStatus | undefined> {
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<ILanguageStatus | undefined> {
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 {

View file

@ -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 {

View file

@ -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<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));
@ -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<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 };