Skip to content

Commit cbc9c4e

Browse files
[clangd] Add support for inline parameter hints
Differential Revision: https://reviews.llvm.org/D98748
1 parent 02265ed commit cbc9c4e

13 files changed

+669
-1
lines changed

clang-tools-extra/clangd/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ add_clang_library(clangDaemon
7676
HeuristicResolver.cpp
7777
Hover.cpp
7878
IncludeFixer.cpp
79+
InlayHints.cpp
7980
JSONTransport.cpp
8081
PathMapping.cpp
8182
Protocol.cpp

clang-tools-extra/clangd/ClangdLSPServer.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
571571
{"referencesProvider", true},
572572
{"astProvider", true}, // clangd extension
573573
{"typeHierarchyProvider", true},
574+
{"clangdInlayHintsProvider", true},
574575
{"memoryUsageProvider", true}, // clangd extension
575576
{"compilationDatabase", // clangd extension
576577
llvm::json::Object{{"automaticReload", true}}},
@@ -1208,6 +1209,11 @@ void ClangdLSPServer::onCallHierarchyOutgoingCalls(
12081209
Reply(std::vector<CallHierarchyOutgoingCall>{});
12091210
}
12101211

1212+
void ClangdLSPServer::onInlayHints(const InlayHintsParams &Params,
1213+
Callback<std::vector<InlayHint>> Reply) {
1214+
Server->inlayHints(Params.textDocument.uri.file(), std::move(Reply));
1215+
}
1216+
12111217
void ClangdLSPServer::applyConfiguration(
12121218
const ConfigurationSettings &Settings) {
12131219
// Per-file update to the compilation database.
@@ -1471,6 +1477,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
14711477
Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink);
14721478
Bind.method("textDocument/semanticTokens/full", this, &ClangdLSPServer::onSemanticTokens);
14731479
Bind.method("textDocument/semanticTokens/full/delta", this, &ClangdLSPServer::onSemanticTokensDelta);
1480+
Bind.method("clangd/inlayHints", this, &ClangdLSPServer::onInlayHints);
14741481
Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage);
14751482
if (Opts.FoldingRanges)
14761483
Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange);

clang-tools-extra/clangd/ClangdLSPServer.h

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
145145
void onCallHierarchyOutgoingCalls(
146146
const CallHierarchyOutgoingCallsParams &,
147147
Callback<std::vector<CallHierarchyOutgoingCall>>);
148+
void onInlayHints(const InlayHintsParams &, Callback<std::vector<InlayHint>>);
148149
void onChangeConfiguration(const DidChangeConfigurationParams &);
149150
void onSymbolInfo(const TextDocumentPositionParams &,
150151
Callback<std::vector<SymbolDetails>>);

clang-tools-extra/clangd/ClangdServer.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "Format.h"
1616
#include "HeaderSourceSwitch.h"
1717
#include "Headers.h"
18+
#include "InlayHints.h"
1819
#include "ParsedAST.h"
1920
#include "Preamble.h"
2021
#include "Protocol.h"
@@ -751,6 +752,17 @@ void ClangdServer::incomingCalls(
751752
});
752753
}
753754

755+
void ClangdServer::inlayHints(PathRef File,
756+
Callback<std::vector<InlayHint>> CB) {
757+
auto Action = [File = File.str(),
758+
CB = std::move(CB)](Expected<InputsAndAST> InpAST) mutable {
759+
if (!InpAST)
760+
return CB(InpAST.takeError());
761+
CB(clangd::inlayHints(InpAST->AST));
762+
};
763+
WorkScheduler->runWithAST("InlayHints", File, std::move(Action));
764+
}
765+
754766
void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
755767
// FIXME: Do nothing for now. This will be used for indexing and potentially
756768
// invalidating other caches.

clang-tools-extra/clangd/ClangdServer.h

