diff --git a/lsp-server/src/manager/symbol.cppm b/lsp-server/src/manager/symbol.cppm index 7b988f1..9192670 100644 --- a/lsp-server/src/manager/symbol.cppm +++ b/lsp-server/src/manager/symbol.cppm @@ -348,12 +348,10 @@ namespace lsp::manager auto stem = entry.path().stem().string(); if (!HasMatchingTopLevelSymbol(*table, stem)) { - spdlog::warn("Skipping system file {}: top-level symbol does not match file name (stem='{}', top-level={})", - entry.path().string(), - stem, - DescribeTopLevelSymbols(*table)); - ++failed; - continue; + spdlog::debug("Indexing system file {} with unmatched top-level symbol (stem='{}', top-level={})", + entry.path().string(), + stem, + DescribeTopLevelSymbols(*table)); } StoredSymbolEntry stored; diff --git a/lsp-server/test/test_provider/CMakeLists.txt b/lsp-server/test/test_provider/CMakeLists.txt index bcd4df1..0b0c997 100644 --- a/lsp-server/test/test_provider/CMakeLists.txt +++ b/lsp-server/test/test_provider/CMakeLists.txt @@ -25,6 +25,7 @@ set(SOURCES json_flow_test.cppm json_provider_coverage_test.cppm definitions_test.cppm + interpreter_test.cppm provider_misc_test.cppm provider_surface_test.cppm ../../src/tree-sitter/parser.c @@ -52,6 +53,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/json_provider_coverage_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/definitions_test.cppm + ${CMAKE_CURRENT_SOURCE_DIR}/interpreter_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/provider_misc_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm ../../src/bridge/glaze.cppm diff --git a/lsp-server/test/test_provider/fixtures.cppm b/lsp-server/test/test_provider/fixtures.cppm index 2e33df0..2fe7874 100644 --- a/lsp-server/test/test_provider/fixtures.cppm +++ b/lsp-server/test/test_provider/fixtures.cppm @@ -22,6 +22,22 @@ export namespace lsp::test::provider return ExecutablePathStorage(); } + inline std::string& InterpreterPathStorage() + { + static std::string value; + return value; + } + + inline void SetInterpreterPath(std::string value) + { + InterpreterPathStorage() = std::move(value); + } + + inline const std::string& InterpreterPath() + { + return InterpreterPathStorage(); + } + inline std::filesystem::path FixturesRoot() { return std::filesystem::path(__FILE__).parent_path() / "fixtures"; diff --git a/lsp-server/test/test_provider/interpreter_test.cppm b/lsp-server/test/test_provider/interpreter_test.cppm new file mode 100644 index 0000000..6f1fc84 --- /dev/null +++ b/lsp-server/test/test_provider/interpreter_test.cppm @@ -0,0 +1,210 @@ +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; + } +} + diff --git a/lsp-server/test/test_provider/test_main.cppm b/lsp-server/test/test_provider/test_main.cppm index d022573..5f72a53 100644 --- a/lsp-server/test/test_provider/test_main.cppm +++ b/lsp-server/test/test_provider/test_main.cppm @@ -7,6 +7,7 @@ import std; import lsp.test.framework; import lsp.test.provider.completion; import lsp.test.provider.definitions; +import lsp.test.provider.interpreter; import lsp.test.provider.json_flow; import lsp.test.provider.json_provider_coverage; import lsp.test.provider.misc; @@ -22,10 +23,18 @@ export int Run(int argc, char** argv) for (int i = 1; i < argc; ++i) { - if (std::string_view(argv[i]) == "--exit-provider") + std::string_view arg(argv[i]); + + if (arg == "--exit-provider") { return lsp::test::provider::RunExitProviderChild(); } + + constexpr std::string_view kInterpreterPrefix = "--interpreter="; + if (arg.starts_with(kInterpreterPrefix)) + { + lsp::test::provider::SetInterpreterPath(std::string(arg.substr(kInterpreterPrefix.size()))); + } } lsp::test::TestRunner runner; @@ -40,6 +49,8 @@ export int Run(int argc, char** argv) lsp::test::provider::CompletionTests::Register(runner); std::cout << " - Definition tests" << std::endl; lsp::test::provider::DefinitionTests::Register(runner); + std::cout << " - Interpreter tests" << std::endl; + lsp::test::provider::InterpreterTests::Register(runner); std::cout << " - JSON flow tests" << std::endl; lsp::test::provider::JsonFlowTests::Register(runner); std::cout << " - JSON provider coverage tests" << std::endl;