🐛 fix(symbol): index all system tsf files
Do not drop system-library .tsf units when the file stem differs from top-level symbols. Add test_provider support for --interpreter=... and an interpreter integration test that validates full indexing plus completion+resolve snippet.
This commit is contained in:
parent
02864dda89
commit
adec86a3aa
|
|
@ -348,12 +348,10 @@ namespace lsp::manager
|
||||||
auto stem = entry.path().stem().string();
|
auto stem = entry.path().stem().string();
|
||||||
if (!HasMatchingTopLevelSymbol(*table, stem))
|
if (!HasMatchingTopLevelSymbol(*table, stem))
|
||||||
{
|
{
|
||||||
spdlog::warn("Skipping system file {}: top-level symbol does not match file name (stem='{}', top-level={})",
|
spdlog::debug("Indexing system file {} with unmatched top-level symbol (stem='{}', top-level={})",
|
||||||
entry.path().string(),
|
entry.path().string(),
|
||||||
stem,
|
stem,
|
||||||
DescribeTopLevelSymbols(*table));
|
DescribeTopLevelSymbols(*table));
|
||||||
++failed;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StoredSymbolEntry stored;
|
StoredSymbolEntry stored;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ set(SOURCES
|
||||||
json_flow_test.cppm
|
json_flow_test.cppm
|
||||||
json_provider_coverage_test.cppm
|
json_provider_coverage_test.cppm
|
||||||
definitions_test.cppm
|
definitions_test.cppm
|
||||||
|
interpreter_test.cppm
|
||||||
provider_misc_test.cppm
|
provider_misc_test.cppm
|
||||||
provider_surface_test.cppm
|
provider_surface_test.cppm
|
||||||
../../src/tree-sitter/parser.c
|
../../src/tree-sitter/parser.c
|
||||||
|
|
@ -52,6 +53,7 @@ target_sources(
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm
|
${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/json_provider_coverage_test.cppm
|
${CMAKE_CURRENT_SOURCE_DIR}/json_provider_coverage_test.cppm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/definitions_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_misc_test.cppm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm
|
${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm
|
||||||
../../src/bridge/glaze.cppm
|
../../src/bridge/glaze.cppm
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,22 @@ export namespace lsp::test::provider
|
||||||
return ExecutablePathStorage();
|
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()
|
inline std::filesystem::path FixturesRoot()
|
||||||
{
|
{
|
||||||
return std::filesystem::path(__FILE__).parent_path() / "fixtures";
|
return std::filesystem::path(__FILE__).parent_path() / "fixtures";
|
||||||
|
|
|
||||||
|
|
@ -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<protocol::ResponseMessage>(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<protocol::uinteger>(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<char>(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()<protocol::CompletionList>(*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()<protocol::CompletionItem>(*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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ import std;
|
||||||
import lsp.test.framework;
|
import lsp.test.framework;
|
||||||
import lsp.test.provider.completion;
|
import lsp.test.provider.completion;
|
||||||
import lsp.test.provider.definitions;
|
import lsp.test.provider.definitions;
|
||||||
|
import lsp.test.provider.interpreter;
|
||||||
import lsp.test.provider.json_flow;
|
import lsp.test.provider.json_flow;
|
||||||
import lsp.test.provider.json_provider_coverage;
|
import lsp.test.provider.json_provider_coverage;
|
||||||
import lsp.test.provider.misc;
|
import lsp.test.provider.misc;
|
||||||
|
|
@ -22,10 +23,18 @@ export int Run(int argc, char** argv)
|
||||||
|
|
||||||
for (int i = 1; i < argc; ++i)
|
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();
|
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;
|
lsp::test::TestRunner runner;
|
||||||
|
|
@ -40,6 +49,8 @@ export int Run(int argc, char** argv)
|
||||||
lsp::test::provider::CompletionTests::Register(runner);
|
lsp::test::provider::CompletionTests::Register(runner);
|
||||||
std::cout << " - Definition tests" << std::endl;
|
std::cout << " - Definition tests" << std::endl;
|
||||||
lsp::test::provider::DefinitionTests::Register(runner);
|
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;
|
std::cout << " - JSON flow tests" << std::endl;
|
||||||
lsp::test::provider::JsonFlowTests::Register(runner);
|
lsp::test::provider::JsonFlowTests::Register(runner);
|
||||||
std::cout << " - JSON provider coverage tests" << std::endl;
|
std::cout << " - JSON provider coverage tests" << std::endl;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue