Skip to content

Commit

Permalink
vscode: add InlayHints support
Browse files Browse the repository at this point in the history
Signed-off-by: vince-fugnitto <vincent.fugnitto@ericsson.com>
  • Loading branch information
vince-fugnitto committed Oct 17, 2022
1 parent a6c02c4 commit 84d088c
Show file tree
Hide file tree
Showing 11 changed files with 600 additions and 39 deletions.
51 changes: 51 additions & 0 deletions packages/plugin-ext/src/common/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/common/cache.ts
export class Cache<T> {

private static readonly enableDebugLogging = false;

private readonly _data = new Map<number, readonly T[]>();
private _idPool = 1;

constructor(
private readonly id: string
) { }

add(item: readonly T[]): number {
const id = this._idPool++;
this._data.set(id, item);
this.logDebugInfo();
return id;
}

get(pid: number, id: number): T | undefined {
return this._data.has(pid) ? this._data.get(pid)![id] : undefined;
}

delete(id: number): void {
this._data.delete(id);
this.logDebugInfo();
}

private logDebugInfo(): void {
if (!Cache.enableDebugLogging) {
return;
}
console.log(`${this.id} cache size — ${this._data.size}`);
}
}
32 changes: 32 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ export interface ReferenceContext {
export type CacheId = number;
export type ChainedCacheId = [CacheId, CacheId];

export type CachedSessionItem<T> = T & { cacheId?: ChainedCacheId };
export type CachedSession<T> = T & { cacheId?: CacheId };

export interface DocumentLink {
cacheId?: ChainedCacheId,
range: Range;
Expand Down Expand Up @@ -727,3 +730,32 @@ export interface CommentInfo {
export interface ProvidedTerminalLink extends theia.TerminalLink {
providerId: string
}

export interface InlayHintLabelPart {
label: string;
tooltip?: string | MarkdownStringDTO;
location?: Location;
command?: Command;
}

export interface InlayHint {
position: { lineNumber: number, column: number };
label: string | InlayHintLabelPart[];
tooltip?: string | MarkdownStringDTO | undefined;
kind?: InlayHintKind;
textEdits?: TextEdit[];
paddingLeft?: boolean;
paddingRight?: boolean;
}

export enum InlayHintKind {
Type = 1,
Parameter = 2,
}

export interface InlayHintsProvider {
onDidChangeInlayHints?: TheiaEvent<void> | undefined;
provideInlayHints(model: monaco.editor.ITextModel, range: Range, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable<InlayHint[] | undefined>;
resolveInlayHint?(hint: InlayHint, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable<InlayHint[] | undefined>;
}

13 changes: 12 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ import {
CommentThreadChangedEvent,
CodeActionProviderDocumentation,
LinkedEditingRanges,
ProvidedTerminalLink
ProvidedTerminalLink,
InlayHint,
CachedSession,
CachedSessionItem
} from './plugin-api-rpc-model';
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { KeysToAnyValues, KeysToKeysToAnyValue } from './types';
Expand Down Expand Up @@ -1550,6 +1553,9 @@ export interface LanguagesExt {
$provideSelectionRanges(handle: number, resource: UriComponents, positions: Position[], token: CancellationToken): PromiseLike<SelectionRange[][]>;
$provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): PromiseLike<RawColorInfo[]>;
$provideColorPresentations(handle: number, resource: UriComponents, colorInfo: RawColorInfo, token: CancellationToken): PromiseLike<ColorPresentation[]>;
$provideInlayHints(handle: number, resource: UriComponents, range: Range, token: CancellationToken): Promise<InlayHintsDto | undefined>;
$resolveInlayHint(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<InlayHintDto | undefined>;
$releaseInlayHints(handle: number, id: number): void;
$provideRenameEdits(handle: number, resource: UriComponents, position: Position, newName: string, token: CancellationToken): PromiseLike<WorkspaceEditDto | undefined>;
$resolveRenameLocation(handle: number, resource: UriComponents, position: Position, token: CancellationToken): PromiseLike<RenameLocation | undefined>;
$provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise<BinaryBuffer | null>;
Expand Down Expand Up @@ -1605,6 +1611,8 @@ export interface LanguagesMain {
$emitFoldingRangeEvent(handle: number, event?: any): void;
$registerSelectionRangeProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void;
$registerDocumentColorProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void;
$registerInlayHintsProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], displayName?: string, eventHandle?: number): void;
$emitInlayHintsEvent(eventHandle: number, event?: any): void;
$registerRenameProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], supportsResolveInitialValues: boolean): void;
$registerDocumentSemanticTokensProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[],
legend: theia.SemanticTokensLegend, eventHandle: number | undefined): void;
Expand Down Expand Up @@ -2018,3 +2026,6 @@ export interface SecretsMain {
$setPassword(extensionId: string, key: string, value: string): Promise<void>;
$deletePassword(extensionId: string, key: string): Promise<void>;
}

export type InlayHintDto = CachedSessionItem<InlayHint>;
export type InlayHintsDto = CachedSession<{ hints: InlayHint[] }>;
87 changes: 85 additions & 2 deletions packages/plugin-ext/src/main/browser/languages-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ import {
WorkspaceEditDto,
WorkspaceTextEditDto,
PluginInfo,
LanguageStatus as LanguageStatusDTO
LanguageStatus as LanguageStatusDTO,
InlayHintDto
} from '../../common/plugin-api-rpc';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
SerializedDocumentFilter, MarkerData, Range, RelatedInformation,
MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction, CompletionDto, CodeActionProviderDocumentation
MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction, CompletionDto, CodeActionProviderDocumentation, InlayHint, InlayHintLabelPart
} from '../../common/plugin-api-rpc-model';
import { RPCProtocol } from '../../common/rpc-protocol';
import { MonacoLanguages, WorkspaceSymbolProvider } from '@theia/monaco/lib/browser/monaco-languages';
Expand Down Expand Up @@ -806,6 +807,63 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable {
}, token);
}

$registerInlayHintsProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], displayName?: string, eventHandle?: number): void {
const languageSelector = this.toLanguageSelector(selector);
const inlayHintsProvider = this.createInlayHintsProvider(handle);
if (typeof eventHandle === 'number') {
const emitter = new Emitter<void>();
this.register(eventHandle, emitter);
inlayHintsProvider.onDidChangeInlayHints = emitter.event;
}
this.register(handle, (monaco.languages.registerInlayHintsProvider as RegistrationFunction<monaco.languages.InlayHintsProvider>)(languageSelector, inlayHintsProvider));

}

createInlayHintsProvider(handle: number): monaco.languages.InlayHintsProvider {
return {
provideInlayHints: async (model: monaco.editor.ITextModel, range: Range, token: monaco.CancellationToken): Promise<monaco.languages.InlayHintList | undefined> => {
const result = await this.proxy.$provideInlayHints(handle, model.uri, range, token);
if (!result) {
return;
}
return {
hints: result.hints.map(hint => reviveHint(hint)),
dispose: () => {
if (typeof result.cacheId === 'number') {
this.proxy.$releaseInlayHints(handle, result.cacheId);
}
}
};
},
resolveInlayHint: async (hint, token): Promise<monaco.languages.InlayHint | undefined> => {
const dto: InlayHintDto = hint;
if (typeof dto.cacheId !== 'number') {
return hint;
}
const result = await this.proxy.$resolveInlayHint(handle, dto.cacheId, token);
if (token.isCancellationRequested) {
return undefined;
}
if (!result) {
return hint;
}
return {
...hint,
tooltip: result.tooltip,
label: reviveInlayLabel(result.label)
};
},
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
$emitInlayHintsEvent(eventHandle: number, event?: any): void {
const obj = this.services.get(eventHandle);
if (obj instanceof Emitter) {
obj.fire(event);
}
}

$registerQuickFixProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], providedCodeActionKinds?: string[],
documentation?: CodeActionProviderDocumentation): void {

Expand Down Expand Up @@ -1165,6 +1223,31 @@ function reviveOnEnterRules(onEnterRules?: SerializedOnEnterRule[]): monaco.lang
return onEnterRules.map(reviveOnEnterRule);
}

function reviveInlayLabel(label: string | InlayHintLabelPart[]): string | monaco.languages.InlayHintLabelPart[] {
let monacoLabel: string | monaco.languages.InlayHintLabelPart[];
if (typeof label === 'string') {
monacoLabel = label;
} else {
const parts: monaco.languages.InlayHintLabelPart[] = [];
for (const part of label) {
const result: monaco.languages.InlayHintLabelPart = {
...part,
location: !!part.location ? { range: part.location?.range, uri: monaco.Uri.revive(part.location.uri) } : undefined
};
parts.push(result);
}
monacoLabel = parts;
}
return monacoLabel;
}

function reviveHint(hint: InlayHint): monaco.languages.InlayHint {
return {
...hint,
label: reviveInlayLabel(hint.label)
};
}

