diff --git a/src/source_language_protocol.cpp b/src/source_language_protocol.cpp index 748892e..bd6ee53 100644 --- a/src/source_language_protocol.cpp +++ b/src/source_language_protocol.cpp @@ -119,6 +119,9 @@ LanguageProtocol::Client::~Client() { process->kill(); exit_status = process->get_exit_status(); } + + if(on_exit_status) + on_exit_status(exit_status); if(Config::get().log.language_server) std::cout << "Language server exit status: " << exit_status << std::endl; } diff --git a/src/source_language_protocol.hpp b/src/source_language_protocol.hpp index b4638c9..c581a8a 100644 --- a/src/source_language_protocol.hpp +++ b/src/source_language_protocol.hpp @@ -155,6 +155,8 @@ namespace LanguageProtocol { void write_notification(const std::string &method, const std::string ¶ms); void handle_server_notification(const std::string &method, const boost::property_tree::ptree ¶ms); void handle_server_request(size_t id, const std::string &method, const boost::property_tree::ptree ¶ms); + + std::function on_exit_status; }; } // namespace LanguageProtocol diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9d1c817..c07a884 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -84,6 +84,12 @@ if(BUILD_TESTING) add_executable(utility_test utility_test.cpp $) target_link_libraries(utility_test juci_shared) add_test(utility_test utility_test) + + add_executable(language_protocol_client_test language_protocol_client_test.cpp $) + target_link_libraries(language_protocol_client_test juci_shared) + add_test(language_protocol_client_test language_protocol_client_test) + + add_executable(language_protocol_server_test language_protocol_server_test.cpp) endif() if(BUILD_FUZZING) diff --git a/tests/language_protocol_client_test.cpp b/tests/language_protocol_client_test.cpp new file mode 100644 index 0000000..a5bb7dc --- /dev/null +++ b/tests/language_protocol_client_test.cpp @@ -0,0 +1,55 @@ +#include "config.hpp" +#include "source_language_protocol.hpp" +#include + +//Requires display server to work +//However, it is possible to use the Broadway backend if the test is run in a pure terminal environment: +//broadwayd& +//make test + +void flush_events() { + while(Gtk::Main::events_pending()) + Gtk::Main::iteration(); +} + +int main() { + auto app = Gtk::Application::create(); + Gsv::init(); + + auto tests_path = boost::filesystem::canonical(JUCI_TESTS_PATH); + auto build_path = boost::filesystem::canonical(JUCI_BUILD_PATH); + + auto view = new Source::LanguageProtocolView(boost::filesystem::canonical(tests_path / "language_protocol_test_files" / "main.rs"), + Source::LanguageManager::get_default()->get_language("rust"), + "rust", + (build_path / "tests" / "language_protocol_server_test").string()); + + while(!view->initialized) + flush_events(); + + g_assert(view->capabilities.document_formatting); + + view->get_buffer()->insert_at_cursor(" "); + g_assert(view->get_buffer()->get_text() == R"( fn main() { + println!("Hello, world!"); +} +)"); + + view->format_style(false); + g_assert(view->get_buffer()->get_text() == R"(fn main() { + println!("Hello, world!"); +} +)"); + + std::atomic exit_status(-1); + view->client->on_exit_status = [&exit_status](int exit_status_) { + exit_status = exit_status_; + }; + + delete view; + + while(exit_status == -1) + flush_events(); + + g_assert_cmpint(exit_status, ==, 0); +} diff --git a/tests/language_protocol_server_test.cpp b/tests/language_protocol_server_test.cpp new file mode 100644 index 0000000..98d04de --- /dev/null +++ b/tests/language_protocol_server_test.cpp @@ -0,0 +1,359 @@ +#include +#include + +int main() { + std::string line; + try { + // Read initialize and respond + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "initialize") + return 1; + + std::string result = R"({ + "jsonrpc": "2.0", + "id": "0", + "result": { + "capabilities": { + "textDocumentSync": { + "openClose": "true", + "change": "2", + "save": "" + }, + "selectionRangeProvider": "true", + "hoverProvider": "true", + "completionProvider": { + "triggerCharacters": [ + ":", + ".", + "'" + ] + }, + "signatureHelpProvider": { + "triggerCharacters": [ + "(", + "," + ] + }, + "definitionProvider": "true", + "typeDefinitionProvider": "true", + "implementationProvider": "true", + "referencesProvider": "true", + "documentHighlightProvider": "true", + "documentSymbolProvider": "true", + "workspaceSymbolProvider": "true", + "codeActionProvider": { + "codeActionKinds": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite" + ], + "resolveProvider": "true" + }, + "codeLensProvider": { + "resolveProvider": "true" + }, + "documentFormattingProvider": "true", + "documentOnTypeFormattingProvider": { + "firstTriggerCharacter": "=", + "moreTriggerCharacter": [ + ".", + ">", + "{" + ] + }, + "renameProvider": { + "prepareProvider": "true" + }, + "foldingRangeProvider": "true", + "workspace": { + "fileOperations": { + "willRename": { + "filters": [ + { + "scheme": "file", + "pattern": { + "glob": "**\/*.rs", + "matches": "file" + } + }, + { + "scheme": "file", + "pattern": { + "glob": "**", + "matches": "folder" + } + } + ] + } + } + }, + "callHierarchyProvider": "true", + "semanticTokensProvider": { + "legend": { + "tokenTypes": [ + "comment", + "keyword", + "string", + "number", + "regexp", + "operator", + "namespace", + "type", + "struct", + "class", + "interface", + "enum", + "enumMember", + "typeParameter", + "function", + "method", + "property", + "macro", + "variable", + "parameter", + "angle", + "arithmetic", + "attribute", + "bitwise", + "boolean", + "brace", + "bracket", + "builtinType", + "characterLiteral", + "colon", + "comma", + "comparison", + "constParameter", + "dot", + "escapeSequence", + "formatSpecifier", + "generic", + "label", + "lifetime", + "logical", + "operator", + "parenthesis", + "punctuation", + "selfKeyword", + "semicolon", + "typeAlias", + "union", + "unresolvedReference" + ], + "tokenModifiers": [ + "documentation", + "declaration", + "definition", + "static", + "abstract", + "deprecated", + "readonly", + "constant", + "controlFlow", + "injected", + "mutable", + "consuming", + "async", + "unsafe", + "attribute", + "trait", + "callable", + "intraDocLink" + ] + }, + "range": "true", + "full": { + "delta": "true" + } + }, + "experimental": { + "joinLines": "true", + "ssr": "true", + "onEnter": "true", + "parentModule": "true", + "runnables": { + "kinds": [ + "cargo" + ] + }, + "workspaceSymbolScopeKindFiltering": "true" + } + }, + "serverInfo": { + "name": "rust-analyzer", + "version": "3022a2c3a 2021-05-25 dev" + }, + "offsetEncoding": "utf-8" + } +})"; + std::cout << "Content-Length: " << result.size() << "\r\n\r\n" + << result; + } + + // Read initialized + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "initialized") + return 2; + } + + // Read textDocument/didOpen + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "textDocument/didOpen") + return 3; + } + + // Read textDocument/didChange + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "textDocument/didChange") + return 4; + } + + // Read and write textDocument/formatting + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "textDocument/formatting") + return 5; + + std::string result = R"({ + "jsonrpc": "2.0", + "id": "1", + "result": [ + { + "range": { + "start": { + "line": "0", + "character": "0" + }, + "end": { + "line": "0", + "character": "1" + } + }, + "newText": "" + } + ] +})"; + std::cout << "Content-Length: " << result.size() << "\r\n\r\n" + << result; + } + + // Read textDocument/didChange + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "textDocument/didChange") + return 6; + } + + // Read textDocument/didClose + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "textDocument/didClose") + return 7; + } + + // Read shutdown and respond + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "shutdown") + return 8; + + std::string result = R"({ + "jsonrpc": "2.0", + "id": "2", + "result": {} +})"; + std::cout << "Content-Length: " << result.size() << "\r\n\r\n" + << result; + } + + // Read exit + { + std::getline(std::cin, line); + auto size = std::atoi(line.substr(16).c_str()); + std::getline(std::cin, line); + std::string buffer; + buffer.resize(size); + std::cin.read(&buffer[0], size); + std::stringstream ss(buffer); + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(ss, pt); + if(pt.get("method") != "exit") + return 9; + } + } + catch(const std::exception &e) { + std::cerr << e.what() << std::endl; + return 100; + } +} diff --git a/tests/language_protocol_test_files/.rustfmt.toml b/tests/language_protocol_test_files/.rustfmt.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/language_protocol_test_files/main.rs b/tests/language_protocol_test_files/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/tests/language_protocol_test_files/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/tests/stubs/notebook.cpp b/tests/stubs/notebook.cpp index 8957310..5fe3f79 100644 --- a/tests/stubs/notebook.cpp +++ b/tests/stubs/notebook.cpp @@ -8,4 +8,8 @@ Source::View *Notebook::get_current_view() { bool Notebook::open(const boost::filesystem::path &file_path, Position position) { return true; } +bool Notebook::open(Source::View *view) { return true; } + void Notebook::open_uri(const std::string &uri) {} + +bool Notebook::close(Source::View *view) { return true; }