module; export module lsp.test.provider.interpreter; import std; import lsp.codec.facade; import lsp.core.dispacther; import lsp.manager.manager_hub; import lsp.protocol; import lsp.provider.completion_item.resolve; import lsp.provider.text_document.completion; import lsp.scheduler.async_executor; import lsp.test.framework; import lsp.test.provider.fixtures; export namespace lsp::test::provider { class InterpreterTests { public: static void Register(TestRunner& runner); private: static TestResult TestInterpreterSystemLibraryCompletionResolve(); }; } namespace lsp::test::provider { namespace { namespace codec = lsp::codec; struct ProviderEnv { scheduler::AsyncExecutor scheduler{ 4 }; manager::ManagerHub hub{}; core::ExecutionContext context; ProviderEnv() : context([](core::ServerLifecycleEvent) {}, scheduler, hub) { hub.Initialize(); } }; protocol::ResponseMessage ParseResponse(const std::string& json) { auto parsed = codec::Deserialize(json); assertTrue(parsed.has_value(), "Failed to deserialize response JSON"); return parsed.value(); } protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker) { auto pos = content.find(marker); assertTrue(pos != std::string::npos, "Marker not found in test content"); protocol::Position result{}; for (std::size_t i = 0; i < pos; ++i) { if (content[i] == '\n') { result.line++; result.character = 0; } else { result.character++; } } if (after_marker) { result.character += static_cast(marker.size()); } return result; } std::filesystem::path ExpandUserPath(const std::string& value) { std::filesystem::path path = value; if (!value.empty() && value[0] == '~') { const char* home = std::getenv("HOME"); assertTrue(home != nullptr, "HOME is not set; cannot expand '~'"); if (value.size() == 1) { path = home; } else if (value[1] == '/') { path = std::filesystem::path(home) / value.substr(2); } } return path; } std::size_t CountTsfFiles(const std::filesystem::path& root) { std::size_t count = 0; auto options = std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied; for (const auto& entry : std::filesystem::recursive_directory_iterator(root, options)) { if (!entry.is_regular_file()) { continue; } auto ext = entry.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); if (ext == ".tsf") { ++count; } } return count; } } void InterpreterTests::Register(TestRunner& runner) { runner.addTest("interpreter system library completion/resolve", TestInterpreterSystemLibraryCompletionResolve); } TestResult InterpreterTests::TestInterpreterSystemLibraryCompletionResolve() { TestResult result{ "", true, "ok" }; const auto& interpreter_root = InterpreterPath(); if (interpreter_root.empty()) { return result; } auto interpreter_path = ExpandUserPath(interpreter_root); auto funcext_path = interpreter_path / "funcext"; assertTrue(std::filesystem::exists(funcext_path), "Interpreter funcext path does not exist: " + funcext_path.string()); assertTrue(std::filesystem::is_directory(funcext_path), "Interpreter funcext path is not a directory: " + funcext_path.string()); const auto expected_tsf_count = CountTsfFiles(funcext_path); ProviderEnv env; env.hub.symbols().LoadSystemLibrary(funcext_path.string()); const auto system_tables = env.hub.symbols().GetSystemSymbolTables(); assertEqual(expected_tsf_count, system_tables.size(), "System library should index all .tsf files under funcext"); auto nonce = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); auto doc_path = std::filesystem::temp_directory_path() / ("tsl_interpreter_completion_" + nonce + ".tsl"); auto uri = ToUri(doc_path); std::string content = "unit InterpreterCompletion;\n" "begin\n" " m := new PageHel\n" "end;\n"; protocol::DidOpenTextDocumentParams open_params; open_params.textDocument.uri = uri; open_params.textDocument.languageId = "tsl"; open_params.textDocument.version = 1; open_params.textDocument.text = content; env.hub.documents().OpenDocument(open_params); protocol::CompletionParams completion_params; completion_params.textDocument.uri = uri; completion_params.position = FindPosition(content, "new PageHel", true); protocol::RequestMessage completion_request; completion_request.id = "c1"; completion_request.method = "textDocument/completion"; completion_request.params = codec::ToLSPAny(completion_params); ::lsp::provider::text_document::Completion completion_provider; auto completion_response_json = completion_provider.ProvideResponse(completion_request, env.context); auto completion_response = ParseResponse(completion_response_json); assertTrue(completion_response.result.has_value(), "Completion should return result"); auto list = codec::FromLSPAny.template operator()(*completion_response.result); auto item_it = std::find_if(list.items.begin(), list.items.end(), [](const auto& item) { return item.label == "PageHelperFreeMutex"; }); assertTrue(item_it != list.items.end(), "Completion should include PageHelperFreeMutex from system library"); protocol::RequestMessage resolve_request; resolve_request.id = "r1"; resolve_request.method = "completionItem/resolve"; resolve_request.params = codec::ToLSPAny(*item_it); ::lsp::provider::completion_item::Resolve resolve_provider; auto resolve_response_json = resolve_provider.ProvideResponse(resolve_request, env.context); auto resolve_response = ParseResponse(resolve_response_json); assertTrue(resolve_response.result.has_value(), "Resolve should return result"); auto resolved = codec::FromLSPAny.template operator()(*resolve_response.result); assertTrue(resolved.insertText.has_value(), "Resolved completion item should include insertText snippet"); assertTrue(resolved.insertText->contains("PageHelperFreeMutex("), "Resolved snippet should include class name"); assertTrue(resolved.insertText->contains("${1:"), "Resolved snippet should include parameter placeholder"); return result; } }