diff --git a/packages/cxx-gen-lsp/src/gen_fwd_h.ts b/packages/cxx-gen-lsp/src/gen_fwd_h.ts index ce952e52..f5849bc6 100644 --- a/packages/cxx-gen-lsp/src/gen_fwd_h.ts +++ b/packages/cxx-gen-lsp/src/gen_fwd_h.ts @@ -24,11 +24,15 @@ import { writeFileSync } from "node:fs"; import { copyrightHeader } from "./copyrightHeader.js"; const fragment = ` -[[noreturn]] void lsp_runtime_error(const std::string& msg); + +class LSPObject; using LSPAny = json; using Pattern = std::string; +[[nodiscard]] auto withUnsafeJson(auto block) { return block(json()); } +[[noreturn]] void lsp_runtime_error(const std::string& msg); + class LSPObject { public: LSPObject() = default; diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index ead7ab4c..12332909 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -23,6 +23,10 @@ add_executable(cxx ${SOURCES}) target_link_libraries(cxx cxx-parser cxx-lsp) +target_compile_definitions(cxx PRIVATE + CXX_VERSION="${CMAKE_PROJECT_VERSION}" +) + if(EMSCRIPTEN) target_link_options(cxx PUBLIC "SHELL:-s EXIT_RUNTIME=1" diff --git a/src/frontend/cxx/lsp_server.cc b/src/frontend/cxx/lsp_server.cc index 481e2237..7f98620f 100644 --- a/src/frontend/cxx/lsp_server.cc +++ b/src/frontend/cxx/lsp_server.cc @@ -41,6 +41,7 @@ #include #include #include +#include #include namespace cxx::lsp { @@ -106,7 +107,7 @@ auto readHeaders(std::istream& input) -> Headers { return headers; } -struct Input { +struct CxxDocument { struct Diagnostics final : cxx::DiagnosticsClient { json messages = json::array(); Vector diagnostics{messages}; @@ -146,7 +147,7 @@ struct Input { TranslationUnit unit; std::unique_ptr toolchain; - Input(const CLI& cli) : cli(cli), unit(&control, &diagnosticsClient) {} + CxxDocument(const CLI& cli) : cli(cli), unit(&control, &diagnosticsClient) {} void parse(std::string source, std::string fileName) { configure(); @@ -273,10 +274,12 @@ struct Input { }; class Server { + const CLI& cli; std::istream& input; + std::unordered_map> documents; public: - Server() : input(std::cin) {} + Server(const CLI& cli) : cli(cli), input(std::cin) {} auto start() -> int { while (input.good()) { @@ -353,14 +356,66 @@ class Server { void operator()(const DidOpenTextDocumentNotification& notification) { std::cerr << 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 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()); } void operator()(const DidCloseTextDocumentNotification& notification) { std::cerr << std::format("Did receive DidCloseTextDocumentNotification\n"); + + const auto uri = notification.params().textDocument().uri(); + documents.erase(uri); } void operator()(const DidChangeTextDocumentNotification& notification) { std::cerr << std::format("Did receive DidChangeTextDocumentNotification\n"); + + const auto textDocument = notification.params().textDocument(); + const auto uri = textDocument.uri(); + const auto version = textDocument.version(); + + if (!documents.contains(uri)) { + std::cerr << std::format("Document not found: {}\n", uri); + return; + } + + // 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()); + } + } + } + + [[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)); } // @@ -369,21 +424,57 @@ class Server { void operator()(const InitializeRequest& request) { std::cerr << std::format("Did receive InitializeRequest\n"); - auto storage = json::object(); - InitializeResult result(storage); - result.serverInfo().name("cxx-lsp").version("0.0.1"); - result.capabilities().textDocumentSync(TextDocumentSyncKind::kFull); - sendToClient(result, request.id()); + 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()); + }); } void operator()(const ShutdownRequest& request) { std::cerr << std::format("Did receive ShutdownRequest\n"); - json storage; - LSPObject result(storage); + withUnsafeJson([&](json storage) { + LSPObject result(storage); + sendToClient(result, request.id()); + }); + } + + 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]; - sendToClient(result, request.id()); + 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"; + + // TODO: responses in C++ LSP API + json response; + response["jsonrpc"] = "2.0"; + response["id"] = std::get(*request.id()); + response["result"] = report; + sendToClient(response); + }); } // @@ -392,17 +483,21 @@ class Server { void operator()(const LSPRequest& request) { std::cerr << "Request: " << request.method() << "\n"; - if (request.id().has_value()) { - // send an empty response. - json storage; + if (!request.id().has_value()) { + // nothing to do for notifications + return; + } + + // send an empty response. + withUnsafeJson([&](json storage) { LSPObject result(storage); sendToClient(result, request.id()); - } + }); } }; int startServer(const CLI& cli) { - Server server; + Server server{cli}; auto exitCode = server.start(); return exitCode; } diff --git a/src/lsp/cxx/lsp/fwd.h b/src/lsp/cxx/lsp/fwd.h index bc6ec298..36a9aa6f 100644 --- a/src/lsp/cxx/lsp/fwd.h +++ b/src/lsp/cxx/lsp/fwd.h @@ -623,11 +623,14 @@ class LogTraceNotification; class CancelNotification; class ProgressNotification; -[[noreturn]] void lsp_runtime_error(const std::string& msg); +class LSPObject; using LSPAny = json; using Pattern = std::string; +[[nodiscard]] auto withUnsafeJson(auto block) { return block(json()); } +[[noreturn]] void lsp_runtime_error(const std::string& msg); + class LSPObject { public: LSPObject() = default; diff --git a/src/lsp/tests/test_types.cc b/src/lsp/tests/test_types.cc index a02693c9..a554be44 100644 --- a/src/lsp/tests/test_types.cc +++ b/src/lsp/tests/test_types.cc @@ -9,155 +9,154 @@ using namespace cxx::lsp; TEST(LSP, Initialization) { - // storage - auto storage = json::object(); + withUnsafeJson([](json storage) { + InitializeResult initializeResult{storage}; + ASSERT_TRUE(!initializeResult); - InitializeResult initializeResult{storage}; - ASSERT_TRUE(!initializeResult); + auto serverInfo = + initializeResult.serverInfo().name("cxx").version("0.1.0"); - auto serverInfo = - initializeResult.serverInfo().name("cxx").version("0.1.0"); + ASSERT_TRUE(initializeResult.serverInfo().has_value()); + ASSERT_EQ(serverInfo.name(), "cxx"); + ASSERT_EQ(serverInfo.version(), "0.1.0"); - ASSERT_TRUE(initializeResult.serverInfo().has_value()); - ASSERT_EQ(serverInfo.name(), "cxx"); - ASSERT_EQ(serverInfo.version(), "0.1.0"); + auto capabilities = initializeResult.capabilities().textDocumentSync( + TextDocumentSyncKind::kIncremental); - auto capabilities = initializeResult.capabilities().textDocumentSync( - TextDocumentSyncKind::kIncremental); + ASSERT_TRUE(capabilities); - ASSERT_TRUE(capabilities); + ASSERT_TRUE(capabilities.textDocumentSync().has_value()); - ASSERT_TRUE(capabilities.textDocumentSync().has_value()); + ASSERT_TRUE(std::holds_alternative( + *capabilities.textDocumentSync())); - ASSERT_TRUE(std::holds_alternative( - *capabilities.textDocumentSync())); + ASSERT_EQ(std::get(*capabilities.textDocumentSync()), + TextDocumentSyncKind::kIncremental); - ASSERT_EQ(std::get(*capabilities.textDocumentSync()), - TextDocumentSyncKind::kIncremental); + capabilities.completionProvider().triggerCharacters( + std::vector{".", ">", ":"}); - capabilities.completionProvider().triggerCharacters( - std::vector{".", ">", ":"}); + ASSERT_TRUE(capabilities.completionProvider().has_value()); - ASSERT_TRUE(capabilities.completionProvider().has_value()); + auto completionProvider = + capabilities.completionProvider(); - auto completionProvider = - capabilities.completionProvider(); + ASSERT_TRUE(completionProvider.triggerCharacters().has_value()); - ASSERT_TRUE(completionProvider.triggerCharacters().has_value()); + auto triggerCharacters = *completionProvider.triggerCharacters(); - auto triggerCharacters = *completionProvider.triggerCharacters(); + ASSERT_EQ(triggerCharacters.size(), 3); + ASSERT_EQ(triggerCharacters.at(0), "."); + ASSERT_EQ(triggerCharacters.at(1), ">"); + ASSERT_EQ(triggerCharacters.at(2), ":"); - ASSERT_EQ(triggerCharacters.size(), 3); - ASSERT_EQ(triggerCharacters.at(0), "."); - ASSERT_EQ(triggerCharacters.at(1), ">"); - ASSERT_EQ(triggerCharacters.at(2), ":"); + ASSERT_TRUE(initializeResult.serverInfo()); + ASSERT_TRUE(initializeResult.capabilities()); - ASSERT_TRUE(initializeResult.serverInfo()); - ASSERT_TRUE(initializeResult.capabilities()); - - ASSERT_TRUE(initializeResult); + ASSERT_TRUE(initializeResult); + }); } TEST(LSP, ArrayProperty) { - json storage = json::object(); - - ConfigurationParams configurationParams{storage}; + withUnsafeJson([](auto storage) { + auto configurationParams = ConfigurationParams{storage}; + ASSERT_FALSE(configurationParams); - ASSERT_FALSE(configurationParams); + auto items = configurationParams.items(); - auto items = configurationParams.items(); + ASSERT_TRUE(items.empty()); - ASSERT_TRUE(items.empty()); - - ASSERT_TRUE(configurationParams); + ASSERT_TRUE(configurationParams); + }); } TEST(LSP, MapProperty) { - json storage = json::object(); - - DocumentDiagnosticReportPartialResult documentDiagnosticReportPartialResult{ - storage}; + withUnsafeJson([](json storage) { + DocumentDiagnosticReportPartialResult documentDiagnosticReportPartialResult{ + storage}; - ASSERT_FALSE(documentDiagnosticReportPartialResult); + ASSERT_FALSE(documentDiagnosticReportPartialResult); - auto relatedDocuments = - documentDiagnosticReportPartialResult.relatedDocuments(); + auto relatedDocuments = + documentDiagnosticReportPartialResult.relatedDocuments(); - ASSERT_TRUE(relatedDocuments.empty()); + ASSERT_TRUE(relatedDocuments.empty()); - ASSERT_TRUE(documentDiagnosticReportPartialResult); + ASSERT_TRUE(documentDiagnosticReportPartialResult); + }); } TEST(LSP, StringProperty) { - json storage = json::object(); + withUnsafeJson([](json storage) { + Location location{storage}; - Location location{storage}; + ASSERT_FALSE(location); - ASSERT_FALSE(location); + ASSERT_EQ(location.uri(), ""); - ASSERT_EQ(location.uri(), ""); + location.uri("file:///path/to/file"); + ASSERT_EQ(location.uri(), "file:///path/to/file"); - location.uri("file:///path/to/file"); - ASSERT_EQ(location.uri(), "file:///path/to/file"); + auto range = location.range(); + ASSERT_EQ(range.start().line(), 0); + ASSERT_EQ(range.start().character(), 0); - auto range = location.range(); - ASSERT_EQ(range.start().line(), 0); - ASSERT_EQ(range.start().character(), 0); - - range.start().line(1); - range.start().character(2); - range.end().line(3); - range.end().character(4); + range.start().line(1); + range.start().character(2); + range.end().line(3); + range.end().character(4); - ASSERT_EQ(range.start().line(), 1); - ASSERT_EQ(range.start().character(), 2); - ASSERT_EQ(range.end().line(), 3); - ASSERT_EQ(range.end().character(), 4); + ASSERT_EQ(range.start().line(), 1); + ASSERT_EQ(range.start().character(), 2); + ASSERT_EQ(range.end().line(), 3); + ASSERT_EQ(range.end().character(), 4); - ASSERT_TRUE(location); + ASSERT_TRUE(location); + }); } TEST(LSP, StringArrayProperty) { - json storage = json::object(); + withUnsafeJson([](json storage) { + auto textDocumentContentRegistrationOptions = + TextDocumentContentRegistrationOptions{storage}; - auto textDocumentContentRegistrationOptions = - TextDocumentContentRegistrationOptions{storage}; + auto schemas = textDocumentContentRegistrationOptions.schemes(); + ASSERT_TRUE(schemas.empty()); - auto schemas = textDocumentContentRegistrationOptions.schemes(); - ASSERT_TRUE(schemas.empty()); + schemas.emplace_back("file"); + schemas.emplace_back("http"); - schemas.emplace_back("file"); - schemas.emplace_back("http"); + ASSERT_EQ(schemas.at(0), "file"); + ASSERT_EQ(schemas.at(1), "http"); - ASSERT_EQ(schemas.at(0), "file"); - ASSERT_EQ(schemas.at(1), "http"); - - ASSERT_EQ(textDocumentContentRegistrationOptions.schemes().at(0), "file"); - ASSERT_EQ(textDocumentContentRegistrationOptions.schemes().at(1), "http"); + ASSERT_EQ(textDocumentContentRegistrationOptions.schemes().at(0), "file"); + ASSERT_EQ(textDocumentContentRegistrationOptions.schemes().at(1), "http"); + }); } TEST(LSP, VariantArrayProperty) { - auto storage = json::object(); - - NotebookDocumentSyncRegistrationOptions - notebookDocumentSyncRegistrationOptions{storage}; + withUnsafeJson([](json storage) { + NotebookDocumentSyncRegistrationOptions + notebookDocumentSyncRegistrationOptions{storage}; - auto notebookSelector = - notebookDocumentSyncRegistrationOptions.notebookSelector(); + auto notebookSelector = + notebookDocumentSyncRegistrationOptions.notebookSelector(); - ASSERT_TRUE(notebookSelector.empty()); + ASSERT_TRUE(notebookSelector.empty()); - auto item = notebookSelector.emplace_back(); + auto item = + notebookSelector.emplace_back(); - ASSERT_FALSE(item.notebook().has_value()); + ASSERT_FALSE(item.notebook().has_value()); - item.notebook("a_notebook"); + item.notebook("a_notebook"); - ASSERT_TRUE(item.notebook().has_value()); + ASSERT_TRUE(item.notebook().has_value()); - ASSERT_EQ(std::get(*item.notebook()), "a_notebook"); + ASSERT_EQ(std::get(*item.notebook()), "a_notebook"); - ASSERT_EQ(notebookSelector.size(), 1); + ASSERT_EQ(notebookSelector.size(), 1); + }); } TEST(LSP, CompletionList) { @@ -228,32 +227,30 @@ TEST(LSP, CompletionList) { } TEST(LSP, CreateCompletionList) { - auto storage = json::object(); - - CompletionList completionList{storage}; + withUnsafeJson([](json storage) { + CompletionList completionList{storage}; - completionList.isIncomplete(false); + completionList.isIncomplete(false); - auto item = completionList.items().emplace_back(); - item.filterText("include"); - item.insertText("include \"$0\""); - item.insertTextFormat(InsertTextFormat::kSnippet); - item.kind(CompletionItemKind::kSnippet); - item.label(" include"); + auto item = completionList.items().emplace_back(); + item.filterText("include"); + item.insertText("include \"$0\""); + item.insertTextFormat(InsertTextFormat::kSnippet); + item.kind(CompletionItemKind::kSnippet); + item.label(" include"); - item.labelDetails().detail(" \"header\""); - item.sortText("000000001"); + item.labelDetails().detail(" \"header\""); + item.sortText("000000001"); - auto textEdit = item.textEdit(); + auto textEdit = item.textEdit(); - textEdit.newText("include \"$0\""); - auto range = textEdit.range(); - range.start().line(0); - range.start().character(1); - range.end().line(0); - range.end().character(4); - - ASSERT_TRUE(completionList); + textEdit.newText("include \"$0\""); + auto range = textEdit.range(); + range.start().line(0); + range.start().character(1); + range.end().line(0); + range.end().character(4); - // std::cout << completionList.get().dump(2) << std::endl; + ASSERT_TRUE(completionList); + }); } \ No newline at end of file