Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -586,6 +586,7 @@ llvm::Expected List) { if (!List) return Reply(List.takeError()); + log("completion excuse: {0}", List->Excuse); CompletionList LSPList; LSPList.isIncomplete = List->HasMore; for (const auto &R : List->Completions) { Index: clangd/CodeComplete.h =================================================================== --- clangd/CodeComplete.h +++ clangd/CodeComplete.h @@ -196,6 +196,10 @@ std::vector Completions; bool HasMore = false; CodeCompletionContext::Kind Context = CodeCompletionContext::CCC_Other; + + // Our best guess at why completions might be poor (human-readable string). + // Only set if we suspect completions are poor *and* we know why. + std::string Excuse; }; raw_ostream &operator<<(raw_ostream &, const CodeCompleteResult &); Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -671,6 +671,42 @@ return false; } +// Tries to come up with a convincing excuse for our bad completion results, +// based on the diagnostics near the completion point. +class ExcuseMaker : public DiagnosticConsumer { + unsigned StartOfLine, Offset; + std::string &Excuse; + SmallString<256> ExcuseBuf; + + public: + ExcuseMaker(StringRef Code, unsigned Offset, std::string &Excuse) + : Offset(Offset), Excuse(Excuse) { + assert(Offset <= Code.size()); + for (StartOfLine = Offset; + StartOfLine > 0 && Code[StartOfLine - 1] != '\n'; --StartOfLine) + ; + } + + void HandleDiagnostic(DiagnosticsEngine::Level Level, + const clang::Diagnostic &Diag) override { + // We accept >= error diagnostics, before the cursor but on the same line. + if (Level < DiagnosticsEngine::Error || !Diag.hasSourceManager()) + return; + auto& SM = Diag.getSourceManager(); + auto Loc = SM.getDecomposedLoc(Diag.getLocation()); + if (Loc.first != SM.getMainFileID()) + return; + unsigned DiagLoc = Loc.second; + if (DiagLoc < StartOfLine || DiagLoc > Offset) + return; + ExcuseBuf.clear(); + Diag.FormatDiagnostic(ExcuseBuf); + } + + ~ExcuseMaker() { Excuse = ExcuseBuf.str(); } +}; + + // The CompletionRecorder captures Sema code-complete output, including context. // It filters out ignored results (but doesn't apply fuzzy-filtering yet). // It doesn't do scoring or conversion to CompletionItem yet, as we want to @@ -1000,7 +1036,8 @@ bool semaCodeComplete(std::unique_ptr Consumer, const clang::CodeCompleteOptions &Options, const SemaCompleteInput &Input, - IncludeStructure *Includes = nullptr) { + IncludeStructure *Includes = nullptr, + std::string *Excuse = nullptr) { trace::Span Tracer("Sema completion"); std::vector ArgStrs; for (const auto &S : Input.Command.CommandLine) @@ -1012,14 +1049,21 @@ // working dirs. } + auto Offset = positionToOffset(Input.Contents, Input.Pos); + if (!Offset) { + elog("Code completion position was invalid {0}", Offset.takeError()); + return false; + } + std::unique_ptr DiagConsumer( + Excuse ? new ExcuseMaker(Input.Contents, *Offset, *Excuse) + : (DiagnosticConsumer *)new IgnoreDiagnostics()); IntrusiveRefCntPtr VFS = Input.VFS; if (Input.Preamble && Input.Preamble->StatCache) VFS = Input.Preamble->StatCache->getConsumingFS(std::move(VFS)); - IgnoreDiagnostics DummyDiagsConsumer; auto CI = createInvocationFromCommandLine( ArgStrs, CompilerInstance::createDiagnostics(new DiagnosticOptions, - &DummyDiagsConsumer, false), + DiagConsumer.get(), false), VFS); if (!CI) { elog("Couldn't create CompilerInvocation"); @@ -1034,11 +1078,6 @@ // Setup code completion. FrontendOpts.CodeCompleteOpts = Options; FrontendOpts.CodeCompletionAt.FileName = Input.FileName; - auto Offset = positionToOffset(Input.Contents, Input.Pos); - if (!Offset) { - elog("Code completion position was invalid {0}", Offset.takeError()); - return false; - } std::tie(FrontendOpts.CodeCompletionAt.Line, FrontendOpts.CodeCompletionAt.Column) = offsetToClangLineColumn(Input.Contents, *Offset); @@ -1063,7 +1102,7 @@ (Input.Preamble && !CompletingInPreamble) ? &Input.Preamble->Preamble : nullptr, std::move(ContentsBuffer), std::move(Input.PCHs), std::move(VFS), - DummyDiagsConsumer); + *DiagConsumer); Clang->getPreprocessorOpts().SingleFileParseMode = CompletingInPreamble; Clang->setCodeCompletionConsumer(Consumer.release()); @@ -1308,7 +1347,7 @@ Recorder = RecorderOwner.get(); semaCodeComplete(std::move(RecorderOwner), Opts.getClangCompleteOpts(), - SemaCCInput, &Includes); + SemaCCInput, &Includes, &Output.Excuse); SPAN_ATTACH(Tracer, "sema_results", NSema); SPAN_ATTACH(Tracer, "index_results", NIndex); Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -2176,6 +2176,50 @@ AllOf(Qualifier("nx::"), Named("Clangd2")))); } +TEST(CompletionTest, Excuses) { + struct { + const char *Code; + const char *Excuse; + } Tests[] = { + { + R"cpp( + class X; + X* x; + int main() { + x->^ + } + )cpp", + "member access into incomplete type 'X'", + }, + { + R"cpp( + // Error is on same line. + template struct X; + X x; ^ + )cpp", + "implicit instantiation of undefined template 'X'", + }, + { + R"cpp( + // Error is on previous line. + template struct X; + X x; + ^ + )cpp", + "", + }, + { + R"cpp( + // Just a warning (declaration does not declare anything). + int; x^ + )cpp", + "", + }, + }; + for (const auto &Test : Tests) + EXPECT_EQ(completions(Test.Code).Excuse, Test.Excuse) << Test.Code; +} + } // namespace } // namespace clangd } // namespace clang