From fc8e22dfcede7aa32a1c150bb4c4e90be0217db7 Mon Sep 17 00:00:00 2001 From: Roberto Raggi Date: Mon, 28 Oct 2024 20:19:50 +0100 Subject: [PATCH] chore: Add a simple thread pool to the LSP server --- .github/workflows/ci.yml | 2 + CMakeLists.txt | 2 + CMakePresets.json | 14 +- src/frontend/CMakeLists.txt | 11 +- src/frontend/cxx/cli.cc | 4 + src/frontend/cxx/cli.h | 1 + src/frontend/cxx/cxx_document.cc | 218 +++++++++++ src/frontend/cxx/cxx_document.h | 47 +++ src/frontend/cxx/frontend.cc | 22 +- src/frontend/cxx/lsp_server.cc | 607 ++++++++++++------------------- src/frontend/cxx/lsp_server.h | 80 +++- src/frontend/cxx/sync_queue.cc | 57 +++ src/frontend/cxx/sync_queue.h | 52 +++ 13 files changed, 723 insertions(+), 394 deletions(-) create mode 100644 src/frontend/cxx/cxx_document.cc create mode 100644 src/frontend/cxx/cxx_document.h create mode 100644 src/frontend/cxx/sync_queue.cc create mode 100644 src/frontend/cxx/sync_queue.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b49358bf..f2b91baf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: -I build/_deps/nlohmann_json-src/include \ -I build/src/parser \ -DCXX_NO_FILESYSTEM \ + -DCXX_NO_THREADS \ $i done @@ -218,6 +219,7 @@ jobs: -I build.wasi/_deps/nlohmann_json-src/include \ -I build.wasi/src/parser \ -DCXX_NO_FILESYSTEM \ + -DCXX_NO_THREADS \ $i done diff --git a/CMakeLists.txt b/CMakeLists.txt index bdb168dc..913fa6f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,8 @@ option(CXX_LIBCXX_WITH_CLANG "Link with libc++" OFF) option(CXX_BUILD_TESTS "Build tests" ON) option(CXX_INTERPROCEDURAL_OPTIMIZATION "Enable interprocedural optimization" OFF) +find_package(Threads) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CXX_LIBCXX_WITH_CLANG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() diff --git a/CMakePresets.json b/CMakePresets.json index fc025bb1..0f4e6c24 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -76,9 +76,7 @@ { "name": "install", "configurePreset": "default", - "targets": [ - "install" - ] + "targets": ["install"] }, { "name": "build-emscripten", @@ -87,9 +85,7 @@ { "name": "install-emscripten", "configurePreset": "emscripten", - "targets": [ - "install" - ] + "targets": ["install"] }, { "name": "build-wasi", @@ -98,9 +94,7 @@ { "name": "install-wasi", "configurePreset": "wasi", - "targets": [ - "install" - ] + "targets": ["install"] } ], "testPresets": [ @@ -174,4 +168,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 12332909..6a27191d 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -27,8 +27,17 @@ target_compile_definitions(cxx PRIVATE CXX_VERSION="${CMAKE_PROJECT_VERSION}" ) +# if cmake founds the Threads package, link with it +if(Threads_FOUND) + target_link_libraries(cxx Threads::Threads) + target_compile_options(cxx PRIVATE -pthread) +else() +target_compile_definitions(cxx PRIVATE CXX_NO_THREADS) +endif() + if(EMSCRIPTEN) - target_link_options(cxx PUBLIC + target_link_options(cxx PRIVATE -pthread) + target_link_options(cxx PRIVATE "SHELL:-s EXIT_RUNTIME=1" "SHELL:-s WASM_BIGINT=1" "SHELL:-s NODERAWFS=1" diff --git a/src/frontend/cxx/cli.cc b/src/frontend/cxx/cli.cc index 78275efc..6887f812 100644 --- a/src/frontend/cxx/cli.cc +++ b/src/frontend/cxx/cli.cc @@ -198,6 +198,10 @@ std::vector options{ {"-lsp", "Start Language Server", &CLI::opt_lsp}, + {"-lsp-test", "Start Language Server in testing mode", &CLI::opt_lsp_test}, + + {"-j", "", "Run jobs in parallel.", CLIOptionDescrKind::kSeparated}, + {"-v", "Show commands to run and use verbose output", &CLI::opt_v}, }; diff --git a/src/frontend/cxx/cli.h b/src/frontend/cxx/cli.h index 10ce3de0..5cc21bd0 100644 --- a/src/frontend/cxx/cli.h +++ b/src/frontend/cxx/cli.h @@ -74,6 +74,7 @@ class CLI { bool opt_v = false; bool opt_emit_ast = false; bool opt_lsp = false; + bool opt_lsp_test = false; void parse(int& argc, char**& argv); diff --git a/src/frontend/cxx/cxx_document.cc b/src/frontend/cxx/cxx_document.cc new file mode 100644 index 00000000..2759999d --- /dev/null +++ b/src/frontend/cxx/cxx_document.cc @@ -0,0 +1,218 @@ +// Copyright (c) 2024 Roberto Raggi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "cxx_document.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cxx::lsp { + +namespace { + +struct Diagnostics final : cxx::DiagnosticsClient { + json messages = json::array(); + Vector diagnostics{messages}; + + void report(const cxx::Diagnostic& diag) override { + std::string_view fileName; + std::uint32_t line = 0; + std::uint32_t column = 0; + + preprocessor()->getTokenStartPosition(diag.token(), &line, &column, + &fileName); + + std::uint32_t endLine = 0; + std::uint32_t endColumn = 0; + + preprocessor()->getTokenEndPosition(diag.token(), &endLine, &endColumn, + nullptr); + + auto tmp = json::object(); + + auto d = diagnostics.emplace_back(); + + int s = std::max(int(line) - 1, 0); + int sc = std::max(int(column) - 1, 0); + int e = std::max(int(endLine) - 1, 0); + int ec = std::max(int(endColumn) - 1, 0); + + d.message(diag.message()); + d.range().start(lsp::Position(tmp).line(s).character(sc)); + d.range().end(lsp::Position(tmp).line(e).character(ec)); + } +}; + +} // namespace + +struct CxxDocument::Private { + const CLI& cli; + long version; + Control control; + Diagnostics diagnosticsClient; + TranslationUnit unit{&control, &diagnosticsClient}; + std::shared_ptr toolchain; + + Private(const CLI& cli, long version) : cli(cli), version(version) {} + + void configure(); +}; + +void CxxDocument::Private::configure() { + auto preprocesor = unit.preprocessor(); + + auto toolchainId = cli.getSingle("-toolchain"); + + if (!toolchainId) { + toolchainId = "wasm32"; + } + + if (toolchainId == "darwin" || toolchainId == "macos") { + toolchain = std::make_unique(preprocesor); + } else if (toolchainId == "wasm32") { + auto wasmToolchain = std::make_unique(preprocesor); + + fs::path app_dir; + +#if __wasi__ + app_dir = fs::path("/usr/bin/"); +#elif !defined(CXX_NO_FILESYSTEM) + app_dir = std::filesystem::canonical( + std::filesystem::path(cli.app_name).remove_filename()); +#elif __unix__ || __APPLE__ + char* app_name = realpath(cli.app_name.c_str(), nullptr); + app_dir = fs::path(app_name).remove_filename().string(); + std::free(app_name); +#endif + + wasmToolchain->setAppdir(app_dir.string()); + + if (auto paths = cli.get("--sysroot"); !paths.empty()) { + wasmToolchain->setSysroot(paths.back()); + } else { + auto sysroot_dir = app_dir / std::string("../lib/wasi-sysroot"); + wasmToolchain->setSysroot(sysroot_dir.string()); + } + + toolchain = std::move(wasmToolchain); + } else if (toolchainId == "linux") { + std::string host; +#ifdef __aarch64__ + host = "aarch64"; +#elif __x86_64__ + host = "x86_64"; +#endif + + std::string arch = cli.getSingle("-arch").value_or(host); + toolchain = std::make_unique(preprocesor, arch); + } else if (toolchainId == "windows") { + auto windowsToolchain = std::make_unique(preprocesor); + + if (auto paths = cli.get("-vctoolsdir"); !paths.empty()) { + windowsToolchain->setVctoolsdir(paths.back()); + } + + if (auto paths = cli.get("-winsdkdir"); !paths.empty()) { + windowsToolchain->setWinsdkdir(paths.back()); + } + + if (auto versions = cli.get("-winsdkversion"); !versions.empty()) { + windowsToolchain->setWinsdkversion(versions.back()); + } + + toolchain = std::move(windowsToolchain); + } + + if (toolchain) { + control.setMemoryLayout(toolchain->memoryLayout()); + + if (!cli.opt_nostdinc) toolchain->addSystemIncludePaths(); + + if (!cli.opt_nostdincpp) toolchain->addSystemCppIncludePaths(); + + toolchain->addPredefinedMacros(); + } + + for (const auto& path : cli.get("-I")) { + preprocesor->addSystemIncludePath(path); + } + + for (const auto& macro : cli.get("-D")) { + auto sep = macro.find_first_of("="); + + if (sep == std::string::npos) { + preprocesor->defineMacro(macro, "1"); + } else { + preprocesor->defineMacro(macro.substr(0, sep), macro.substr(sep + 1)); + } + } + + for (const auto& macro : cli.get("-U")) { + preprocesor->undefMacro(macro); + } +} + +CxxDocument::CxxDocument(const CLI& cli, long version) + : d(std::make_unique(cli, version)) {} + +void CxxDocument::parse(std::string source, std::string fileName) { + d->configure(); + + auto& unit = d->unit; + auto& cli = d->cli; + + unit.setSource(std::move(source), fileName); + + auto preprocessor = unit.preprocessor(); + preprocessor->squeeze(); + + 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, + }); +} + +CxxDocument::~CxxDocument() {} + +auto CxxDocument::version() const -> long { return d->version; } + +auto CxxDocument::diagnostics() const -> Vector { + return Vector(d->diagnosticsClient.messages); +} + +} // namespace cxx::lsp \ No newline at end of file diff --git a/src/frontend/cxx/cxx_document.h b/src/frontend/cxx/cxx_document.h new file mode 100644 index 00000000..f9827365 --- /dev/null +++ b/src/frontend/cxx/cxx_document.h @@ -0,0 +1,47 @@ +// Copyright (c) 2024 Roberto Raggi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include + +#include +#include + +#include "cli.h" + +namespace cxx::lsp { + +class CxxDocument { + public: + explicit CxxDocument(const CLI& cli, long version); + ~CxxDocument(); + + void parse(std::string source, std::string fileName); + + [[nodiscard]] auto version() const -> long; + [[nodiscard]] auto diagnostics() const -> Vector; + + private: + struct Private; + std::unique_ptr d; +}; + +} // namespace cxx::lsp \ No newline at end of file diff --git a/src/frontend/cxx/frontend.cc b/src/frontend/cxx/frontend.cc index c0032d14..fc0f0bf4 100644 --- a/src/frontend/cxx/frontend.cc +++ b/src/frontend/cxx/frontend.cc @@ -18,7 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// cxx #include #include #include @@ -34,24 +33,18 @@ #include #include -#include - -#include "ast_printer.h" -#include "lsp_server.h" -#include "verify_diagnostics_client.h" - -// std -#include #include +#include #include -#include #include #include #include -#include #include +#include "ast_printer.h" #include "cli.h" +#include "lsp_server.h" +#include "verify_diagnostics_client.h" namespace { using namespace cxx; @@ -305,6 +298,10 @@ auto main(int argc, char* argv[]) -> int { const auto& inputFiles = cli.positionals(); + if (cli.opt_lsp_test) { + cli.opt_lsp = true; + } + if (!cli.opt_lsp && inputFiles.empty()) { std::cerr << "cxx: no input files" << std::endl << "Usage: cxx [options] file..." << std::endl; @@ -314,7 +311,8 @@ auto main(int argc, char* argv[]) -> int { int existStatus = EXIT_SUCCESS; if (cli.opt_lsp) { - existStatus = lsp::startServer(cli); + lsp::Server server(cli); + existStatus = server.start(); } else { for (const auto& fileName : inputFiles) { if (!runOnFile(cli, fileName)) { diff --git a/src/frontend/cxx/lsp_server.cc b/src/frontend/cxx/lsp_server.cc index 7f98620f..3e7f07c6 100644 --- a/src/frontend/cxx/lsp_server.cc +++ b/src/frontend/cxx/lsp_server.cc @@ -20,65 +20,89 @@ #include "lsp_server.h" -#include -#include -#include -#include -#include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include -#include #include #include +#include "cxx_document.h" + namespace cxx::lsp { -struct Header { - std::string name; - std::string value; -}; +Server::Server(const CLI& cli) + : cli(cli), input(std::cin), output(std::cout), log(std::cerr) { + // create workers + const auto workerCount = 4; -} // namespace cxx::lsp + auto worker = [] { -template <> -struct std::less { - using is_transparent = void; + }; +} - auto operator()(const cxx::lsp::Header& lhs, - const cxx::lsp::Header& rhs) const -> bool { - return lhs.name < rhs.name; - } +Server::~Server() {} - auto operator()(const cxx::lsp::Header& lhs, - const std::string_view& rhs) const -> bool { - return lhs.name < rhs; +auto Server::start() -> int { + log << std::format("Starting LSP server\n"); + + startWorkersIfNeeded(); + + while (!done_ && input.good()) { + if (done_) { + break; + } + + if (auto req = nextRequest()) { + auto request = LSPRequest(req.value()); + visit(*this, request); + } } - auto operator()(const std::string_view& lhs, - const cxx::lsp::Header& rhs) const -> bool { - return lhs < rhs.name; + stopWorkersIfNeeded(); + + return 0; +} + +auto Server::nextRequest() -> std::optional { + if (cli.opt_lsp_test) { + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } else if (line.starts_with("#")) { + continue; + } + return json::parse(line); + } + return std::nullopt; } -}; -namespace cxx::lsp { + const auto headers = readHeaders(input); + + // Get Content-Length + const auto it = headers.find("Content-Length"); + + if (it == headers.end()) { + return std::nullopt; + }; -using Headers = std::set
; + const auto contentLength = std::stoi(it->second); -auto readHeaders(std::istream& input) -> Headers { - Headers headers; + // Read content + std::string content(contentLength, '\0'); + input.read(content.data(), content.size()); + + // Parse JSON + auto request = json::parse(content); + return request; +} + +auto Server::readHeaders(std::istream& input) + -> std::unordered_map { + std::unordered_map headers; std::string line; @@ -101,405 +125,250 @@ auto readHeaders(std::istream& input) -> Headers { value.erase(0, value.find_first_not_of(" \t\r\n")); value.erase(value.find_last_not_of(" \t\r\n") + 1); - headers.insert({name, value}); + headers.insert_or_assign(std::move(name), std::move(value)); } return headers; } -struct CxxDocument { - struct Diagnostics final : cxx::DiagnosticsClient { - json messages = json::array(); - Vector diagnostics{messages}; - - void report(const cxx::Diagnostic& diag) override { - std::string_view fileName; - std::uint32_t line = 0; - std::uint32_t column = 0; - - preprocessor()->getTokenStartPosition(diag.token(), &line, &column, - &fileName); - - std::uint32_t endLine = 0; - std::uint32_t endColumn = 0; - - preprocessor()->getTokenEndPosition(diag.token(), &endLine, &endColumn, - nullptr); +void Server::sendToClient(const json& message) { +#ifndef CXX_NO_THREADS + auto locker = std::unique_lock(outputMutex_); +#endif - auto tmp = json::object(); + if (cli.opt_lsp_test) { + output << message.dump(2) << "\n"; + } else { + const auto text = message.dump(); + output << std::format("Content-Length: {}\r\n\r\n{}", text.size(), text); + } - auto d = diagnostics.emplace_back(); + output.flush(); +} - int s = std::max(int(line) - 1, 0); - int sc = std::max(int(column) - 1, 0); - int e = std::max(int(endLine) - 1, 0); - int ec = std::max(int(endColumn) - 1, 0); +void Server::sendToClient(const LSPObject& result, + std::optional> id) { + auto response = json::object(); + response["jsonrpc"] = "2.0"; - d.message(diag.message()); - d.range().start(lsp::Position(tmp).line(s).character(sc)); - d.range().end(lsp::Position(tmp).line(e).character(ec)); + if (id.has_value()) { + if (std::holds_alternative(id.value())) { + response["id"] = std::get(id.value()); + } else { + response["id"] = std::get(id.value()); } - }; - - const CLI& cli; - Control control; - Diagnostics diagnosticsClient; - TranslationUnit unit; - std::unique_ptr toolchain; + } - CxxDocument(const CLI& cli) : cli(cli), unit(&control, &diagnosticsClient) {} + response["result"] = result; - void parse(std::string source, std::string fileName) { - configure(); + sendToClient(response); +} - unit.setSource(std::move(source), fileName); +void Server::sendNotification(const LSPRequest& notification) { + json response = notification; + response["jsonrpc"] = "2.0"; - auto preprocessor = unit.preprocessor(); - preprocessor->squeeze(); + sendToClient(response); +} - 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, - }); +auto Server::pathFromUri(const std::string& uri) -> std::string { + if (uri.starts_with("file://")) { + return uri.substr(7); + } else if (cli.opt_lsp_test && uri.starts_with("test://")) { + return uri.substr(7); } - private: - void configure() { - auto preprocesor = unit.preprocessor(); + lsp_runtime_error(std::format("Unsupported URI scheme: {}\n", uri)); +} - auto toolchainId = cli.getSingle("-toolchain"); +void Server::startWorkersIfNeeded() { +#ifndef CXX_NO_THREADS + const auto threadCountOption = cli.getSingle("-j"); - if (!toolchainId) { - toolchainId = "wasm32"; - } + if (!threadCountOption.has_value()) { + return; + } - if (toolchainId == "darwin" || toolchainId == "macos") { - toolchain = std::make_unique(preprocesor); - } else if (toolchainId == "wasm32") { - auto wasmToolchain = std::make_unique(preprocesor); - - fs::path app_dir; - -#if __wasi__ - app_dir = fs::path("/usr/bin/"); -#elif !defined(CXX_NO_FILESYSTEM) - app_dir = std::filesystem::canonical( - std::filesystem::path(cli.app_name).remove_filename()); -#elif __unix__ || __APPLE__ - char* app_name = realpath(cli.app_name.c_str(), nullptr); - app_dir = fs::path(app_name).remove_filename().string(); - std::free(app_name); -#endif + auto workerCount = std::stoi(threadCountOption.value()); - wasmToolchain->setAppdir(app_dir.string()); + if (workerCount <= 0) { + workerCount = int(std::thread::hardware_concurrency()); + } - if (auto paths = cli.get("--sysroot"); !paths.empty()) { - wasmToolchain->setSysroot(paths.back()); - } else { - auto sysroot_dir = app_dir / std::string("../lib/wasi-sysroot"); - wasmToolchain->setSysroot(sysroot_dir.string()); + for (int i = 0; i < workerCount; ++i) { + workers_.emplace_back([this] { + while (true) { + auto task = syncQueue_.pop(); + if (syncQueue_.closed()) break; + task(); } - - toolchain = std::move(wasmToolchain); - } else if (toolchainId == "linux") { - std::string host; -#ifdef __aarch64__ - host = "aarch64"; -#elif __x86_64__ - host = "x86_64"; + }); + } #endif +} - std::string arch = cli.getSingle("-arch").value_or(host); - toolchain = std::make_unique(preprocesor, arch); - } else if (toolchainId == "windows") { - auto windowsToolchain = std::make_unique(preprocesor); - - if (auto paths = cli.get("-vctoolsdir"); !paths.empty()) { - windowsToolchain->setVctoolsdir(paths.back()); - } - - if (auto paths = cli.get("-winsdkdir"); !paths.empty()) { - windowsToolchain->setWinsdkdir(paths.back()); - } - - if (auto versions = cli.get("-winsdkversion"); !versions.empty()) { - windowsToolchain->setWinsdkversion(versions.back()); - } - - toolchain = std::move(windowsToolchain); - } - - if (toolchain) { - control.setMemoryLayout(toolchain->memoryLayout()); - - if (!cli.opt_nostdinc) toolchain->addSystemIncludePaths(); - - if (!cli.opt_nostdincpp) toolchain->addSystemCppIncludePaths(); - - toolchain->addPredefinedMacros(); - } - - for (const auto& path : cli.get("-I")) { - preprocesor->addSystemIncludePath(path); - } - - if (cli.opt_v) { - std::cerr << std::format("#include <...> search starts here:\n"); - const auto& paths = preprocesor->systemIncludePaths(); - for (auto it = rbegin(paths); it != rend(paths); ++it) { - std::cerr << std::format(" {}\n", *it); - } - std::cerr << std::format("End of search list.\n"); - } +void Server::stopWorkersIfNeeded() { +#ifndef CXX_NO_THREADS + if (workers_.empty()) { + return; + } - for (const auto& macro : cli.get("-D")) { - auto sep = macro.find_first_of("="); + syncQueue_.close(); - if (sep == std::string::npos) { - preprocesor->defineMacro(macro, "1"); - } else { - preprocesor->defineMacro(macro.substr(0, sep), macro.substr(sep + 1)); - } - } + for (int i = 0; i < workers_.size(); ++i) { + syncQueue_.push([] {}); + } - for (const auto& macro : cli.get("-U")) { - preprocesor->undefMacro(macro); - } + std::ranges::for_each(workers_, &std::thread::join); +#endif +} - std::cerr << "Starting LSP server\n"; +void Server::run(std::function task) { +#ifndef CXX_NO_THREADS + if (!workers_.empty()) { + syncQueue_.push(std::move(task)); + return; } -}; +#endif -class Server { - const CLI& cli; - std::istream& input; - std::unordered_map> documents; + task(); +} - public: - Server(const CLI& cli) : cli(cli), input(std::cin) {} +void Server::parse(std::string uri, std::string text, long version) { + run([text = std::move(text), uri = std::move(uri), version, this] { + auto doc = std::make_shared(cli, version); + doc->parse(std::move(text), pathFromUri(uri)); - auto start() -> int { - while (input.good()) { - auto req = nextRequest(); + { +#ifndef CXX_NO_THREADS + auto locker = std::unique_lock(outputMutex_); +#endif - if (!req.has_value()) { - continue; + if (documents_.contains(uri) && documents_.at(uri)->version() > version) { + return; } - visit(*this, LSPRequest(req.value())); + documents_[uri] = doc; } - return 0; - } - - auto nextRequest() -> std::optional { - const auto headers = readHeaders(input); - - // Get Content-Length - const auto it = headers.find("Content-Length"); - - if (it == headers.end()) { - return std::nullopt; - }; - - const auto contentLength = std::stoi(it->value); - - // Read content - std::string content(contentLength, '\0'); - input.read(content.data(), content.size()); + withUnsafeJson([&](json storage) { + PublishDiagnosticsNotification publishDiagnostics(storage); + publishDiagnostics.method("textDocument/publishDiagnostics"); + publishDiagnostics.params().uri(uri); + publishDiagnostics.params().diagnostics(doc->diagnostics()); + publishDiagnostics.params().version(version); - // Parse JSON - auto request = json::parse(content); - return request; - } + sendNotification(publishDiagnostics); + }); + }); +} - void sendToClient(const json& message, std::ostream& output = std::cout) { - const auto text = message.dump(); - output << "Content-Length: " << text.size() << "\r\n\r\n"; - output << text; - output.flush(); - } +void Server::operator()(const InitializeRequest& request) { + log << std::format("Did receive InitializeRequest\n"); - void sendToClient( - const LSPObject& result, - std::optional> id = std::nullopt, - std::ostream& output = std::cout) { - auto response = json::object(); - response["jsonrpc"] = "2.0"; - - if (id.has_value()) { - if (std::holds_alternative(id.value())) { - response["id"] = std::get(id.value()); - } else { - response["id"] = std::get(id.value()); - } - } + withUnsafeJson([&](json storage) { + InitializeResult result(storage); + result.serverInfo().name("cxx-lsp").version(CXX_VERSION); + result.capabilities().textDocumentSync(TextDocumentSyncKind::kFull); + sendToClient(result, request.id()); + }); +} - response["result"] = result; +void Server::operator()(const InitializedNotification& notification) { + log << std::format("Did receive InitializedNotification\n"); +} - sendToClient(response); - } +void Server::operator()(const ShutdownRequest& request) { + log << std::format("Did receive ShutdownRequest\n"); - // - // notifications - // - void operator()(const InitializedNotification& notification) { - std::cerr << std::format("Did receive InitializedNotification\n"); - } + withUnsafeJson([&](json storage) { + LSPObject result(storage); + sendToClient(result, request.id()); + }); +} - void operator()(const ExitNotification& notification) { - std::cerr << std::format("Did receive ExitNotification\n"); - } +void Server::operator()(const ExitNotification& notification) { + log << std::format("Did receive ExitNotification\n"); + done_ = true; +} - void operator()(const DidOpenTextDocumentNotification& notification) { - std::cerr << std::format("Did receive DidOpenTextDocumentNotification\n"); +void Server::operator()(const DidOpenTextDocumentNotification& notification) { + log << std::format("Did receive DidOpenTextDocumentNotification\n"); - auto textDocument = notification.params().textDocument(); - const auto uri = textDocument.uri(); - const auto text = textDocument.text(); - const auto version = textDocument.version(); + auto textDocument = notification.params().textDocument(); + parse(textDocument.uri(), textDocument.text(), textDocument.version()); +} - auto doc = std::make_shared(cli); - doc->parse(std::move(text), pathFromUri(uri)); - documents[uri] = doc; +void Server::operator()(const DidCloseTextDocumentNotification& notification) { + log << std::format("Did receive DidCloseTextDocumentNotification\n"); - std::cerr << std::format("Parsed document: {}, reported {} messages\n", uri, - doc->diagnosticsClient.messages.size()); - } + const auto uri = notification.params().textDocument().uri(); + documents_.erase(uri); +} - void operator()(const DidCloseTextDocumentNotification& notification) { - std::cerr << std::format("Did receive DidCloseTextDocumentNotification\n"); +void Server::operator()(const DidChangeTextDocumentNotification& notification) { + log << std::format("Did receive DidChangeTextDocumentNotification\n"); - const auto uri = notification.params().textDocument().uri(); - documents.erase(uri); - } + const auto textDocument = notification.params().textDocument(); + const auto uri = textDocument.uri(); + const auto version = textDocument.version(); - void operator()(const DidChangeTextDocumentNotification& notification) { - std::cerr << std::format("Did receive DidChangeTextDocumentNotification\n"); + // update the document + auto contentChanges = notification.params().contentChanges(); + const auto contentChangeCount = contentChanges.size(); - const auto textDocument = notification.params().textDocument(); - const auto uri = textDocument.uri(); - const auto version = textDocument.version(); + std::string text; - if (!documents.contains(uri)) { - std::cerr << std::format("Document not found: {}\n", uri); - return; - } + for (std::size_t i = 0; i < contentChangeCount; ++i) { + auto contentChange = contentChanges.at(i); - // update the document - auto contentChanges = notification.params().contentChanges(); - const std::size_t contentChangeCount = contentChanges.size(); - for (std::size_t i = 0; i < contentChangeCount; ++i) { - auto change = contentChanges.at(i); - if (std::holds_alternative( - change)) { - auto text = - std::get(change).text(); - - // parse the document - auto doc = std::make_shared(cli); - doc->parse(std::move(text), pathFromUri(uri)); - documents[uri] = doc; - - std::cerr << std::format("Parsed document: {}, reported {} messages\n", - uri, doc->diagnosticsClient.messages.size()); - } + if (auto change = std::get_if( + &contentChange)) { + text = change->text(); + } else { + lsp_runtime_error("Unsupported content change\n"); } } - [[nodiscard]] auto pathFromUri(const std::string& uri) -> std::string { - if (uri.starts_with("file://")) { - return uri.substr(7); - } - - lsp_runtime_error(std::format("Unsupported URI scheme: {}\n", uri)); - } - - // - // life cycle requests - // - - void operator()(const InitializeRequest& request) { - std::cerr << std::format("Did receive InitializeRequest\n"); - - withUnsafeJson([&](json storage) { - InitializeResult result(storage); - result.serverInfo().name("cxx-lsp").version(CXX_VERSION); - result.capabilities().textDocumentSync(TextDocumentSyncKind::kFull); - result.capabilities().diagnosticProvider().identifier( - "cxx-lsp"); - // .workspaceDiagnostics(true); - - sendToClient(result, request.id()); - }); - } + parse(textDocument.uri(), std::move(text), textDocument.version()); +} - void operator()(const ShutdownRequest& request) { - std::cerr << std::format("Did receive ShutdownRequest\n"); +auto Server::latestDocument(const std::string& uri) + -> std::shared_ptr { +#ifndef CXX_NO_THREADS + auto lock = std::unique_lock(documentsMutex_); +#endif - withUnsafeJson([&](json storage) { - LSPObject result(storage); - sendToClient(result, request.id()); - }); + if (!documents_.contains(uri)) { + return {}; } - void operator()(const DocumentDiagnosticRequest& request) { - std::cerr << std::format("Did receive DocumentDiagnosticRequest\n"); - - auto textDocument = request.params().textDocument(); - auto uri = textDocument.uri(); - - if (!documents.contains(uri)) { - std::cerr << std::format("Document not found: {}\n", uri); - return; - } - - auto doc = documents[uri]; - - withUnsafeJson([&](json storage) { - FullDocumentDiagnosticReport report(storage); - - auto diagnostics = Vector(doc->diagnosticsClient.messages); - report.items(diagnostics); - - // TODO: string literals in C++ LSP API - storage["kind"] = "full"; + return documents_[uri]; +} - // TODO: responses in C++ LSP API - json response; - response["jsonrpc"] = "2.0"; - response["id"] = std::get(*request.id()); - response["result"] = report; - sendToClient(response); - }); - } +void Server::operator()(const DocumentDiagnosticRequest& request) { + log << std::format("Did receive DocumentDiagnosticRequest\n"); +} - // - // Other requests - // - void operator()(const LSPRequest& request) { - std::cerr << "Request: " << request.method() << "\n"; +void Server::operator()(const CancelNotification& notification) { + auto id = notification.params().id(); + log << std::format("Did receive CancelNotification for request with id {}\n", + id); +} - if (!request.id().has_value()) { - // nothing to do for notifications - return; - } +void Server::operator()(const LSPRequest& request) { + log << std::format("Did receive LSPRequest {}\n", request.method()); - // send an empty response. - withUnsafeJson([&](json storage) { - LSPObject result(storage); - sendToClient(result, request.id()); - }); + if (!request.id().has_value()) { + // nothing to do for notifications + return; } -}; -int startServer(const CLI& cli) { - Server server{cli}; - auto exitCode = server.start(); - return exitCode; + // send an empty response. + withUnsafeJson([&](json storage) { + LSPObject result(storage); + sendToClient(result, request.id()); + }); } } // namespace cxx::lsp diff --git a/src/frontend/cxx/lsp_server.h b/src/frontend/cxx/lsp_server.h index 82b2fa5c..680f4518 100644 --- a/src/frontend/cxx/lsp_server.h +++ b/src/frontend/cxx/lsp_server.h @@ -20,10 +20,86 @@ #pragma once +#include + +#include +#include + +#ifndef CXX_NO_THREADS +#include +#include +#endif + +#include + #include "cli.h" +#include "sync_queue.h" namespace cxx::lsp { -int startServer(const CLI& cli); +class CxxDocument; + +class Server { + public: + Server(const CLI& cli); + ~Server(); + + auto start() -> int; + + void operator()(const InitializeRequest& request); + void operator()(const InitializedNotification& notification); + + void operator()(const ShutdownRequest& request); + void operator()(const ExitNotification& notification); + + void operator()(const DidOpenTextDocumentNotification& notification); + void operator()(const DidCloseTextDocumentNotification& notification); + void operator()(const DidChangeTextDocumentNotification& notification); + + void operator()(const DocumentDiagnosticRequest& request); + + void operator()(const CancelNotification& notification); + void operator()(const LSPRequest& request); + + private: + void startWorkersIfNeeded(); + void stopWorkersIfNeeded(); + + void run(std::function task); + + void parse(std::string uri, std::string text, long version); + + [[nodiscard]] auto latestDocument(const std::string& uri) + -> std::shared_ptr; + + [[nodiscard]] auto nextRequest() -> std::optional; + + void sendNotification(const LSPRequest& notification); + + void sendToClient(const json& message); + + void sendToClient( + const LSPObject& result, + std::optional> id = std::nullopt); + + [[nodiscard]] auto pathFromUri(const std::string& uri) -> std::string; + + [[nodiscard]] auto readHeaders(std::istream& input) + -> std::unordered_map; + + private: + const CLI& cli; + std::istream& input; + std::ostream& output; + std::ostream& log; + std::unordered_map> documents_; +#ifndef CXX_NO_THREADS + SyncQueue syncQueue_; + std::vector workers_; + std::mutex documentsMutex_; + std::mutex outputMutex_; +#endif + bool done_ = false; +}; -} +} // namespace cxx::lsp diff --git a/src/frontend/cxx/sync_queue.cc b/src/frontend/cxx/sync_queue.cc new file mode 100644 index 00000000..356029d7 --- /dev/null +++ b/src/frontend/cxx/sync_queue.cc @@ -0,0 +1,57 @@ +// Copyright (c) 2024 Roberto Raggi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "sync_queue.h" + +#ifndef CXX_NO_THREADS + +namespace cxx::lsp { + +auto SyncQueue::closed() -> bool { + std::unique_lock lock(m_mutex); + return m_closed; +} + +void SyncQueue::close() { + std::unique_lock lock(m_mutex); + m_closed = true; + m_cv.notify_all(); +} + +void SyncQueue::push(std::function task) { + std::unique_lock lock(m_mutex); + m_queue.push_back(std::move(task)); + m_cv.notify_one(); +} + +auto SyncQueue::pop() -> std::function { + std::unique_lock lock(m_mutex); + + m_cv.wait(lock, [this] { return !m_queue.empty(); }); + + auto message = m_queue.front(); + m_queue.pop_front(); + + return message; +} + +} // namespace cxx::lsp + +#endif \ No newline at end of file diff --git a/src/frontend/cxx/sync_queue.h b/src/frontend/cxx/sync_queue.h new file mode 100644 index 00000000..3032bc60 --- /dev/null +++ b/src/frontend/cxx/sync_queue.h @@ -0,0 +1,52 @@ +// Copyright (c) 2024 Roberto Raggi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#ifndef CXX_NO_THREADS + +#include +#include +#include +#include + +namespace cxx::lsp { + +class SyncQueue { + public: + SyncQueue() = default; + ~SyncQueue() = default; + + [[nodiscard]] auto closed() -> bool; + [[nodiscard]] auto pop() -> std::function; + + void push(std::function task); + void close(); + + private: + std::mutex m_mutex; + std::condition_variable m_cv; + std::deque> m_queue; + bool m_closed = false; +}; + +} // namespace cxx::lsp + +#endif