+3
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ class ClangdServer {
262262
void incomingCalls(const CallHierarchyItem &Item,
263263
Callback<std::vector<CallHierarchyIncomingCall>>);
264264

265+
/// Resolve inlay hints for a given document.
266+
void inlayHints(PathRef File, Callback<std::vector<InlayHint>>);
267+
265268
/// Retrieve the top symbols from the workspace matching a query.
266269
void workspaceSymbols(StringRef Query, int Limit,
267270
Callback<std::vector<SymbolInformation>> CB);
+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//===--- InlayHints.cpp ------------------------------------------*- C++-*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
#include "InlayHints.h"
9+
#include "ParsedAST.h"
10+
#include "support/Logger.h"
11+
#include "clang/AST/DeclarationName.h"
12+
#include "clang/AST/ExprCXX.h"
13+
#include "clang/AST/RecursiveASTVisitor.h"
14+
#include "clang/Basic/SourceManager.h"
15+
16+
namespace clang {
17+
namespace clangd {
18+
19+
class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
20+
public:
21+
InlayHintVisitor(std::vector<InlayHint> &Results, ParsedAST &AST)
22+
: Results(Results), AST(AST.getASTContext()) {}
23+
24+
bool VisitCXXConstructExpr(CXXConstructExpr *E) {
25+
// Weed out constructor calls that don't look like a function call with
26+
// an argument list, by checking the validity of getParenOrBraceRange().
27+
// Also weed out std::initializer_list constructors as there are no names
28+
// for the individual arguments.
29+
if (!E->getParenOrBraceRange().isValid() ||
30+
E->isStdInitListInitialization()) {
31+
return true;
32+
}
33+
34+
processCall(E->getParenOrBraceRange().getBegin(), E->getConstructor(),
35+
{E->getArgs(), E->getNumArgs()});
36+
return true;
37+
}
38+
39+
bool VisitCallExpr(CallExpr *E) {
40+
// Do not show parameter hints for operator calls written using operator
41+
// syntax or user-defined literals. (Among other reasons, the resulting
42+
// hints can look awkard, e.g. the expression can itself be a function
43+
// argument and then we'd get two hints side by side).
44+
if (isa<CXXOperatorCallExpr>(E) || isa<UserDefinedLiteral>(E))
45+
return true;
46+
47+
processCall(E->getRParenLoc(),
48+
dyn_cast_or_null<FunctionDecl>(E->getCalleeDecl()),
49+
{E->getArgs(), E->getNumArgs()});
50+
return true;
51+
}
52+
53+
// FIXME: Handle RecoveryExpr to try to hint some invalid calls.
54+
55+
private:
56+
// The purpose of Anchor is to deal with macros. It should be the call's
57+
// opening or closing parenthesis or brace. (Always using the opening would
58+
// make more sense but CallExpr only exposes the closing.) We heuristically
59+
// assume that if this location does not come from a macro definition, then
60+
// the entire argument list likely appears in the main file and can be hinted.
61+
void processCall(SourceLocation Anchor, const FunctionDecl *Callee,
62+
llvm::ArrayRef<const Expr *const> Args) {
63+
if (Args.size() == 0 || !Callee)
64+
return;
65+
66+
// If the anchor location comes from a macro defintion, there's nowhere to
67+
// put hints.
68+
if (!AST.getSourceManager().getTopMacroCallerLoc(Anchor).isFileID())
69+
return;
70+
71+
// The parameter name of a move or copy constructor is not very interesting.
72+
if (auto *Ctor = dyn_cast<CXXConstructorDecl>(Callee))
73+
if (Ctor->isCopyOrMoveConstructor())
74+
return;
75+
76+
// FIXME: Exclude setters (i.e. functions with one argument whose name
77+
// begins with "set"), as their parameter name is also not likely to be
78+
// interesting.
79+
80+
// Don't show hints for variadic parameters.
81+
size_t FixedParamCount = getFixedParamCount(Callee);
82+
size_t ArgCount = std::min(FixedParamCount, Args.size());
83+
84+
NameVec ParameterNames = chooseParameterNames(Callee, ArgCount);
85+
86+
for (size_t I = 0; I < ArgCount; ++I) {
87+
StringRef Name = ParameterNames[I];
88+
if (!shouldHint(Args[I], Name))
89+
continue;
90+
91+
addInlayHint(Args[I]->getSourceRange(), InlayHintKind::ParameterHint,
92+
Name.str() + ": ");
93+
}
94+
}
95+
96+
bool shouldHint(const Expr *Arg, StringRef ParamName) {
97+
if (ParamName.empty())
98+
return false;
99+
100+
// If the argument expression is a single name and it matches the
101+
// parameter name exactly, omit the hint.
102+
if (ParamName == getSpelledIdentifier(Arg))
103+
return false;
104+
105+
// FIXME: Exclude argument expressions preceded by a /*paramName=*/ comment.
106+
107+
return true;
108+
}
109+
110+
// If "E" spells a single unqualified identifier, return that name.
111+
// Otherwise, return an empty string.
112+
static StringRef getSpelledIdentifier(const Expr *E) {
113+
E = E->IgnoreUnlessSpelledInSource();
114+
115+
if (auto *DRE = dyn_cast<DeclRefExpr>(E))
116+
if (!DRE->getQualifier())
117+
return getSimpleName(*DRE->getDecl());
118+
119+
if (auto *ME = dyn_cast<MemberExpr>(E))
120+
if (!ME->getQualifier() && ME->isImplicitAccess())
121+
return getSimpleName(*ME->getMemberDecl());
122+
123+
return {};
124+
}
125+
126+
using NameVec = SmallVector<StringRef, 8>;
127+
128+
NameVec chooseParameterNames(const FunctionDecl *Callee, size_t ArgCount) {
129+
// The current strategy here is to use all the parameter names from the
130+
// canonical declaration, unless they're all empty, in which case we
131+
// use all the parameter names from the definition (in present in the
132+
// translation unit).
133+
// We could try a bit harder, e.g.:
134+
// - try all re-declarations, not just canonical + definition
135+
// - fall back arg-by-arg rather than wholesale
136+
137+
NameVec ParameterNames = getParameterNamesForDecl(Callee, ArgCount);
138+
139+
if (llvm::all_of(ParameterNames, std::mem_fn(&StringRef::empty))) {
140+
if (const FunctionDecl *Def = Callee->getDefinition()) {
141+
ParameterNames = getParameterNamesForDecl(Def, ArgCount);
142+
}
143+
}
144+
assert(ParameterNames.size() == ArgCount);
145+
146+
// Standard library functions often have parameter names that start
147+
// with underscores, which makes the hints noisy, so strip them out.
148+
for (auto &Name : ParameterNames)
149+
stripLeadingUnderscores(Name);
150+
151+
return ParameterNames;
152+
}
153+
154+
static void stripLeadingUnderscores(StringRef &Name) {
155+
Name = Name.ltrim('_');
156+
}
157+
158+
// Return the number of fixed parameters Function has, that is, not counting
159+
// parameters that are variadic (instantiated from a parameter pack) or
160+
// C-style varargs.
161+
static size_t getFixedParamCount(const FunctionDecl *Function) {
162+
if (FunctionTemplateDecl *Template = Function->getPrimaryTemplate()) {
163+
FunctionDecl *F = Template->getTemplatedDecl();
164+
size_t Result = 0;
165+
for (ParmVarDecl *Parm : F->parameters()) {
166+
if (Parm->isParameterPack()) {
167+
break;
168+
}
169+
++Result;
170+
}
171+
return Result;
172+
}
173+
// C-style varargs don't need special handling, they're already
174+
// not included in getNumParams().
175+
return Function->getNumParams();
176+
}
177+
178+
static StringRef getSimpleName(const NamedDecl &D) {
179+
if (IdentifierInfo *Ident = D.getDeclName().getAsIdentifierInfo()) {
180+
return Ident->getName();
181+
}
182+
183+
return StringRef();
184+
}
185+
186+
NameVec getParameterNamesForDecl(const FunctionDecl *Function,
187+
size_t ArgCount) {
188+
NameVec Result;
189+
for (size_t I = 0; I < ArgCount; ++I) {
190+
const ParmVarDecl *Parm = Function->getParamDecl(I);
191+
assert(Parm);
192+
Result.emplace_back(getSimpleName(*Parm));
193+
}
194+
return Result;
195+
}
196+
197+
void addInlayHint(SourceRange R, InlayHintKind Kind, llvm::StringRef Label) {
198+
auto FileRange =
199+
toHalfOpenFileRange(AST.getSourceManager(), AST.getLangOpts(), R);
200+
if (!FileRange)
201+
return;
202+
Results.push_back(InlayHint{
203+
Range{
204+
sourceLocToPosition(AST.getSourceManager(), FileRange->getBegin()),
205+
sourceLocToPosition(AST.getSourceManager(), FileRange->getEnd())},
206+
Kind, Label.str()});
207+
}
208+
209+
std::vector<InlayHint> &Results;
210+
ASTContext &AST;
211+
};
212+
213+
std::vector<InlayHint> inlayHints(ParsedAST &AST) {
214+
std::vector<InlayHint> Results;
215+
InlayHintVisitor Visitor(Results, AST);
216+
Visitor.TraverseAST(AST.getASTContext());
217+
return Results;
218+
}
219+
220+
} // namespace clangd
221+
} // namespace clang

