[clangd] Complete filenames after < / ".

Summary:
Extract prefix filters to CodeComplete so it can be easily tested.

Fixes https://github.com/clangd/clangd/issues/366

Reviewers: adamcz

Subscribers: ilya-biryukov, MaskRay, jkorous, arphaman, kadircet, usaxena95, cfe-commits

Tags: #clang

Differential Revision: https://reviews.llvm.org/D79456
This commit is contained in:
Sam McCall 2020-05-06 01:39:59 +02:00
parent fa8fc9ffcc
commit 032727f4f8
5 changed files with 86 additions and 24 deletions

View File

@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "ClangdLSPServer.h"
#include "CodeComplete.h"
#include "Diagnostics.h"
#include "DraftStore.h"
#include "GlobalCompilationDatabase.h"
@ -593,9 +594,8 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
llvm::json::Object{
{"allCommitCharacters", " \t()[]{}<>:;,+-/*%^&#?.=\"'|"},
{"resolveProvider", false},
// We do extra checks for '>' and ':' in completion to only
// trigger on '->' and '::'.
{"triggerCharacters", {".", ">", ":"}},
// We do extra checks, e.g. that > is part of ->.
{"triggerCharacters", {".", "<", ">", ":", "\"", "/"}},
}},
{"semanticTokensProvider",
llvm::json::Object{
@ -1425,23 +1425,19 @@ std::vector<Fix> ClangdLSPServer::getFixes(llvm::StringRef File,
return FixItsIter->second;
}
// A completion request is sent when the user types '>' or ':', but we only
// want to trigger on '->' and '::'. We check the preceeding text to make
// sure it matches what we expected.
// Running the lexer here would be more robust (e.g. we can detect comments
// and avoid triggering completion there), but we choose to err on the side
// of simplicity here.
bool ClangdLSPServer::shouldRunCompletion(
const CompletionParams &Params) const {
llvm::StringRef Trigger = Params.context.triggerCharacter;
if (Params.context.triggerKind != CompletionTriggerKind::TriggerCharacter ||
(Trigger != ">" && Trigger != ":"))
if (Params.context.triggerKind != CompletionTriggerKind::TriggerCharacter)
return true;
auto Code = DraftMgr.getDraft(Params.textDocument.uri.file());
if (!Code)
return true; // completion code will log the error for untracked doc.
// A completion request is sent when the user types '>' or ':', but we only
// want to trigger on '->' and '::'. We check the preceeding character to make
// sure it matches what we expected.
// Running the lexer here would be more robust (e.g. we can detect comments
// and avoid triggering completion there), but we choose to err on the side
// of simplicity here.
auto Offset = positionToOffset(Code->Contents, Params.position,
/*AllowColumnsBeyondLineLength=*/false);
if (!Offset) {
@ -1449,15 +1445,7 @@ bool ClangdLSPServer::shouldRunCompletion(
Params.position, Params.textDocument.uri.file());
return true;
}
if (*Offset < 2)
return false;
if (Trigger == ">")
return Code->Contents[*Offset - 2] == '-'; // trigger only on '->'.
if (Trigger == ":")
return Code->Contents[*Offset - 2] == ':'; // trigger only on '::'.
assert(false && "unhandled trigger character");
return true;
return allowImplicitCompletion(Code->Contents, *Offset);
}
void ClangdLSPServer::onHighlightingsReady(

View File

@ -1912,5 +1912,43 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
return OS;
}
// Heuristically detect whether the `Line` is an unterminated include filename.
bool isIncludeFile(llvm::StringRef Line) {
Line = Line.ltrim();
if (!Line.consume_front("#"))
return false;
Line = Line.ltrim();
if (!(Line.consume_front("include_next") || Line.consume_front("include") ||
Line.consume_front("import")))
return false;
Line = Line.ltrim();
if (Line.consume_front("<"))
return Line.count('>') == 0;
if (Line.consume_front("\""))
return Line.count('"') == 0;
return false;
}
bool allowImplicitCompletion(llvm::StringRef Content, unsigned Offset) {
// Look at last line before completion point only.
Content = Content.take_front(Offset);
auto Pos = Content.rfind('\n');
if (Pos != llvm::StringRef::npos)
Content = Content.substr(Pos + 1);
// Complete after scope operators.
if (Content.endswith(".") || Content.endswith("->") || Content.endswith("::"))
return true;
// Complete after `#include <` and #include `<foo/`.
if ((Content.endswith("<") || Content.endswith("\"") ||
Content.endswith("/")) &&
isIncludeFile(Content))
return true;
// Complete words. Give non-ascii characters the benefit of the doubt.
return !Content.empty() &&
(isIdentifierBody(Content.back()) || !llvm::isASCII(Content.back()));
}
} // namespace clangd
} // namespace clang

View File

@ -314,6 +314,10 @@ struct CompletionPrefix {
CompletionPrefix guessCompletionPrefix(llvm::StringRef Content,
unsigned Offset);
// Whether it makes sense to complete at the point based on typed characters.
// For instance, we implicitly trigger at `a->^` but not at `a>^`.
bool allowImplicitCompletion(llvm::StringRef Content, unsigned Offset);
} // namespace clangd
} // namespace clang

View File

@ -11,8 +11,11 @@
# CHECK-NEXT: "resolveProvider": false,
# CHECK-NEXT: "triggerCharacters": [
# CHECK-NEXT: ".",
# CHECK-NEXT: "<",
# CHECK-NEXT: ">",
# CHECK-NEXT: ":"
# CHECK-NEXT: ":",
# CHECK-NEXT: "\"",
# CHECK-NEXT: "/"
# CHECK-NEXT: ]
# CHECK-NEXT: },
# CHECK-NEXT: "declarationProvider": true,

View File

@ -25,6 +25,7 @@
#include "clang/Tooling/CompilationDatabase.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Path.h"
#include "llvm/Testing/Support/Annotations.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@ -2828,6 +2829,34 @@ TEST(NoCompileCompletionTest, WithIndex) {
ElementsAre(AllOf(Qualifier(""), Scope("a::"))));
}
TEST(AllowImplicitCompletion, All) {
const char *Yes[] = {
"foo.^bar",
"foo->^bar",
"foo::^bar",
" # include <^foo.h>",
"#import <foo/^bar.h>",
"#include_next \"^",
};
const char *No[] = {
"foo>^bar",
"foo:^bar",
"foo\n^bar",
"#include <foo.h> //^",
"#include \"foo.h\"^",
"#error <^",
"#<^",
};
for (const char *Test : Yes) {
llvm::Annotations A(Test);
EXPECT_TRUE(allowImplicitCompletion(A.code(), A.point())) << Test;
}
for (const char *Test : No) {
llvm::Annotations A(Test);
EXPECT_FALSE(allowImplicitCompletion(A.code(), A.point())) << Test;
}
}
} // namespace
} // namespace clangd
} // namespace clang