#include #include #include #include #include #include #include #include #include #include "fold/lsp/diagnostic.hpp" #include "fold/lsp/server.hpp" #include "fold/lsp/transport.hpp" namespace { // In-memory LSP transport. The queue holds entire bodies (no Content-Length // framing) so tests can describe messages declaratively; the LSP server // only sees parsed bodies via readMessage anyway. class ScriptedTransport final : public fold::lsp::Transport { public: void enqueue(const nlohmann::json& msg) { in_.push_back(msg.dump()); } std::optional readMessage() override { if (in_.empty()) return std::nullopt; std::string m = std::move(in_.front()); in_.pop_front(); return m; } void writeMessage(const nlohmann::json& m) override { out_.push_back(m); } const std::vector& out() const { return out_; } private: std::deque in_; std::vector out_; }; std::string locateFixtures() { const char* env = std::getenv("FOLD_FIXTURES_DIR"); std::string fixtures = env ? env : "../fixtures"; if (!std::filesystem::exists(fixtures + "/clones/exact.cpp")) { fixtures = std::filesystem::current_path().string() + "/../fixtures"; } REQUIRE(std::filesystem::exists(fixtures + "/clones/exact.cpp")); return fixtures; } void writeCompileDb(const std::string& fixtures) { namespace fs = std::filesystem; nlohmann::json db = nlohmann::json::array(); for (const auto& f : {"clones/exact.cpp", "clones/renamed.cpp"}) { fs::path file = fs::path(fixtures) / f; db.push_back({ {"directory", fixtures}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } std::ofstream(fs::path(fixtures) / "compile_commands.json") << db.dump(); } } // namespace TEST_CASE("LSP: initialize returns server capabilities", "[lsp][handshake]") { auto transport = std::make_unique(); auto* t = transport.get(); t->enqueue({{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", {{"rootUri", "file:///tmp"}}}}); t->enqueue({{"jsonrpc", "2.0"}, {"id", 2}, {"method", "shutdown"}}); t->enqueue({{"jsonrpc", "2.0"}, {"method", "exit"}}); fold::lsp::Server server(std::move(transport)); REQUIRE(server.run() == 0); REQUIRE(t->out().size() == 2); const auto& init = t->out()[0]; REQUIRE(init["id"] == 1); REQUIRE(init["result"]["serverInfo"]["name"] == "fold"); REQUIRE(init["result"]["capabilities"]["textDocumentSync"]["save"].get() == true); REQUIRE(t->out()[1]["id"] == 2); } TEST_CASE("LSP: didSave triggers publishDiagnostics with fold findings", "[lsp][diagnostics]") { std::string fixtures = locateFixtures(); writeCompileDb(fixtures); std::string clone_file = fixtures + "/clones/exact.cpp"; auto transport = std::make_unique(); auto* t = transport.get(); t->enqueue({{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", { {"rootUri", fold::lsp::uriFromAbsolutePath(fixtures)}, }}}); t->enqueue({{"jsonrpc", "2.0"}, {"method", "initialized"}}); t->enqueue({{"jsonrpc", "2.0"}, {"method", "textDocument/didSave"}, {"params", { {"textDocument", {{"uri", fold::lsp::uriFromAbsolutePath(clone_file)}}}, }}}); t->enqueue({{"jsonrpc", "2.0"}, {"id", 2}, {"method", "shutdown"}}); t->enqueue({{"jsonrpc", "2.0"}, {"method", "exit"}}); fold::lsp::Server server(std::move(transport)); REQUIRE(server.run() == 0); bool saw_diagnostics = false; for (const auto& msg : t->out()) { if (msg.value("method", "") == "textDocument/publishDiagnostics") { auto diags = msg["params"]["diagnostics"]; if (diags.is_array() && !diags.empty()) { REQUIRE(diags[0]["source"] == "fold"); REQUIRE(diags[0].contains("range")); saw_diagnostics = true; } } } REQUIRE(saw_diagnostics); } TEST_CASE("LSP URI helpers round-trip POSIX paths", "[lsp][uri]") { std::string p = "/tmp/has space/file#hash.cpp"; std::string uri = fold::lsp::uriFromAbsolutePath(p); REQUIRE(uri.rfind("file://", 0) == 0); REQUIRE(fold::lsp::absolutePathFromUri(uri) == p); }