From 807316500a3eab42c563d11d9b330d5beae9a782 Mon Sep 17 00:00:00 2001 From: Roberto Raggi Date: Sat, 16 Nov 2024 13:11:09 +0000 Subject: [PATCH] Add initial responses to member completion requests in the LSP server --- src/frontend/cxx/frontend.cc | 3 ++ src/frontend/cxx/verify_diagnostics_client.cc | 2 + src/lsp/cxx/lsp/cxx_document.cc | 48 +++++++++++++++++-- src/lsp/cxx/lsp/cxx_document.h | 5 +- src/lsp/cxx/lsp/lsp_server.cc | 14 +++--- src/parser/cxx/diagnostics_client.h | 4 ++ src/parser/cxx/parser.cc | 38 +++++++++++++++ src/parser/cxx/parser.h | 5 +- src/parser/cxx/parser_fwd.h | 18 +++++++ tests/unit_tests/lsp/code_completion_01.yml | 24 ++++++++++ 10 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/lsp/code_completion_01.yml diff --git a/src/frontend/cxx/frontend.cc b/src/frontend/cxx/frontend.cc index c227a31f..4a27f2ef 100644 --- a/src/frontend/cxx/frontend.cc +++ b/src/frontend/cxx/frontend.cc @@ -26,12 +26,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include diff --git a/src/frontend/cxx/verify_diagnostics_client.cc b/src/frontend/cxx/verify_diagnostics_client.cc index e558450b..ecf12ebd 100644 --- a/src/frontend/cxx/verify_diagnostics_client.cc +++ b/src/frontend/cxx/verify_diagnostics_client.cc @@ -76,6 +76,8 @@ auto VerifyDiagnosticsClient::hasErrors() const -> bool { } void VerifyDiagnosticsClient::report(const Diagnostic& diagnostic) { + if (!shouldReportErrors()) return; + if (verify_) { reportedDiagnostics_.push_back(diagnostic); return; diff --git a/src/lsp/cxx/lsp/cxx_document.cc b/src/lsp/cxx/lsp/cxx_document.cc index 2edc26d3..22feb3f8 100644 --- a/src/lsp/cxx/lsp/cxx_document.cc +++ b/src/lsp/cxx/lsp/cxx_document.cc @@ -23,14 +23,18 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include @@ -38,6 +42,9 @@ #include #endif +#include +#include + namespace cxx::lsp { namespace { @@ -74,6 +81,7 @@ struct CxxDocument::Private { Diagnostics diagnosticsClient; TranslationUnit unit{&diagnosticsClient}; std::shared_ptr toolchain; + Vector completionItems; #ifndef CXX_NO_THREADS std::atomic cancelled{false}; @@ -202,10 +210,20 @@ void CxxDocument::cancel() { auto CxxDocument::fileName() const -> const std::string& { return d->fileName; } -void CxxDocument::requestCodeCompletion(std::uint32_t line, - std::uint32_t column) { +void CxxDocument::codeCompletionAt(std::string source, std::uint32_t line, + std::uint32_t column, + Vector completionItems) { + std::swap(d->completionItems, completionItems); + auto& unit = d->unit; + + (void)unit.blockErrors(true); + unit.preprocessor()->requestCodeCompletionAt(line, column); + + parse(std::move(source)); + + std::swap(d->completionItems, completionItems); } void CxxDocument::parse(std::string source) { @@ -227,13 +245,37 @@ void CxxDocument::parse(std::string source) { unit.endPreprocessing(); + auto stopParsingPredicate = [this] { return isCancelled(); }; + + auto complete = [this](const CodeCompletionContext& context) { + if (auto memberCompletionContext = + std::get_if(&context)) { + // simple member completion + auto objectType = memberCompletionContext->objectType; + + if (auto pointerType = type_cast(objectType)) { + objectType = type_cast(pointerType->elementType()); + } + + if (auto classType = type_cast(objectType)) { + auto classSymbol = classType->symbol(); + for (auto member : classSymbol->members()) { + if (!member->name()) continue; + auto item = d->completionItems.emplace_back(); + item.label(to_string(member->name())); + } + } + } + }; + unit.parse(ParserConfiguration{ .checkTypes = cli.opt_fcheck, .fuzzyTemplateResolution = true, .staticAssert = cli.opt_fstatic_assert || cli.opt_fcheck, .reflect = !cli.opt_fno_reflect, .templates = cli.opt_ftemplates, - .stopParsingPredicate = [this] { return isCancelled(); }, + .stopParsingPredicate = stopParsingPredicate, + .complete = complete, }); } diff --git a/src/lsp/cxx/lsp/cxx_document.h b/src/lsp/cxx/lsp/cxx_document.h index 4fa31f4f..181ca71f 100644 --- a/src/lsp/cxx/lsp/cxx_document.h +++ b/src/lsp/cxx/lsp/cxx_document.h @@ -39,10 +39,11 @@ class CxxDocument { [[nodiscard]] auto fileName() const -> const std::string&; - void requestCodeCompletion(std::uint32_t line, std::uint32_t column); - void parse(std::string source); + void codeCompletionAt(std::string source, std::uint32_t line, + std::uint32_t column, Vector result); + [[nodiscard]] auto version() const -> long; [[nodiscard]] auto diagnostics() const -> Vector; diff --git a/src/lsp/cxx/lsp/lsp_server.cc b/src/lsp/cxx/lsp/lsp_server.cc index 151370f5..fa50dcb8 100644 --- a/src/lsp/cxx/lsp/lsp_server.cc +++ b/src/lsp/cxx/lsp/lsp_server.cc @@ -493,22 +493,24 @@ void Server::operator()(CompletionRequest request) { auto column = request.params().position().character(); const auto& text = documentContents_.at(uri); - auto value = text.value; + auto source = text.value; run([=, this, fileName = pathFromUri(uri)] { withUnsafeJson([&](json storage) { + CompletionResponse response(storage); + response.id(request.id()); + // the version is not relevant for code completion requests as we don't // need to store the document in the cache. auto cxxDocument = std::make_shared(cli, std::move(fileName), /*version=*/0); + auto completionItems = response.result>(); + // cxx expects 1-based line and column numbers - cxxDocument->requestCodeCompletion(line + 1, column + 1); - cxxDocument->parse(std::move(value)); + cxxDocument->codeCompletionAt(std::move(source), line + 1, column + 1, + completionItems); - CompletionResponse response(storage); - response.id(request.id()); - auto completionItems = response.result>(); sendToClient(response); }); }); diff --git a/src/parser/cxx/diagnostics_client.h b/src/parser/cxx/diagnostics_client.h index e7a994b8..fd36b7ed 100644 --- a/src/parser/cxx/diagnostics_client.h +++ b/src/parser/cxx/diagnostics_client.h @@ -48,6 +48,10 @@ class DiagnosticsClient { [[nodiscard]] auto fatalErrors() const -> bool { return fatalErrors_; } void setFatalErrors(bool fatalErrors) { fatalErrors_ = fatalErrors; } + [[nodiscard]] auto shouldReportErrors() const -> bool { + return !blockErrors_; + } + auto blockErrors(bool blockErrors = true) -> bool { std::swap(blockErrors_, blockErrors); return blockErrors; diff --git a/src/parser/cxx/parser.cc b/src/parser/cxx/parser.cc index 397777c1..c58f29c8 100644 --- a/src/parser/cxx/parser.cc +++ b/src/parser/cxx/parser.cc @@ -38,7 +38,15 @@ #include #include #include +#include #include +#include + +#include "cxx/cxx_fwd.h" +#include "cxx/parser_fwd.h" +#include "cxx/source_location.h" +#include "cxx/symbols_fwd.h" +#include "cxx/token_fwd.h" namespace cxx { @@ -1271,6 +1279,20 @@ void Parser::parse_skip_declaration(bool& skipping) { skipping = true; } +auto Parser::parse_completion(SourceLocation& loc) -> bool { + // if already reported a completion, return false + if (didAcceptCompletionToken_) return false; + + // if there is no completer, return false + if (!config_.complete) return false; + + if (!match(TokenKind::T_CODE_COMPLETION, loc)) return false; + + didAcceptCompletionToken_ = true; + + return true; +} + auto Parser::parse_primary_expression(ExpressionAST*& yyast, const ExprContext& ctx) -> bool { UnqualifiedIdAST* name = nullptr; @@ -2434,6 +2456,22 @@ auto Parser::parse_member_expression(ExpressionAST*& yyast) -> bool { ast->isTemplateIntroduced = match(TokenKind::T_TEMPLATE, ast->templateLoc); + const Type* objectType = nullptr; + + if (ast->baseExpression) { + // test if the base expression has a type + objectType = ast->baseExpression->type; + } + + if (SourceLocation completionLoc; + objectType && parse_completion(completionLoc)) { + // trigger the completion + config_.complete(MemberCompletionContext{ + .objectType = objectType, + .accessOp = ast->accessOp, + }); + } + if (!parse_unqualified_id(ast->unqualifiedId, ast->nestedNameSpecifier, ast->isTemplateIntroduced, /*inRequiresClause*/ false)) diff --git a/src/parser/cxx/parser.h b/src/parser/cxx/parser.h index b007955b..aeee6d9e 100644 --- a/src/parser/cxx/parser.h +++ b/src/parser/cxx/parser.h @@ -108,6 +108,7 @@ class Parser final { static auto prec(TokenKind tk) -> Prec; [[nodiscard]] auto shouldStopParsing() const -> bool { + if (didAcceptCompletionToken_) return true; if (config_.stopParsingPredicate) return config_.stopParsingPredicate(); return false; } @@ -141,6 +142,8 @@ class Parser final { void parse_translation_unit(UnitAST*& yyast); + [[nodiscard]] auto parse_completion(SourceLocation& loc) -> bool; + [[nodiscard]] auto parse_id(const Identifier* id, SourceLocation& loc) -> bool; [[nodiscard]] auto parse_nospace() -> bool; @@ -871,7 +874,7 @@ class Parser final { std::uint32_t cursor_ = 0; int templateParameterDepth_ = -1; int templateParameterCount_ = 0; - + bool didAcceptCompletionToken_ = false; std::vector pendingFunctionDefinitions_; template diff --git a/src/parser/cxx/parser_fwd.h b/src/parser/cxx/parser_fwd.h index dfb00efb..e8e99180 100644 --- a/src/parser/cxx/parser_fwd.h +++ b/src/parser/cxx/parser_fwd.h @@ -21,12 +21,29 @@ #pragma once +#include +#include +#include + #include +#include namespace cxx { class Parser; +struct UnqualifiedCompletionContext { + Scope* scope = nullptr; +}; + +struct MemberCompletionContext { + const Type* objectType = nullptr; + TokenKind accessOp = TokenKind::T_DOT; +}; + +using CodeCompletionContext = + std::variant; + struct ParserConfiguration { bool checkTypes = false; bool fuzzyTemplateResolution = false; @@ -34,6 +51,7 @@ struct ParserConfiguration { bool reflect = true; bool templates = false; std::function stopParsingPredicate; + std::function complete; }; } // namespace cxx diff --git a/tests/unit_tests/lsp/code_completion_01.yml b/tests/unit_tests/lsp/code_completion_01.yml new file mode 100644 index 00000000..7f1bce33 --- /dev/null +++ b/tests/unit_tests/lsp/code_completion_01.yml @@ -0,0 +1,24 @@ +# RUN: %cxx -lsp-test < %s | %filecheck %s + +{ "method": "initialize", "id": 0 } + +# CHECK: "id": 0 + +{ "method": "textDocument/didOpen", "id": 1, "params": { "textDocument": { "uri": "test:///source.cc", "version": 0, "text": "struct P { int x, y; }; void ff() { P p; p.\n\n\n}" } } } + +{ "method": "$/setTrace", "id": 2, "params": { "value": "verbose" } } + +{ "method": "textDocument/completion", "id": 3, "params": { "textDocument": { "uri": "test:///source.cc" }, "position": { "line": 2, "character": 1 } } } + +# CHECK: "message": "Did receive CompletionRequest" +# CHECK: "id": 3 +# CHECK: "result": +# CHECK: "label": "x" +# CHECK: "label": "y" + +{ "method": "shutdown", "id": 4 } + +{ "method": "exit" } + + +