Skip to content

Commit

Permalink
Add client support for inlay hints
Browse files Browse the repository at this point in the history
  • Loading branch information
HighCommander4 committed May 3, 2021
1 parent 6ccdd94 commit 4f04c20
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 9 deletions.
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,26 @@
"when": "clangd.ast.hasData"
}
]
}
},
"colors": [
{
"id": "clangd.inlayHints.foreground",
"description": "Foreground color of inlay hints",
"defaults": {
"dark": "#A0A0A0F0",
"light": "#747474",
"highContrast": "#BEBEBE"
}
},
{
"id": "clangd.inlayHints.background",
"description": "Background color of inlay hints",
"defaults": {
"dark": "#11223300",
"light": "#11223300",
"highContrast": "#11223300"
}
}
]
}
}
29 changes: 21 additions & 8 deletions src/clangd-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@ import * as ast from './ast';
import * as config from './config';
import * as configFileWatcher from './config-file-watcher';
import * as fileStatus from './file-status';
import * as inlayHints from './inlay-hints';
import * as install from './install';
import * as memoryUsage from './memory-usage';
import * as openConfig from './open-config';
import * as semanticHighlighting from './semantic-highlighting';
import * as switchSourceHeader from './switch-source-header';
import * as typeHierarchy from './type-hierarchy';

const clangdDocumentSelector = [
{scheme: 'file', language: 'c'},
{scheme: 'file', language: 'cpp'},
// CUDA is not supported by vscode, but our extension does supports it.
{scheme: 'file', language: 'cuda'},
{scheme: 'file', language: 'objective-c'},
{scheme: 'file', language: 'objective-cpp'},
];

export function isClangdDocument(document: vscode.TextDocument) {
return vscode.languages.match(clangdDocumentSelector, document);
}

