Skip to content

Commit

Permalink
[clangd] Add support for inline parameter hints
Browse files Browse the repository at this point in the history
Differential Revision: https://reviews.llvm.org/D98748
  • Loading branch information
HighCommander4 committed Apr 14, 2021
1 parent 02265ed commit cbc9c4e
Show file tree
Hide file tree
Showing 13 changed files with 669 additions and 1 deletion.
1 change: 1 addition & 0 deletions clang-tools-extra/clangd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ add_clang_library(clangDaemon
HeuristicResolver.cpp
Hover.cpp
IncludeFixer.cpp
InlayHints.cpp
JSONTransport.cpp
PathMapping.cpp
Protocol.cpp
Expand Down
7 changes: 7 additions & 0 deletions clang-tools-extra/clangd/ClangdLSPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
{"referencesProvider", true},
{"astProvider", true}, // clangd extension
{"typeHierarchyProvider", true},
{"clangdInlayHintsProvider", true},
{"memoryUsageProvider", true}, // clangd extension
{"compilationDatabase", // clangd extension
llvm::json::Object{{"automaticReload", true}}},
Expand Down Expand Up @@ -1208,6 +1209,11 @@ void ClangdLSPServer::onCallHierarchyOutgoingCalls(
Reply(std::vector<CallHierarchyOutgoingCall>{});
}

void ClangdLSPServer::onInlayHints(const InlayHintsParams &Params,
Callback<std::vector<InlayHint>> Reply) {
Server->inlayHints(Params.textDocument.uri.file(), std::move(Reply));
}

void ClangdLSPServer::applyConfiguration(
const ConfigurationSettings &Settings) {
// Per-file update to the compilation database.
Expand Down Expand Up @@ -1471,6 +1477,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink);
Bind.method("textDocument/semanticTokens/full", this, &ClangdLSPServer::onSemanticTokens);
Bind.method("textDocument/semanticTokens/full/delta", this, &ClangdLSPServer::onSemanticTokensDelta);
Bind.method("clangd/inlayHints", this, &ClangdLSPServer::onInlayHints);
Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage);
if (Opts.FoldingRanges)
Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange);
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clangd/ClangdLSPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
void onCallHierarchyOutgoingCalls(
const CallHierarchyOutgoingCallsParams &,
Callback<std::vector<CallHierarchyOutgoingCall>>);
void onInlayHints(const InlayHintsParams &, Callback<std::vector<InlayHint>>);
void onChangeConfiguration(const DidChangeConfigurationParams &);
void onSymbolInfo(const TextDocumentPositionParams &,
Callback<std::vector<SymbolDetails>>);
Expand Down
12 changes: 12 additions & 0 deletions clang-tools-extra/clangd/ClangdServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "Format.h"
#include "HeaderSourceSwitch.h"
#include "Headers.h"
#include "InlayHints.h"
#include "ParsedAST.h"
#include "Preamble.h"
#include "Protocol.h"
Expand Down Expand Up @@ -751,6 +752,17 @@ void ClangdServer::incomingCalls(
});
}

void ClangdServer::inlayHints(PathRef File,
Callback<std::vector<InlayHint>> CB) {
auto Action = [File = File.str(),
CB = std::move(CB)](Expected<InputsAndAST> InpAST) mutable {
if (!InpAST)
return CB(InpAST.takeError());
CB(clangd::inlayHints(InpAST->AST));
};
WorkScheduler->runWithAST("InlayHints", File, std::move(Action));
}