clang-tools-extra/clangd/InlayHints.h

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===--- InlayHints.h --------------------------------------------*- C++-*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// Support for the proposed "inlay hints" LSP feature.
10+
// The version currently implemented is the one proposed here:
11+
// https://github.com/microsoft/vscode-languageserver-node/pull/609/.
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H
16+
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H
17+
18+
#include "Protocol.h"
19+
#include <vector>
20+
21+
namespace clang {
22+
namespace clangd {
23+
class ParsedAST;
24+
25+
// Compute and return all inlay hints for a file.
26+
std::vector<InlayHint> inlayHints(ParsedAST &AST);
27+
28+
} // namespace clangd
29+
} // namespace clang
30+
31+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H

clang-tools-extra/clangd/Protocol.cpp

+19
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,25 @@ llvm::json::Value toJSON(const CallHierarchyOutgoingCall &C) {
13031303
return llvm::json::Object{{"to", C.to}, {"fromRanges", C.fromRanges}};
13041304
}
13051305

1306+
bool fromJSON(const llvm::json::Value &Params, InlayHintsParams &R,
1307+
llvm::json::Path P) {
1308+
llvm::json::ObjectMapper O(Params, P);
1309+
return O && O.map("textDocument", R.textDocument);
1310+
}
1311+
1312+
llvm::json::Value toJSON(InlayHintKind K) {
1313+
switch (K) {
1314+
case InlayHintKind::ParameterHint:
1315+
return "parameter";
1316+
}
1317+
llvm_unreachable("Unknown clang.clangd.InlayHintKind");
1318+
}
1319+
1320+
llvm::json::Value toJSON(const InlayHint &H) {
1321+
return llvm::json::Object{
1322+
{"range", H.range}, {"kind", H.kind}, {"label", H.label}};
1323+
}
1324+
13061325
static const char *toString(OffsetEncoding OE) {
13071326
switch (OE) {
13081327
case OffsetEncoding::UTF8:

0 commit comments

Comments
 (0)