425 lines
16 KiB
C++
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;
|
|
}
|
|
}
|