void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
// FIXME: Do nothing for now. This will be used for indexing and potentially
// invalidating other caches.
Expand Down
3 changes: 3 additions & 0 deletions clang-tools-extra/clangd/ClangdServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ class ClangdServer {
void incomingCalls(const CallHierarchyItem &Item,
Callback<std::vector<CallHierarchyIncomingCall>>);

/// Resolve inlay hints for a given document.
void inlayHints(PathRef File, Callback<std::vector<InlayHint>>);

/// Retrieve the top symbols from the workspace matching a query.
void workspaceSymbols(StringRef Query, int Limit,
Callback<std::vector<SymbolInformation>> CB);
Expand Down
221 changes: 221 additions & 0 deletions clang-tools-extra/clangd/InlayHints.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//===--- InlayHints.cpp ------------------------------------------*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "InlayHints.h"
#include "ParsedAST.h"
#include "support/Logger.h"
#include "clang/AST/DeclarationName.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/SourceManager.h"

namespace clang {
namespace clangd {

class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
public:
InlayHintVisitor(std::vector<InlayHint> &Results, ParsedAST &AST)
: Results(Results), AST(AST.getASTContext()) {}

bool VisitCXXConstructExpr(CXXConstructExpr *E) {
// Weed out constructor calls that don't look like a function call with
// an argument list, by checking the validity of getParenOrBraceRange().
// Also weed out std::initializer_list constructors as there are no names
// for the individual arguments.
if (!E->getParenOrBraceRange().isValid() ||
E->isStdInitListInitialization()) {
return true;
}

processCall(E->getParenOrBraceRange().getBegin(), E->getConstructor(),
{E->getArgs(), E->getNumArgs()});
return true;
}

bool VisitCallExpr(CallExpr *E) {
// Do not show parameter hints for operator calls written using operator
// syntax or user-defined literals. (Among other reasons, the resulting
// hints can look awkard, e.g. the expression can itself be a function
// argument and then we'd get two hints side by side).
if (isa<CXXOperatorCallExpr>(E) || isa<UserDefinedLiteral>(E))
return true;

processCall(E->getRParenLoc(),
dyn_cast_or_null<FunctionDecl>(E->getCalleeDecl()),
{E->getArgs(), E->getNumArgs()});
return true;
}

// FIXME: Handle RecoveryExpr to try to hint some invalid calls.

private:
// The purpose of Anchor is to deal with macros. It should be the call's
// opening or closing parenthesis or brace. (Always using the opening would
// make more sense but CallExpr only exposes the closing.) We heuristically
// assume that if this location does not come from a macro definition, then
// the entire argument list likely appears in the main file and can be hinted.
void processCall(SourceLocation Anchor, const FunctionDecl *Callee,
llvm::ArrayRef<const Expr *const> Args) {
if (Args.size() == 0 || !Callee)
return;

// If the anchor location comes from a macro defintion, there's nowhere to
// put hints.
if (!AST.getSourceManager().getTopMacroCallerLoc(Anchor).isFileID())
return;

// The parameter name of a move or copy constructor is not very interesting.
if (auto *Ctor = dyn_cast<CXXConstructorDecl>(Callee))
if (Ctor->isCopyOrMoveConstructor())
return;

// FIXME: Exclude setters (i.e. functions with one argument whose name
// begins with "set"), as their parameter name is also not likely to be
// interesting.

// Don't show hints for variadic parameters.
size_t FixedParamCount = getFixedParamCount(Callee);
size_t ArgCount = std::min(FixedParamCount, Args.size());

NameVec ParameterNames = chooseParameterNames(Callee, ArgCount);

for (size_t I = 0; I < ArgCount; ++I) {
StringRef Name = ParameterNames[I];
if (!shouldHint(Args[I], Name))
continue;

addInlayHint(Args[I]->getSourceRange(), InlayHintKind::ParameterHint,
Name.str() + ": ");
}
}

bool shouldHint(const Expr *Arg, StringRef ParamName) {
if (ParamName.empty())
return false;

// If the argument expression is a single name and it matches the
// parameter name exactly, omit the hint.
if (ParamName == getSpelledIdentifier(Arg))
return false;

// FIXME: Exclude argument expressions preceded by a /*paramName=*/ comment.

return true;
}

// If "E" spells a single unqualified identifier, return that name.
// Otherwise, return an empty string.
static StringRef getSpelledIdentifier(const Expr *E) {
E = E->IgnoreUnlessSpelledInSource();

if (auto *DRE = dyn_cast<DeclRefExpr>(E))
if (!DRE->getQualifier())
return getSimpleName(*DRE->getDecl());

if (auto *ME = dyn_cast<MemberExpr>(E))
if (!ME->getQualifier() && ME->isImplicitAccess())
return getSimpleName(*ME->getMemberDecl());

return {};
}

using NameVec = SmallVector<StringRef, 8>;

NameVec chooseParameterNames(const FunctionDecl *Callee, size_t ArgCount) {
// The current strategy here is to use all the parameter names from the
// canonical declaration, unless they're all empty, in which case we
// use all the parameter names from the definition (in present in the
// translation unit).
// We could try a bit harder, e.g.:
// - try all re-declarations, not just canonical + definition
// - fall back arg-by-arg rather than wholesale

NameVec ParameterNames = getParameterNamesForDecl(Callee, ArgCount);

if (llvm::all_of(ParameterNames, std::mem_fn(&StringRef::empty))) {
if (const FunctionDecl *Def = Callee->getDefinition()) {
ParameterNames = getParameterNamesForDecl(Def, ArgCount);
}
}
assert(ParameterNames.size() == ArgCount);

// Standard library functions often have parameter names that start
// with underscores, which makes the hints noisy, so strip them out.
for (auto &Name : ParameterNames)
stripLeadingUnderscores(Name);

return ParameterNames;
}

static void stripLeadingUnderscores(StringRef &Name) {
Name = Name.ltrim('_');
}

// Return the number of fixed parameters Function has, that is, not counting
// parameters that are variadic (instantiated from a parameter pack) or
// C-style varargs.
static size_t getFixedParamCount(const FunctionDecl *Function) {
if (FunctionTemplateDecl *Template = Function->getPrimaryTemplate()) {
FunctionDecl *F = Template->getTemplatedDecl();
size_t Result = 0;
for (ParmVarDecl *Parm : F->parameters()) {
if (Parm->isParameterPack()) {
break;
}
++Result;
}
return Result;
}
// C-style varargs don't need special handling, they're already
// not included in getNumParams().
return Function->getNumParams();
}

static StringRef getSimpleName(const NamedDecl &D) {
if (IdentifierInfo *Ident = D.getDeclName().getAsIdentifierInfo()) {
return Ident->getName();
}

return StringRef();
}

NameVec getParameterNamesForDecl(const FunctionDecl *Function,
size_t ArgCount) {
NameVec Result;
for (size_t I = 0; I < ArgCount; ++I) {
const ParmVarDecl *Parm = Function->getParamDecl(I);
assert(Parm);
Result.emplace_back(getSimpleName(*Parm));
}
return Result;
}

void addInlayHint(SourceRange R, InlayHintKind Kind, llvm::StringRef Label) {
auto FileRange =
toHalfOpenFileRange(AST.getSourceManager(), AST.getLangOpts(), R);
if (!FileRange)
return;
Results.push_back(InlayHint{
Range{
sourceLocToPosition(AST.getSourceManager(), FileRange->getBegin()),
sourceLocToPosition(AST.getSourceManager(), FileRange->getEnd())},
Kind, Label.str()});
}

std::vector<InlayHint> &Results;
ASTContext &AST;
};

std::vector<InlayHint> inlayHints(ParsedAST &AST) {
std::vector<InlayHint> Results;
InlayHintVisitor Visitor(Results, AST);
Visitor.TraverseAST(AST.getASTContext());
return Results;
}

} // namespace clangd
} // namespace clang
31 changes: 31 additions & 0 deletions clang-tools-extra/clangd/InlayHints.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===--- InlayHints.h --------------------------------------------*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Support for the proposed "inlay hints" LSP feature.
// The version currently implemented is the one proposed here:
// https://github.com/microsoft/vscode-languageserver-node/pull/609/.
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H

