tsl-devkit/lsp-server/test/test_provider/completion_test.cppm

425 lines
16 KiB
C++

module;
export module lsp.test.provider.completion;
import std;
import lsp.test.framework;
import lsp.provider.text_document.completion;
import lsp.provider.completion_item.resolve;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class CompletionTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestClassMethodCompletion();
static TestResult TestNewCompletion();
static TestResult TestUnitScopedNewCompletion();
static TestResult TestCreateObjectCompletion();
static TestResult TestCreateObjectQuotedCompletion();
static TestResult TestCreateObjectQualifiedCompletion();
static TestResult TestUnitContextCompletion();
static TestResult TestUnitMemberCompletion();
static TestResult TestObjectMemberCompletion();
static TestResult TestObjectMemberQualifiedTypeCompletion();
static TestResult TestFunctionCompletion();
static TestResult TestKeywordCompletion();
static TestResult TestClassContextCompletion();
static TestResult TestUnitScopedNewAliasCompletion();
static TestResult TestCompletionResolveNewSnippet();
static TestResult TestCompletionResolveCreateObjectSnippet();
};
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
scheduler::AsyncExecutor scheduler{ 1 };
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);
if (!parsed)
{
throw std::runtime_error("Failed to deserialize response");
}
return *parsed;
}
protocol::Position FindPosition(const std::string& content, const std::string& marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
result.character += static_cast<std::uint32_t>(marker.size());
return result;
}
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
{
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = version;
open_params.textDocument.text = text;
hub.documents().OpenDocument(open_params);
}
std::vector<protocol::CompletionItem> RequestCompletion(ProviderEnv& env,
const std::string& uri,
const std::string& content,
const std::string& marker)
{
protocol::CompletionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, marker);
protocol::RequestMessage request;
request.id = "c1";
request.method = "textDocument/completion";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Completion handler;
auto json = handler.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
if (!response.result.has_value())
{
return {};
}
auto list = codec::FromLSPAny.template operator()<protocol::CompletionList>(response.result.value());
return list.items;
}
bool HasLabel(const std::vector<protocol::CompletionItem>& items, const std::string& label)
{
return std::any_of(items.begin(), items.end(), [&](const auto& item) {
return item.label == label;
});
}
std::optional<protocol::CompletionItem> FindItem(const std::vector<protocol::CompletionItem>& items,
const std::string& label)
{
auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
return item.label == label;
});
if (it == items.end())
{
return std::nullopt;
}
return *it;
}
}
void CompletionTests::Register(TestRunner& runner)
{
runner.addTest("completion class(Widget).", TestClassMethodCompletion);
runner.addTest("completion new Widget", TestNewCompletion);
runner.addTest("completion new unit(MainUnit).Widget", TestUnitScopedNewCompletion);
runner.addTest("completion createobject", TestCreateObjectCompletion);
runner.addTest("completion createobject quoted", TestCreateObjectQuotedCompletion);
runner.addTest("completion createobject qualified", TestCreateObjectQualifiedCompletion);
runner.addTest("completion unit(", TestUnitContextCompletion);
runner.addTest("completion unit member", TestUnitMemberCompletion);
runner.addTest("completion object member", TestObjectMemberCompletion);
runner.addTest("completion object member qualified type", TestObjectMemberQualifiedTypeCompletion);
runner.addTest("completion function prefix", TestFunctionCompletion);
runner.addTest("completion keyword prefix", TestKeywordCompletion);
runner.addTest("completion class(", TestClassContextCompletion);
runner.addTest("completion new alias", TestUnitScopedNewAliasCompletion);
runner.addTest("completion resolve new snippet", TestCompletionResolveNewSnippet);
runner.addTest("completion resolve createobject snippet", TestCompletionResolveCreateObjectSnippet);
}
TestResult CompletionTests::TestClassMethodCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "class(Widget).Sta");
assertTrue(HasLabel(items, "StaticFoo"), "class() should suggest static methods");
return result;
}
TestResult CompletionTests::TestNewCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new Wid");
assertTrue(HasLabel(items, "Widget"), "new context should suggest classes");
return result;
}
TestResult CompletionTests::TestUnitScopedNewCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new unit(MainUnit).Wid");
assertTrue(HasLabel(items, "Widget"), "unit scoped new should suggest unit classes");
return result;
}
TestResult CompletionTests::TestCreateObjectCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(Wid");
assertTrue(HasLabel(items, "Widget"), "createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestCreateObjectQuotedCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
assertTrue(HasLabel(items, "Widget"), "quoted createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestCreateObjectQualifiedCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(MainUnit.Wid");
assertTrue(HasLabel(items, "Widget"), "qualified createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestUnitContextCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit(Main");
assertTrue(HasLabel(items, "MainUnit"), "unit context should list visible units");
return result;
}
TestResult CompletionTests::TestUnitMemberCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit(MainUnit).Uni");
assertTrue(HasLabel(items, "UnitFunc"), "unit member completion should list functions");
return result;
}
TestResult CompletionTests::TestObjectMemberCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "obj.Fo");
assertTrue(HasLabel(items, "Foo"), "object member completion should list instance methods");
return result;
}
TestResult CompletionTests::TestObjectMemberQualifiedTypeCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit_inst.Wor");
assertTrue(HasLabel(items, "Work"), "qualified type should resolve workspace class members");
return result;
}
TestResult CompletionTests::TestFunctionCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "UnitF");
assertTrue(HasLabel(items, "UnitFunc"), "function prefix should return UnitFunc");
return result;
}
TestResult CompletionTests::TestKeywordCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "fun");
assertTrue(HasLabel(items, "function"), "keyword completion should include 'function'");
return result;
}
TestResult CompletionTests::TestClassContextCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "class(Wid");
assertTrue(HasLabel(items, "Widget"), "class context should suggest classes with static methods");
return result;
}
TestResult CompletionTests::TestUnitScopedNewAliasCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new MainUnit.Wid");
assertTrue(HasLabel(items, "Widget"), "alias scoped new should suggest classes");
return result;
}
TestResult CompletionTests::TestCompletionResolveNewSnippet()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new Wid");
auto item = FindItem(items, "Widget");
assertTrue(item.has_value(), "Expected Widget completion");
protocol::RequestMessage request;
request.id = "r1";
request.method = "completionItem/resolve";
request.params = codec::ToLSPAny(*item);
::lsp::provider::completion_item::Resolve resolver;
auto json = resolver.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
assertTrue(resolved.insertText.value().find("Widget(") == 0, "Resolved new snippet should start with Widget(");
return result;
}
TestResult CompletionTests::TestCompletionResolveCreateObjectSnippet()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
auto item = FindItem(items, "Widget");
assertTrue(item.has_value(), "Expected Widget completion");
protocol::RequestMessage request;
request.id = "r2";
request.method = "completionItem/resolve";
request.params = codec::ToLSPAny(*item);
::lsp::provider::completion_item::Resolve resolver;
auto json = resolver.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
auto snippet = resolved.insertText.value();
assertTrue(!snippet.empty() && snippet.front() == 'W', "Resolved createobject snippet should not add quote");
return result;
}
}