function toMonacoAction(action: CodeAction): monaco.languages.CodeAction {
return {
...action,
Expand Down
34 changes: 1 addition & 33 deletions packages/plugin-ext/src/plugin/custom-editors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { WebviewImpl, WebviewsExtImpl } from './webviews';
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { WorkspaceExtImpl } from './workspace';
import { Cache } from '../common/cache';

export class CustomEditorsExtImpl implements CustomEditorsExt {
private readonly proxy: CustomEditorsMain;
Expand Down Expand Up @@ -333,36 +334,3 @@ class CustomDocumentStore {
}
}

// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/common/cache.ts
class Cache<T> {
private static readonly enableDebugLogging = false;
private readonly _data = new Map<number, readonly T[]>();
private _idPool = 1;

constructor(
private readonly id: string
) { }

add(item: readonly T[]): number {
const id = this._idPool++;
this._data.set(id, item);
this.logDebugInfo();
return id;
}

get(pid: number, id: number): T | undefined {
return this._data.has(pid) ? this._data.get(pid)![id] : undefined;
}

delete(id: number): void {
this._data.delete(id);
this.logDebugInfo();
}

private logDebugInfo(): void {
if (!Cache.enableDebugLogging) {
return;
}
console.log(`${this.id} cache size — ${this._data.size}`);
}
}
33 changes: 33 additions & 0 deletions packages/plugin-ext/src/plugin/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
WorkspaceEditDto,
PluginInfo,
Plugin,
InlayHintsDto,
InlayHintDto,
} from '../common/plugin-api-rpc';
import { RPCProtocol } from '../common/rpc-protocol';
import * as theia from '@theia/plugin';
Expand Down Expand Up @@ -101,6 +103,7 @@ import { DisposableCollection, disposableTimeout, Disposable as TheiaDisposable
import { Severity } from '@theia/core/lib/common/severity';
import { LinkedEditingRangeAdapter } from './languages/linked-editing-range';
import { serializeEnterRules, serializeIndentation, serializeRegExp } from './languages-utils';
import { InlayHintsAdapter } from './languages/inlay-hints';

type Adapter = CompletionAdapter |
SignatureHelpAdapter |
Expand All @@ -124,6 +127,7 @@ type Adapter = CompletionAdapter |
FoldingProviderAdapter |
SelectionRangeProviderAdapter |
ColorProviderAdapter |
InlayHintsAdapter |
RenameAdapter |
CallHierarchyAdapter |
DocumentRangeSemanticTokensAdapter |
Expand Down Expand Up @@ -602,6 +606,35 @@ export class LanguagesExtImpl implements LanguagesExt {
}
// ### Color Provider end

// ### InlayHints Provider begin
registerInlayHintsProvider(selector: theia.DocumentSelector, provider: theia.InlayHintsProvider, pluginInfo: PluginInfo): theia.Disposable {
const eventHandle = typeof provider.onDidChangeInlayHints === 'function' ? this.nextCallId() : undefined;
const callId = this.addNewAdapter(new InlayHintsAdapter(provider, this.documents, this.commands));
this.proxy.$registerInlayHintsProvider(callId, pluginInfo, this.transformDocumentSelector(selector));

let result = this.createDisposable(callId);

if (eventHandle !== undefined) {
const subscription = provider.onDidChangeInlayHints!(() => this.proxy.$emitInlayHintsEvent(eventHandle));
result = Disposable.from(result, subscription);
}

return result;
}

$provideInlayHints(handle: number, resource: UriComponents, range: Range, token: theia.CancellationToken): Promise<InlayHintsDto | undefined> {
return this.withAdapter(handle, InlayHintsAdapter, adapter => adapter.provideInlayHints(URI.revive(resource), range, token), undefined);
}

$resolveInlayHint(handle: number, id: ChainedCacheId, token: theia.CancellationToken): Promise<InlayHintDto | undefined> {
return this.withAdapter(handle, InlayHintsAdapter, adapter => adapter.resolveInlayHint(id, token), undefined);
}

$releaseInlayHints(handle: number, id: number): void {
this.withAdapter(handle, InlayHintsAdapter, async adapter => adapter.releaseHints(id), undefined);
}
// ### InlayHints Provider end

// ### Folding Range Provider begin
registerFoldingRangeProvider(selector: theia.DocumentSelector, provider: theia.FoldingRangeProvider, pluginInfo: PluginInfo): theia.Disposable {
const callId = this.addNewAdapter(new FoldingProviderAdapter(provider, this.documents));
Expand Down
Loading

0 comments on commit 84d088c

Please sign in to comment.