[clangd] Add code completion of param name on /* inside function calls.

For example, if you have:
  void foo(int bar);
  foo(/*^
it should auto-complete to "bar=".

Because Sema callbacks for code completion in comments happen before we
have an AST we need to cheat in clangd by detecting completion on /*
before, moving cursor back by two characters, then running a simplified
verion of SignatureHelp to extract argument name(s) from possible
overloads.

Differential Revision: https://reviews.llvm.org/D110823
This commit is contained in:
Adam Czachorowski 2021-09-30 15:11:29 +02:00
parent 7dfb139554
commit 8fbac4e88a
4 changed files with 145 additions and 4 deletions

View File

@ -542,7 +542,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
"^", "&", "#", "?", ".", "=", "\"", "'", "|"}},
{"resolveProvider", false},
// We do extra checks, e.g. that > is part of ->.
{"triggerCharacters", {".", "<", ">", ":", "\"", "/"}},
{"triggerCharacters", {".", "<", ">", ":", "\"", "/", "*"}},
}},
{"semanticTokensProvider",
llvm::json::Object{

View File

@ -1098,6 +1098,50 @@ private:
const SymbolIndex *Index;
}; // SignatureHelpCollector
// Used only for completion of C-style comments in function call (i.e.
// /*foo=*/7). Similar to SignatureHelpCollector, but needs to do less work.
class ParamNameCollector final : public CodeCompleteConsumer {
public:
ParamNameCollector(const clang::CodeCompleteOptions &CodeCompleteOpts,
std::set<std::string> &ParamNames)
: CodeCompleteConsumer(CodeCompleteOpts),
Allocator(std::make_shared<clang::GlobalCodeCompletionAllocator>()),
CCTUInfo(Allocator), ParamNames(ParamNames) {}
void ProcessOverloadCandidates(Sema &S, unsigned CurrentArg,
OverloadCandidate *Candidates,
unsigned NumCandidates,
SourceLocation OpenParLoc) override {
assert(CurrentArg <= (unsigned)std::numeric_limits<int>::max() &&
"too many arguments");
for (unsigned I = 0; I < NumCandidates; ++I) {
OverloadCandidate Candidate = Candidates[I];
auto *Func = Candidate.getFunction();
if (!Func || Func->getNumParams() <= CurrentArg)
continue;
auto *PVD = Func->getParamDecl(CurrentArg);
if (!PVD)
continue;
auto *Ident = PVD->getIdentifier();
if (!Ident)
continue;
auto Name = Ident->getName();
if (!Name.empty())
ParamNames.insert(Name.str());
}
}
private:
GlobalCodeCompletionAllocator &getAllocator() override { return *Allocator; }
CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; }
std::shared_ptr<clang::GlobalCodeCompletionAllocator> Allocator;
CodeCompletionTUInfo CCTUInfo;
std::set<std::string> &ParamNames;
};
struct SemaCompleteInput {
PathRef FileName;
size_t Offset;
@ -1860,6 +1904,59 @@ CompletionPrefix guessCompletionPrefix(llvm::StringRef Content,
return Result;
}
// Code complete the argument name on "/*" inside function call.
// Offset should be pointing to the start of the comment, i.e.:
// foo(^/*, rather than foo(/*^) where the cursor probably is.
CodeCompleteResult codeCompleteComment(PathRef FileName, unsigned Offset,
llvm::StringRef Prefix,
const PreambleData *Preamble,
const ParseInputs &ParseInput) {
if (Preamble == nullptr) // Can't run without Sema.
return CodeCompleteResult();
clang::CodeCompleteOptions Options;
Options.IncludeGlobals = false;
Options.IncludeMacros = false;
Options.IncludeCodePatterns = false;
Options.IncludeBriefComments = false;
std::set<std::string> ParamNames;
// We want to see signatures coming from newly introduced includes, hence a
// full patch.
semaCodeComplete(
std::make_unique<ParamNameCollector>(Options, ParamNames), Options,
{FileName, Offset, *Preamble,
PreamblePatch::createFullPatch(FileName, ParseInput, *Preamble),
ParseInput});
if (ParamNames.empty())
return CodeCompleteResult();
CodeCompleteResult Result;
Result.Context = CodeCompletionContext::CCC_NaturalLanguage;
for (llvm::StringRef Name : ParamNames) {
if (!Name.startswith(Prefix))
continue;
CodeCompletion Item;
Item.Name = Name.str() + "=";
Item.Kind = CompletionItemKind::Text;
Result.Completions.push_back(Item);
}
return Result;
}
// If Offset is inside what looks like argument comment (e.g.
// "/*^" or "/* foo^"), returns new offset pointing to the start of the /*
// (place where semaCodeComplete should run).
llvm::Optional<unsigned>
maybeFunctionArgumentCommentStart(llvm::StringRef Content) {
while (!Content.empty() && isAsciiIdentifierContinue(Content.back()))
Content = Content.drop_back();
Content = Content.rtrim();
if (Content.endswith("/*"))
return Content.size() - 2;
return None;
}
CodeCompleteResult codeComplete(PathRef FileName, Position Pos,
const PreambleData *Preamble,
const ParseInputs &ParseInput,
@ -1870,6 +1967,19 @@ CodeCompleteResult codeComplete(PathRef FileName, Position Pos,
elog("Code completion position was invalid {0}", Offset.takeError());
return CodeCompleteResult();
}
auto Content = llvm::StringRef(ParseInput.Contents).take_front(*Offset);
if (auto OffsetBeforeComment = maybeFunctionArgumentCommentStart(Content)) {
// We are doing code completion of a comment, where we currently only
// support completing param names in function calls. To do this, we
// require information from Sema, but Sema's comment completion stops at
// parsing, so we must move back the position before running it, extract
// information we need and construct completion items ourselves.
auto CommentPrefix = Content.substr(*OffsetBeforeComment + 2).trim();
return codeCompleteComment(FileName, *OffsetBeforeComment, CommentPrefix,
Preamble, ParseInput);
}
auto Flow = CodeCompleteFlow(
FileName, Preamble ? Preamble->Includes : IncludeStructure(),
SpecFuzzyFind, Opts);
@ -2053,7 +2163,8 @@ bool allowImplicitCompletion(llvm::StringRef Content, unsigned Offset) {
Content = Content.substr(Pos + 1);
// Complete after scope operators.
if (Content.endswith(".") || Content.endswith("->") || Content.endswith("::"))
if (Content.endswith(".") || Content.endswith("->") ||
Content.endswith("::") || Content.endswith("/*"))
return true;
// Complete after `#include <` and #include `<foo/`.
if ((Content.endswith("<") || Content.endswith("\"") ||

View File

@ -48,7 +48,8 @@
# CHECK-NEXT: ">",
# CHECK-NEXT: ":",
# CHECK-NEXT: "\"",
# CHECK-NEXT: "/"
# CHECK-NEXT: "/",
# CHECK-NEXT: "*"
# CHECK-NEXT: ]
# CHECK-NEXT: },
# CHECK-NEXT: "declarationProvider": true,

View File

@ -3029,7 +3029,7 @@ TEST(CompletionTest, CompletionRange) {
// Sema doesn't trigger at all here, while the no-sema completion runs
// heuristics as normal and reports a range. It'd be nice to be consistent.
const char *NoCompletion = "/* [[]]^ */";
const char *NoCompletion = "/* foo [[]]^ */";
Completions = completions(NoCompletion);
EXPECT_EQ(Completions.CompletionRange, llvm::None);
Completions = completionsNoCompile(NoCompletion);
@ -3279,6 +3279,35 @@ TEST(CompletionTest, PreambleCodeComplete) {
EXPECT_THAT(Result.Completions, Not(testing::IsEmpty()));
}
TEST(CompletionTest, CommentParamName) {
clangd::CodeCompleteOptions Opts;
const std::string Code = R"cpp(
void fun(int foo, int bar);
void overloaded(int param_int);
void overloaded(int param_int, int param_other);
void overloaded(char param_char);
int main() {
)cpp";
EXPECT_THAT(completions(Code + "fun(/*^", {}, Opts).Completions,
UnorderedElementsAre(Labeled("foo=")));
EXPECT_THAT(completions(Code + "fun(1, /*^", {}, Opts).Completions,
UnorderedElementsAre(Labeled("bar=")));
EXPECT_THAT(completions(Code + "/*^", {}, Opts).Completions, IsEmpty());
// Test de-duplication.
EXPECT_THAT(
completions(Code + "overloaded(/*^", {}, Opts).Completions,
UnorderedElementsAre(Labeled("param_int="), Labeled("param_char=")));
// Comment already has some text in it.
EXPECT_THAT(completions(Code + "fun(/* ^", {}, Opts).Completions,
UnorderedElementsAre(Labeled("foo=")));
EXPECT_THAT(completions(Code + "fun(/* f^", {}, Opts).Completions,
UnorderedElementsAre(Labeled("foo=")));
EXPECT_THAT(completions(Code + "fun(/* x^", {}, Opts).Completions, IsEmpty());
EXPECT_THAT(completions(Code + "fun(/* f ^", {}, Opts).Completions,
IsEmpty());
}
} // namespace
} // namespace clangd
} // namespace clang