class ClangdLanguageClient extends vscodelc.LanguageClient {
// Override the default implementation for failed requests. The default
// behavior is just to log failures in the output panel, however output panel
Expand Down Expand Up @@ -68,14 +82,7 @@ export class ClangdContext implements vscode.Disposable {

const clientOptions: vscodelc.LanguageClientOptions = {
// Register the server for c-family and cuda files.
documentSelector: [
{scheme: 'file', language: 'c'},
{scheme: 'file', language: 'cpp'},
// CUDA is not supported by vscode, but our extension does supports it.
{scheme: 'file', language: 'cuda'},
{scheme: 'file', language: 'objective-c'},
{scheme: 'file', language: 'objective-cpp'},
],
documentSelector: clangdDocumentSelector,
initializationOptions: {
clangdFileStatus: true,
fallbackFlags: config.get<string[]>('fallbackFlags')
Expand Down Expand Up @@ -149,6 +156,7 @@ export class ClangdContext implements vscode.Disposable {
semanticHighlighting.activate(this);
this.client.registerFeature(new EnableEditsNearCursorFeature);
typeHierarchy.activate(this);
inlayHints.activate(this);
memoryUsage.activate(this);
ast.activate(this);
openConfig.activate(this);
Expand All @@ -159,6 +167,11 @@ export class ClangdContext implements vscode.Disposable {
configFileWatcher.activate(this);
}

get visibleClangdEditors(): vscode.TextEditor[] {
return vscode.window.visibleTextEditors.filter(
(e) => isClangdDocument(e.document));
}

dispose() {
this.subscriptions.forEach((d) => { d.dispose(); });
this.subscriptions = []
Expand Down
233 changes: 233 additions & 0 deletions src/inlay-hints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// This file implements the client side of the proposed inlay hints
// extension to LSP. The proposal is based on the one at
// https://github.com/microsoft/language-server-protocol/issues/956,
// with some modifications that reflect discussions in that issue.
// The feature allows the server to provide the client with inline
// annotations to display for e.g. parameter names at call sites.
// The client-side implementation is adapted from rust-analyzer's.

import * as vscode from 'vscode';
import * as vscodelc from 'vscode-languageclient/node';

import {ClangdContext, isClangdDocument} from './clangd-context';

export function activate(context: ClangdContext) {
const feature = new InlayHintsFeature(context);
context.client.registerFeature(feature);
}

// Currently, only one hint kind (parameter hints) are supported,
// but others (e.g. type hints) may be added in the future.
enum InlayHintKind {
Parameter = 'parameter'
}

interface InlayHint {
range: vscodelc.Range;
kind: InlayHintKind | string;
label: string;
}

interface InlayHintsParams {
textDocument: vscodelc.TextDocumentIdentifier;
}

namespace InlayHintsRequest {
export const type =
new vscodelc.RequestType<InlayHintsParams, InlayHint[], void>(
'clangd/inlayHints');
}

interface InlayDecorations {
// Hints are grouped based on their InlayHintKind, because different kinds
// require different decoration types.
// A future iteration of the API may have free-form hint kinds, and instead
// specify style-related information (e.g. before vs. after) explicitly.
// With such an API, we could group hints based on unique presentation styles
// instead.
parameterHints: vscode.DecorationOptions[];
}

interface HintStyle {
decorationType: vscode.TextEditorDecorationType;

toDecoration(hint: InlayHint,
conv: vscodelc.Protocol2CodeConverter): vscode.DecorationOptions;
}

const parameterHintStyle = createHintStyle('before');

function createHintStyle(position: 'before'|'after'): HintStyle {
const fg = new vscode.ThemeColor('clangd.inlayHints.foreground');
const bg = new vscode.ThemeColor('clangd.inlayHints.background');
return {
decorationType: vscode.window.createTextEditorDecorationType({
[position]: {
color: fg,
backgroundColor: bg,
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: ';font-size:smaller'
}
}),
toDecoration(hint: InlayHint, conv: vscodelc.Protocol2CodeConverter):
vscode.DecorationOptions {
return {
range: conv.asRange(hint.range),
renderOptions: {[position]: {contentText: hint.label}}
};
}
};
}

interface FileEntry {
document: vscode.TextDocument;

// Last applied decorations.
cachedDecorations: InlayDecorations|null;

// Source of the token to cancel in-flight inlay hints request if any.
inlaysRequest: vscode.CancellationTokenSource|null;
}

class InlayHintsFeature implements vscodelc.StaticFeature {
private enabled = false;
private sourceFiles = new Map<string, FileEntry>(); // keys are URIs

constructor(private readonly context: ClangdContext) {
vscode.window.onDidChangeVisibleTextEditors(
this.onDidChangeVisibleTextEditors, this, context.subscriptions);
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this,
context.subscriptions);
}

fillClientCapabilities(_capabilities: vscodelc.ClientCapabilities) {}
fillInitializeParams(_params: vscodelc.InitializeParams) {}

initialize(capabilities: vscodelc.ServerCapabilities,
_documentSelector: vscodelc.DocumentSelector|undefined) {
const serverCapabilities: vscodelc.ServerCapabilities&
{clangdInlayHintsProvider?: boolean} = capabilities;
if (serverCapabilities.clangdInlayHintsProvider) {
this.enabled = true;
this.startShowingHints();
}
}

onDidChangeVisibleTextEditors() {
if (!this.enabled)
return;

const newSourceFiles = new Map<string, FileEntry>();

// Rerender all, even up-to-date editors for simplicity
this.context.visibleClangdEditors.forEach(async editor => {
const uri = editor.document.uri.toString();
const file = this.sourceFiles.get(uri) ?? {
document: editor.document,
cachedDecorations: null,
inlaysRequest: null
};
newSourceFiles.set(uri, file);

// No text documents changed, so we may try to use the cache
if (!file.cachedDecorations) {
const hints = await this.fetchHints(file);
if (!hints)
return;

file.cachedDecorations = this.hintsToDecorations(hints);
}

this.renderDecorations(editor, file.cachedDecorations);
});

// Cancel requests for no longer visible (disposed) source files
this.sourceFiles.forEach((file, uri) => {
if (!newSourceFiles.has(uri)) {
file.inlaysRequest?.cancel();
}
});

this.sourceFiles = newSourceFiles;
}

onDidChangeTextDocument({contentChanges,
document}: vscode.TextDocumentChangeEvent) {
if (!this.enabled || contentChanges.length === 0 ||
!isClangdDocument(document))
return;
this.syncCacheAndRenderHints();
}

dispose() { this.stopShowingHints(); }

private startShowingHints() {
// Set up initial cache shape
this.context.visibleClangdEditors.forEach(
editor => this.sourceFiles.set(editor.document.uri.toString(), {
document: editor.document,
inlaysRequest: null,
cachedDecorations: null
}));

this.syncCacheAndRenderHints();
}

private stopShowingHints() {
this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
this.context.visibleClangdEditors.forEach(
editor =>
this.renderDecorations(editor, {parameterHints: []}));
}

private renderDecorations(editor: vscode.TextEditor,
decorations: InlayDecorations) {
editor.setDecorations(parameterHintStyle.decorationType,
decorations.parameterHints);
}

private syncCacheAndRenderHints() {
this.sourceFiles.forEach(
(file, uri) => this.fetchHints(file).then(hints => {
if (!hints)
return;

file.cachedDecorations = this.hintsToDecorations(hints);

for (const editor of this.context.visibleClangdEditors) {
if (editor.document.uri.toString() == uri) {
this.renderDecorations(editor, file.cachedDecorations);
}
}
}));
}

private hintsToDecorations(hints: InlayHint[]): InlayDecorations {
const decorations: InlayDecorations = {parameterHints: []};
const conv = this.context.client.protocol2CodeConverter;
for (const hint of hints) {
switch (hint.kind) {
case InlayHintKind.Parameter: {
decorations.parameterHints.push(parameterHintStyle.toDecoration(hint, conv));
continue;
}
// Don't handle unknown hint kinds because we don't know how to style
// them. This may change in a future version of the protocol.
}
}
return decorations;
}

private async fetchHints(file: FileEntry): Promise<InlayHint[]|null> {
file.inlaysRequest?.cancel();

const tokenSource = new vscode.CancellationTokenSource();
file.inlaysRequest = tokenSource;

const request = {textDocument: {uri: file.document.uri.toString()}};

return this.context.client.sendRequest(InlayHintsRequest.type, request,
tokenSource.token);
}
}

0 comments on commit 4f04c20

Please sign in to comment.