#include "Protocol.h"
#include <vector>

namespace clang {
namespace clangd {
class ParsedAST;

// Compute and return all inlay hints for a file.
std::vector<InlayHint> inlayHints(ParsedAST &AST);

} // namespace clangd
} // namespace clang

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_INLAY_HINTS_H
19 changes: 19 additions & 0 deletions clang-tools-extra/clangd/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,25 @@ llvm::json::Value toJSON(const CallHierarchyOutgoingCall &C) {
return llvm::json::Object{{"to", C.to}, {"fromRanges", C.fromRanges}};
}

bool fromJSON(const llvm::json::Value &Params, InlayHintsParams &R,
llvm::json::Path P) {
llvm::json::ObjectMapper O(Params, P);
return O && O.map("textDocument", R.textDocument);
}

llvm::json::Value toJSON(InlayHintKind K) {
switch (K) {
case InlayHintKind::ParameterHint:
return "parameter";
}
llvm_unreachable("Unknown clang.clangd.InlayHintKind");
}

llvm::json::Value toJSON(const InlayHint &H) {
return llvm::json::Object{
{"range", H.range}, {"kind", H.kind}, {"label", H.label}};
}

static const char *toString(OffsetEncoding OE) {
switch (OE) {
case OffsetEncoding::UTF8:
Expand Down
Loading

0 comments on commit cbc9c4e

Please sign in to comment.