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

3508 lines
151 KiB
C++

module;
export module lsp.test.provider.misc;
import std;
import spdlog;
import lsp.test.framework;
import lsp.provider.initialize.initialize;
import lsp.provider.initialized.initialized;
import lsp.provider.text_document.did_open;
import lsp.provider.text_document.did_change;
import lsp.provider.text_document.did_close;
import lsp.provider.text_document.code_action;
import lsp.provider.text_document.code_lens;
import lsp.provider.text_document.color_presentation;
import lsp.provider.text_document.document_color;
import lsp.provider.text_document.document_highlight;
import lsp.provider.text_document.document_symbol;
import lsp.provider.text_document.folding_range;
import lsp.provider.text_document.hover;
import lsp.provider.text_document.implementation;
import lsp.provider.text_document.prepare_call_hierarchy;
import lsp.provider.text_document.prepare_type_hierarchy;
import lsp.provider.text_document.prepare_rename;
import lsp.provider.text_document.rename;
import lsp.provider.text_document.linked_editing_range;
import lsp.provider.text_document.references;
import lsp.provider.text_document.selection_range;
import lsp.provider.text_document.semantic_tokens;
import lsp.provider.text_document.signature_help;
import lsp.provider.text_document.type_definition;
import lsp.provider.text_document.diagnostic;
import lsp.provider.text_document.document_link;
import lsp.provider.text_document.inlay_hint;
import lsp.provider.text_document.formatting;
import lsp.provider.text_document.range_formatting;
import lsp.provider.text_document.on_type_formatting;
import lsp.provider.text_document.inline_value;
import lsp.provider.text_document.moniker;
import lsp.provider.call_hierarchy.incoming_calls;
import lsp.provider.call_hierarchy.outgoing_calls;
import lsp.provider.type_hierarchy.subtypes;
import lsp.provider.type_hierarchy.supertypes;
import lsp.provider.code_action.resolve;
import lsp.provider.code_lens.resolve;
import lsp.provider.document_link.resolve;
import lsp.provider.inlay_hint.resolve;
import lsp.provider.client.register_capability;
import lsp.provider.client.unregister_capability;
import lsp.provider.window.work_done_progress_create;
import lsp.provider.window.show_message_request;
import lsp.provider.window.show_document;
import lsp.provider.window.log_message;
import lsp.provider.window.show_message;
import lsp.provider.telemetry.event;
import lsp.provider.workspace.symbol;
import lsp.provider.workspace.diagnostic;
import lsp.provider.workspace.did_create_files;
import lsp.provider.workspace.did_delete_files;
import lsp.provider.workspace.did_rename_files;
import lsp.provider.workspace.did_change_watched_files;
import lsp.provider.workspace.did_change_configuration;
import lsp.provider.workspace.did_change_workspace_folders;
import lsp.provider.workspace.configuration;
import lsp.provider.workspace.apply_edit;
import lsp.provider.workspace.workspace_folders;
import lsp.provider.workspace.code_lens_refresh;
import lsp.provider.workspace.diagnostic_refresh;
import lsp.provider.workspace.inlay_hint_refresh;
import lsp.provider.workspace.inline_value_refresh;
import lsp.provider.workspace.semantic_tokens_refresh;
import lsp.provider.workspace.execute_command;
import lsp.provider.workspace.will_create_files;
import lsp.provider.workspace.will_delete_files;
import lsp.provider.workspace.will_rename_files;
import lsp.provider.workspace_symbol.resolve;
import lsp.provider.text_document.publish_diagnostics;
import lsp.provider.shutdown.shutdown;
import lsp.provider.cancel_request.cancel_request;
import lsp.provider.trace.set_trace;
import lsp.provider.exit.exit;
import lsp.core.dispatcher;
import lsp.manager.manager_hub;
import lsp.manager.symbol;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.language.ast;
import tree_sitter;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class ProviderMiscTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestInitializeProvider();
static TestResult TestInitializedNotification();
static TestResult TestDidOpenDidChangeDidClose();
static TestResult TestHoverProvider();
static TestResult TestRenameProvider();
static TestResult TestPrepareRenameProvider();
static TestResult TestRenameInvalidName();
static TestResult TestLinkedEditingRangeProvider();
static TestResult TestReferencesProvider();
static TestResult TestDocumentHighlightProvider();
static TestResult TestImplementationProvider();
static TestResult TestTypeDefinitionProvider();
static TestResult TestDocumentSymbolProvider();
static TestResult TestDocumentLinkProvider();
static TestResult TestDocumentLinkResolveProvider();
static TestResult TestFoldingRangeProvider();
static TestResult TestSelectionRangeProvider();
static TestResult TestDocumentColorProvider();
static TestResult TestColorPresentationProvider();
static TestResult TestTextDocumentDiagnosticProvider();
static TestResult TestInlayHintProvider();
static TestResult TestInlayHintResolveProvider();
static TestResult TestCodeLensProvider();
static TestResult TestCodeLensResolveProvider();
static TestResult TestPrepareCallHierarchyProvider();
static TestResult TestCallHierarchyIncomingCallsProvider();
static TestResult TestCallHierarchyOutgoingCallsProvider();
static TestResult TestPrepareTypeHierarchyProvider();
static TestResult TestTypeHierarchySupertypesProvider();
static TestResult TestTypeHierarchySubtypesProvider();
static TestResult TestDidCreateFilesProvider();
static TestResult TestDidDeleteFilesProvider();
static TestResult TestDidRenameFilesProvider();
static TestResult TestDidChangeWatchedFilesProvider();
static TestResult TestDidChangeConfigurationProvider();
static TestResult TestDidChangeWorkspaceFoldersProvider();
static TestResult TestWorkspaceSymbolProvider();
static TestResult TestWorkspaceDiagnosticProvider();
static TestResult TestWorkspaceConfigurationProvider();
static TestResult TestWorkspaceApplyEditProvider();
static TestResult TestWorkspaceWorkspaceFoldersProvider();
static TestResult TestWorkspaceRefreshProviders();
static TestResult TestClientCapabilityProviders();
static TestResult TestWindowWorkDoneProgressCreateProvider();
static TestResult TestWindowShowMessageRequestProvider();
static TestResult TestWindowShowDocumentProvider();
static TestResult TestWindowMessageNotifications();
static TestResult TestTelemetryEventNotification();
static TestResult TestPublishDiagnosticsNotification();
static TestResult TestSemanticTokensProvider();
static TestResult TestSignatureHelpProvider();
static TestResult TestCodeActionProvider();
static TestResult TestCodeActionResolveProvider();
static TestResult TestDocumentFormattingProvider();
static TestResult TestDocumentRangeFormattingProvider();
static TestResult TestDocumentOnTypeFormattingProvider();
static TestResult TestInlineValueProvider();
static TestResult TestMonikerProvider();
static TestResult TestExecuteCommandProvider();
static TestResult TestWillFileOperationsProviders();
static TestResult TestWorkspaceSymbolResolveProvider();
static TestResult TestShutdownProvider();
static TestResult TestCancelRequestProvider();
static TestResult TestSetTraceProvider();
static TestResult TestExitProvider();
};
int RunExitProviderChild();
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
std::vector<core::ServerLifecycleEvent> events;
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([this](core::ServerLifecycleEvent event) { events.push_back(event); }, 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++;
}
}
return result;
}
protocol::LSPObject ToPositionObject(const protocol::Position& pos)
{
return protocol::LSPObject{
{ "line", static_cast<protocol::integer>(pos.line) },
{ "character", static_cast<protocol::integer>(pos.character) },
};
}
protocol::LSPObject ToRangeObject(const protocol::Range& range)
{
return protocol::LSPObject{
{ "start", ToPositionObject(range.start) },
{ "end", ToPositionObject(range.end) },
};
}
protocol::Range FullDocumentRange(const std::string& content)
{
protocol::Range range{};
range.start.line = 0;
range.start.character = 0;
protocol::uinteger line_count = 0;
protocol::uinteger last_line_len = 0;
for (char ch : content)
{
if (ch == '\n')
{
line_count++;
last_line_len = 0;
}
else
{
last_line_len++;
}
}
range.end.line = line_count;
range.end.character = last_line_len;
return range;
}
std::optional<protocol::uinteger> GetUInteger(const protocol::LSPAny& any)
{
if (any.Is<protocol::uinteger>())
{
return any.Get<protocol::uinteger>();
}
if (any.Is<protocol::integer>())
{
return static_cast<protocol::uinteger>(any.Get<protocol::integer>());
}
return std::nullopt;
}
std::optional<double> GetDecimal(const protocol::LSPAny& any)
{
if (any.Is<protocol::decimal>())
{
return any.Get<protocol::decimal>();
}
if (any.Is<protocol::uinteger>())
{
return static_cast<double>(any.Get<protocol::uinteger>());
}
if (any.Is<protocol::integer>())
{
return static_cast<double>(any.Get<protocol::integer>());
}
return std::nullopt;
}
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);
}
}
void ProviderMiscTests::Register(TestRunner& runner)
{
runner.addTest("initialize provider", TestInitializeProvider);
runner.addTest("initialized notification", TestInitializedNotification);
runner.addTest("didOpen/didChange/didClose", TestDidOpenDidChangeDidClose);
runner.addTest("hover provider", TestHoverProvider);
runner.addTest("rename provider", TestRenameProvider);
runner.addTest("prepareRename provider", TestPrepareRenameProvider);
runner.addTest("rename invalid name", TestRenameInvalidName);
runner.addTest("linkedEditingRange provider", TestLinkedEditingRangeProvider);
runner.addTest("references provider", TestReferencesProvider);
runner.addTest("documentHighlight provider", TestDocumentHighlightProvider);
runner.addTest("implementation provider", TestImplementationProvider);
runner.addTest("typeDefinition provider", TestTypeDefinitionProvider);
runner.addTest("documentSymbol provider", TestDocumentSymbolProvider);
runner.addTest("documentLink provider", TestDocumentLinkProvider);
runner.addTest("documentLink resolve provider", TestDocumentLinkResolveProvider);
runner.addTest("foldingRange provider", TestFoldingRangeProvider);
runner.addTest("selectionRange provider", TestSelectionRangeProvider);
runner.addTest("documentColor provider", TestDocumentColorProvider);
runner.addTest("colorPresentation provider", TestColorPresentationProvider);
runner.addTest("textDocument diagnostic provider", TestTextDocumentDiagnosticProvider);
runner.addTest("inlayHint provider", TestInlayHintProvider);
runner.addTest("inlayHint resolve provider", TestInlayHintResolveProvider);
runner.addTest("codeLens provider", TestCodeLensProvider);
runner.addTest("codeLens resolve provider", TestCodeLensResolveProvider);
runner.addTest("prepareCallHierarchy provider", TestPrepareCallHierarchyProvider);
runner.addTest("callHierarchy incomingCalls provider", TestCallHierarchyIncomingCallsProvider);
runner.addTest("callHierarchy outgoingCalls provider", TestCallHierarchyOutgoingCallsProvider);
runner.addTest("prepareTypeHierarchy provider", TestPrepareTypeHierarchyProvider);
runner.addTest("typeHierarchy supertypes provider", TestTypeHierarchySupertypesProvider);
runner.addTest("typeHierarchy subtypes provider", TestTypeHierarchySubtypesProvider);
runner.addTest("didCreateFiles notification", TestDidCreateFilesProvider);
runner.addTest("didDeleteFiles notification", TestDidDeleteFilesProvider);
runner.addTest("didRenameFiles notification", TestDidRenameFilesProvider);
runner.addTest("didChangeWatchedFiles notification", TestDidChangeWatchedFilesProvider);
runner.addTest("didChangeConfiguration notification", TestDidChangeConfigurationProvider);
runner.addTest("didChangeWorkspaceFolders notification", TestDidChangeWorkspaceFoldersProvider);
runner.addTest("workspace symbol provider", TestWorkspaceSymbolProvider);
runner.addTest("workspace diagnostic provider", TestWorkspaceDiagnosticProvider);
runner.addTest("workspace configuration provider", TestWorkspaceConfigurationProvider);
runner.addTest("workspace applyEdit provider", TestWorkspaceApplyEditProvider);
runner.addTest("workspace workspaceFolders provider", TestWorkspaceWorkspaceFoldersProvider);
runner.addTest("workspace refresh providers", TestWorkspaceRefreshProviders);
runner.addTest("client capability providers", TestClientCapabilityProviders);
runner.addTest("window workDoneProgressCreate provider", TestWindowWorkDoneProgressCreateProvider);
runner.addTest("window showMessageRequest provider", TestWindowShowMessageRequestProvider);
runner.addTest("window showDocument provider", TestWindowShowDocumentProvider);
runner.addTest("window message notifications", TestWindowMessageNotifications);
runner.addTest("telemetry event notification", TestTelemetryEventNotification);
runner.addTest("publish diagnostics notification", TestPublishDiagnosticsNotification);
runner.addTest("semantic tokens provider", TestSemanticTokensProvider);
runner.addTest("signature help provider", TestSignatureHelpProvider);
runner.addTest("code action provider", TestCodeActionProvider);
runner.addTest("codeAction/resolve provider", TestCodeActionResolveProvider);
runner.addTest("document formatting provider", TestDocumentFormattingProvider);
runner.addTest("document range formatting provider", TestDocumentRangeFormattingProvider);
runner.addTest("document onType formatting provider", TestDocumentOnTypeFormattingProvider);
runner.addTest("inline value provider", TestInlineValueProvider);
runner.addTest("moniker provider", TestMonikerProvider);
runner.addTest("workspace executeCommand provider", TestExecuteCommandProvider);
runner.addTest("workspace will file operations providers", TestWillFileOperationsProviders);
runner.addTest("workspaceSymbol/resolve provider", TestWorkspaceSymbolResolveProvider);
runner.addTest("shutdown provider", TestShutdownProvider);
runner.addTest("cancel request provider", TestCancelRequestProvider);
runner.addTest("setTrace provider", TestSetTraceProvider);
runner.addTest("exit provider", TestExitProvider);
}
TestResult ProviderMiscTests::TestInitializeProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::InitializeParams params;
params.trace = protocol::TraceValueLiterals::Off;
params.workspaceFolders = std::vector<protocol::WorkspaceFolder>{
{ .uri = ToUri(FixturePath("workspace")), .name = "workspace" }
};
protocol::RequestMessage request;
request.id = "init";
request.method = "initialize";
request.params = codec::ToLSPAny(params);
::lsp::provider::Initialize provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Initialize should return result");
assertTrue(response.result.value().Is<protocol::LSPObject>(), "Initialize result should be an object");
const auto& result_obj = response.result.value().Get<protocol::LSPObject>();
auto server_info_it = result_obj.find("serverInfo");
assertTrue(server_info_it != result_obj.end(), "Initialize should return serverInfo");
assertTrue(server_info_it->second.Is<protocol::LSPObject>(), "serverInfo should be an object");
const auto& server_info = server_info_it->second.Get<protocol::LSPObject>();
auto name_it = server_info.find("name");
assertTrue(name_it != server_info.end(), "serverInfo should include name");
assertTrue(name_it->second.Is<protocol::string>(), "serverInfo.name should be string");
assertTrue(!name_it->second.Get<protocol::string>().empty(), "Initialize should set server name");
auto capabilities_it = result_obj.find("capabilities");
assertTrue(capabilities_it != result_obj.end(), "Initialize should return capabilities");
assertTrue(capabilities_it->second.Is<protocol::LSPObject>(), "capabilities should be an object");
const auto& capabilities = capabilities_it->second.Get<protocol::LSPObject>();
assertTrue(capabilities.find("textDocumentSync") != capabilities.end(),
"Initialize should set textDocumentSync");
auto completion_it = capabilities.find("completionProvider");
assertTrue(completion_it != capabilities.end(), "Initialize should set completionProvider");
if (completion_it != capabilities.end() && completion_it->second.Is<protocol::LSPObject>())
{
const auto& completion = completion_it->second.Get<protocol::LSPObject>();
auto resolve_it = completion.find("resolveProvider");
assertTrue(resolve_it != completion.end(), "Initialize should include resolveProvider");
assertTrue(resolve_it->second.Is<protocol::boolean>() && resolve_it->second.Get<protocol::boolean>(),
"Initialize should enable completion resolve");
}
assertTrue(capabilities.find("definitionProvider") != capabilities.end(), "Initialize should enable definitionProvider");
assertFalse(capabilities.contains("hoverProvider"), "Initialize should not advertise hoverProvider");
assertFalse(capabilities.contains("referencesProvider"), "Initialize should not advertise referencesProvider");
assertFalse(capabilities.contains("renameProvider"), "Initialize should not advertise renameProvider");
assertFalse(capabilities.contains("documentSymbolProvider"), "Initialize should not advertise documentSymbolProvider");
assertFalse(capabilities.contains("workspaceSymbolProvider"), "Initialize should not advertise workspaceSymbolProvider");
assertFalse(capabilities.contains("workspace"), "Initialize should not advertise workspace capabilities");
env.scheduler.WaitAll();
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module);
bool found_workspace = std::any_of(indexed.begin(), indexed.end(), [](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WorkspaceUnit";
});
assertTrue(found_workspace, "Workspace symbols should be indexed");
assertTrue(!env.events.empty(), "Initialize should emit lifecycle event");
assertTrue(env.events.back() == core::ServerLifecycleEvent::kInitialized, "Initialize should emit initialized");
return result;
}
TestResult ProviderMiscTests::TestHoverProvider()
{
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);
protocol::HoverParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "UnitFunc(1);");
protocol::RequestMessage request;
request.id = "hover";
request.method = "textDocument/hover";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Hover provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Hover should return result");
assertFalse(response.result->Is<std::nullptr_t>(), "Hover result should not be null");
auto hover = codec::FromLSPAny.template operator()<protocol::Hover>(response.result.value());
assertTrue(hover.contents.value.find("UnitFunc") != std::string::npos, "Hover contents should mention UnitFunc");
assertTrue(hover.range.has_value(), "Hover should include range");
return result;
}
TestResult ProviderMiscTests::TestInitializedNotification()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
::lsp::provider::Initialized provider;
protocol::NotificationMessage notification;
notification.method = "initialized";
provider.HandleNotification(notification, env.context);
return result;
}
TestResult ProviderMiscTests::TestDidOpenDidChangeDidClose()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = 1;
open_params.textDocument.text = content;
protocol::NotificationMessage open_msg;
open_msg.method = "textDocument/didOpen";
open_msg.params = codec::ToLSPAny(open_params);
::lsp::provider::text_document::DidOpen open_provider;
open_provider.HandleNotification(open_msg, env.context);
auto stored = env.hub.documents().GetContent(uri);
assertTrue(stored.has_value(), "Document should be opened");
protocol::DidChangeTextDocumentParams change_params;
change_params.textDocument.uri = uri;
change_params.textDocument.version = 2;
protocol::TextDocumentContentChangeEvent change;
change.range.start.line = 0;
change.range.start.character = 0;
change.range.end.line = 100;
change.range.end.character = 0;
change.text = "var replaced: integer;";
change_params.contentChanges.push_back(change);
protocol::NotificationMessage change_msg;
change_msg.method = "textDocument/didChange";
change_msg.params = codec::ToLSPAny(change_params);
::lsp::provider::text_document::DidChange change_provider;
change_provider.HandleNotification(change_msg, env.context);
auto updated = env.hub.documents().GetContent(uri);
assertTrue(updated.has_value() && updated.value() == change.text, "Document should be updated");
protocol::DidCloseTextDocumentParams close_params;
close_params.textDocument.uri = uri;
protocol::NotificationMessage close_msg;
close_msg.method = "textDocument/didClose";
close_msg.params = codec::ToLSPAny(close_params);
::lsp::provider::text_document::DidClose close_provider;
close_provider.HandleNotification(close_msg, env.context);
auto closed = env.hub.documents().GetContent(uri);
assertFalse(closed.has_value(), "Document should be closed");
return result;
}
TestResult ProviderMiscTests::TestRenameProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RenameParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.newName = "renamed";
protocol::RequestMessage request;
request.id = "rename";
request.method = "textDocument/rename";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Rename provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Rename should return workspace edit");
auto edit = codec::FromLSPAny.template operator()<protocol::WorkspaceEdit>(response.result.value());
auto it = edit.changes.find(uri);
assertTrue(it != edit.changes.end(), "Rename should include edits for document");
assertEqual(std::size_t(3), it->second.size(), "Rename should edit all occurrences");
return result;
}
TestResult ProviderMiscTests::TestPrepareRenameProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::PrepareRenameParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
protocol::RequestMessage request;
request.id = "prep";
request.method = "textDocument/prepareRename";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::PrepareRename provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "PrepareRename should return result");
assertFalse(response.result->Is<std::nullptr_t>(), "PrepareRename result should not be null");
auto range = codec::FromLSPAny.template operator()<protocol::Range>(response.result.value());
assertEqual(std::uint32_t(1), range.start.line, "PrepareRename should return range on assignment line");
assertEqual(std::uint32_t(0), range.start.character, "PrepareRename should start at identifier");
assertEqual(std::uint32_t(6), range.end.character, "PrepareRename should cover full identifier");
return result;
}
TestResult ProviderMiscTests::TestRenameInvalidName()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RenameParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.newName = "1bad";
protocol::RequestMessage request;
request.id = "rename_invalid";
request.method = "textDocument/rename";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Rename provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.error.has_value(), "Invalid rename should return error");
assertEqual(static_cast<int>(protocol::ErrorCodes::InvalidParams),
static_cast<int>(response.error->code),
"Invalid rename should return invalid params");
return result;
}
TestResult ProviderMiscTests::TestLinkedEditingRangeProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::LinkedEditingRangeParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
protocol::RequestMessage request;
request.id = "linked_editing";
request.method = "textDocument/linkedEditingRange";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::LinkedEditingRange provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "LinkedEditingRange should not return error");
assertTrue(response.result.has_value(), "LinkedEditingRange should return result");
auto linked = codec::FromLSPAny.template operator()<protocol::LinkedEditingRanges>(response.result.value());
assertEqual(std::size_t(3), linked.ranges.size(), "LinkedEditingRange should include occurrences (incl decl)");
return result;
}
TestResult ProviderMiscTests::TestReferencesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::ReferenceParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.context.includeDeclaration = true;
protocol::RequestMessage request;
request.id = "refs";
request.method = "textDocument/references";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::References provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto locations = codec::FromLSPAny.template operator()<std::vector<protocol::Location>>(response.result.value());
assertEqual(std::size_t(3), locations.size(), "References provider should return all occurrences (incl decl)");
return result;
}
TestResult ProviderMiscTests::TestDocumentHighlightProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentHighlightParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
protocol::RequestMessage request;
request.id = "hl";
request.method = "textDocument/documentHighlight";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::DocumentHighlight provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto highlights = codec::FromLSPAny.template operator()<std::vector<protocol::DocumentHighlight>>(response.result.value());
std::size_t reads = 0;
std::size_t writes = 0;
std::size_t texts = 0;
for (const auto& highlight : highlights)
{
switch (highlight.kind)
{
case protocol::DocumentHighlightKind::Read:
++reads;
break;
case protocol::DocumentHighlightKind::Write:
++writes;
break;
case protocol::DocumentHighlightKind::Text:
++texts;
break;
}
}
assertEqual(std::size_t(1), reads, "DocumentHighlight should include one read reference");
assertEqual(std::size_t(1), writes, "DocumentHighlight should include one write reference");
assertEqual(std::size_t(1), texts, "DocumentHighlight should include declaration");
return result;
}
TestResult ProviderMiscTests::TestImplementationProvider()
{
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);
protocol::TextDocumentPositionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "function UnitFunc(a: integer): integer;");
params.position.character += static_cast<protocol::uinteger>(std::string("function ").size());
protocol::RequestMessage request;
request.id = "impl_func";
request.method = "textDocument/implementation";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Implementation provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Implementation should not return error");
assertTrue(response.result.has_value(), "Implementation should return result");
auto location = codec::FromLSPAny.template operator()<std::optional<protocol::Location>>(response.result.value());
assertTrue(location.has_value(), "Implementation should resolve function implementation");
auto expected = FindPosition(content, "function UnitFunc(a: integer): integer;\nbegin");
expected.character += static_cast<protocol::uinteger>(std::string("function ").size());
assertEqual(uri, location->uri, "Implementation location URI mismatch");
assertEqual(expected.line, location->range.start.line, "Implementation start line mismatch");
assertEqual(expected.character, location->range.start.character, "Implementation start character mismatch");
protocol::TextDocumentPositionParams method_params;
method_params.textDocument.uri = uri;
method_params.position = FindPosition(content, "function Foo(x: integer): integer;");
method_params.position.character += static_cast<protocol::uinteger>(std::string("function ").size());
protocol::RequestMessage method_request;
method_request.id = "impl_method";
method_request.method = "textDocument/implementation";
method_request.params = codec::ToLSPAny(method_params);
auto method_json = provider.ProvideResponse(method_request, env.context);
auto method_response = ParseResponse(method_json);
assertFalse(method_response.error.has_value(), "Method implementation should not return error");
assertTrue(method_response.result.has_value(), "Method implementation should return result");
auto method_location =
codec::FromLSPAny.template operator()<std::optional<protocol::Location>>(method_response.result.value());
assertTrue(method_location.has_value(), "Implementation should resolve method implementation");
auto method_expected = FindPosition(content, "function Widget.Foo(x: integer): integer;\nbegin");
method_expected.character += static_cast<protocol::uinteger>(std::string("function ").size());
assertEqual(uri, method_location->uri, "Method implementation location URI mismatch");
assertEqual(method_expected.line, method_location->range.start.line, "Method implementation start line mismatch");
assertEqual(method_expected.character,
method_location->range.start.character,
"Method implementation start character mismatch");
return result;
}
TestResult ProviderMiscTests::TestTypeDefinitionProvider()
{
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);
protocol::TextDocumentPositionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "obj: Widget");
protocol::RequestMessage request;
request.id = "type_def";
request.method = "textDocument/typeDefinition";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::TypeDefinition provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "TypeDefinition should not return error");
assertTrue(response.result.has_value(), "TypeDefinition should return result");
auto location = codec::FromLSPAny.template operator()<std::optional<protocol::Location>>(response.result.value());
assertTrue(location.has_value(), "TypeDefinition should resolve class type definition");
auto expected = FindPosition(content, "type Widget = class");
expected.character += static_cast<protocol::uinteger>(std::string("type ").size());
assertEqual(uri, location->uri, "TypeDefinition location URI mismatch");
assertEqual(expected.line, location->range.start.line, "TypeDefinition start line mismatch");
assertEqual(expected.character, location->range.start.character, "TypeDefinition start character mismatch");
return result;
}
TestResult ProviderMiscTests::TestDocumentSymbolProvider()
{
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);
protocol::LSPObject params;
params["textDocument"] = protocol::LSPObject{ { "uri", uri } };
protocol::RequestMessage request;
request.id = "doc_symbols";
request.method = "textDocument/documentSymbol";
request.params = protocol::LSPAny(params);
::lsp::provider::text_document::DocumentSymbol provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto symbols = codec::FromLSPAny.template operator()<std::vector<protocol::DocumentSymbol>>(response.result.value());
assertTrue(!symbols.empty(), "DocumentSymbol should return symbols");
auto unit_it = std::find_if(symbols.begin(), symbols.end(), [](const protocol::DocumentSymbol& symbol) {
return symbol.name == "MainUnit";
});
assertTrue(unit_it != symbols.end(), "DocumentSymbol should include unit symbol");
assertTrue(unit_it->children.has_value(), "Unit symbol should include children");
const auto& children = unit_it->children.value();
bool found_widget = std::any_of(children.begin(), children.end(), [](const protocol::DocumentSymbol& symbol) {
return symbol.name == "Widget" && symbol.kind == protocol::SymbolKind::Class;
});
bool found_unit_func = std::any_of(children.begin(), children.end(), [](const protocol::DocumentSymbol& symbol) {
return symbol.name == "UnitFunc" && symbol.kind == protocol::SymbolKind::Function;
});
assertTrue(found_widget, "Unit children should include Widget");
assertTrue(found_unit_func, "Unit children should include UnitFunc");
return result;
}
TestResult ProviderMiscTests::TestDocumentLinkProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
env.hub.symbols().LoadSystemLibrary(FixturePath("system"));
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentLinkParams params;
params.textDocument.uri = uri;
protocol::RequestMessage request;
request.id = "doc_link";
request.method = "textDocument/documentLink";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::DocumentLink provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "DocumentLink should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "DocumentLink result should be array");
const auto& links = response.result->Get<protocol::LSPArray>();
bool found_workspace = false;
bool found_system = false;
for (const auto& link_any : links)
{
if (!link_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& link = link_any.Get<protocol::LSPObject>();
auto target_it = link.find("target");
if (target_it == link.end() || !target_it->second.Is<protocol::string>())
{
continue;
}
const auto& target = target_it->second.Get<protocol::string>();
if (target.find("WorkspaceUnit.tsf") != std::string::npos)
{
found_workspace = true;
}
if (target.find("SystemUnit.tsf") != std::string::npos)
{
found_system = true;
}
}
assertTrue(found_workspace, "DocumentLink should resolve WorkspaceUnit");
assertTrue(found_system, "DocumentLink should resolve SystemUnit");
return result;
}
TestResult ProviderMiscTests::TestDocumentLinkResolveProvider()
{
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 pos = FindPosition(content, "WorkspaceUnit");
protocol::Range range;
range.start = pos;
range.end = pos;
range.end.character += static_cast<protocol::uinteger>(std::string("WorkspaceUnit").size());
protocol::LSPObject data;
data["kind"] = protocol::string("unit");
data["name"] = protocol::string("WorkspaceUnit");
data["baseUri"] = protocol::string(uri);
protocol::LSPObject link;
link["range"] = codec::ToLSPAny(range);
link["data"] = protocol::LSPAny(std::move(data));
protocol::RequestMessage request;
request.id = "doc_link_resolve";
request.method = "documentLink/resolve";
request.params = protocol::LSPAny(std::move(link));
::lsp::provider::document_link::Resolve provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "DocumentLink resolve should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "DocumentLink resolve result should be object");
const auto& resolved = response.result->Get<protocol::LSPObject>();
auto target_it = resolved.find("target");
assertTrue(target_it != resolved.end(), "DocumentLink resolve should set target");
assertTrue(target_it->second.Is<protocol::string>(), "DocumentLink target should be string");
assertTrue(target_it->second.Get<protocol::string>().find("WorkspaceUnit.tsf") != std::string::npos,
"DocumentLink resolve should target WorkspaceUnit");
return result;
}
TestResult ProviderMiscTests::TestFoldingRangeProvider()
{
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);
protocol::FoldingRangeParams params;
params.textDocument.uri = uri;
protocol::RequestMessage request;
request.id = "folding";
request.method = "textDocument/foldingRange";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::FoldingRange provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "FoldingRange should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "FoldingRange result should be array");
const auto& ranges = response.result->Get<protocol::LSPArray>();
assertTrue(!ranges.empty(), "FoldingRange should include ranges");
bool has_span = false;
for (const auto& range_any : ranges)
{
if (!range_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& range_obj = range_any.Get<protocol::LSPObject>();
auto start_it = range_obj.find("startLine");
auto end_it = range_obj.find("endLine");
if (start_it == range_obj.end() || end_it == range_obj.end())
{
continue;
}
auto start_line = GetUInteger(start_it->second);
auto end_line = GetUInteger(end_it->second);
if (start_line && end_line && *end_line > *start_line)
{
has_span = true;
break;
}
}
assertTrue(has_span, "FoldingRange should include multi-line spans");
return result;
}
TestResult ProviderMiscTests::TestSelectionRangeProvider()
{
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);
protocol::SelectionRangeParams params;
params.textDocument.uri = uri;
params.positions.push_back(FindPosition(content, "UnitFunc(1);"));
protocol::RequestMessage request;
request.id = "select";
request.method = "textDocument/selectionRange";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::SelectionRange provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "SelectionRange should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "SelectionRange result should be array");
const auto& ranges = response.result->Get<protocol::LSPArray>();
assertEqual(std::size_t(1), ranges.size(), "SelectionRange should return one entry");
assertTrue(ranges[0].Is<protocol::LSPObject>(), "SelectionRange entry should be object");
const auto& range_obj = ranges[0].Get<protocol::LSPObject>();
auto range_it = range_obj.find("range");
assertTrue(range_it != range_obj.end(), "SelectionRange entry should include range");
assertTrue(range_it->second.Is<protocol::LSPObject>(), "SelectionRange range should be object");
return result;
}
TestResult ProviderMiscTests::TestDocumentColorProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("color_literals.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentColorParams params;
params.textDocument.uri = uri;
protocol::RequestMessage request;
request.id = "doc_color";
request.method = "textDocument/documentColor";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::DocumentColor provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "DocumentColor should not return error");
assertTrue(response.result.has_value(), "DocumentColor should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "DocumentColor result should be array");
const auto& infos = response.result->Get<protocol::LSPArray>();
assertTrue(!infos.empty(), "DocumentColor should return matches");
auto red_pos = FindPosition(content, "#ff0000");
bool found_red = false;
for (const auto& info_any : infos)
{
if (!info_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& info = info_any.Get<protocol::LSPObject>();
auto range_it = info.find("range");
auto color_it = info.find("color");
if (range_it == info.end() || color_it == info.end())
{
continue;
}
if (!range_it->second.Is<protocol::LSPObject>() || !color_it->second.Is<protocol::LSPObject>())
{
continue;
}
const auto& range = range_it->second.Get<protocol::LSPObject>();
auto start_it = range.find("start");
if (start_it == range.end() || !start_it->second.Is<protocol::LSPObject>())
{
continue;
}
const auto& start = start_it->second.Get<protocol::LSPObject>();
auto line_it = start.find("line");
auto ch_it = start.find("character");
if (line_it == start.end() || ch_it == start.end())
{
continue;
}
auto line = GetUInteger(line_it->second);
auto ch = GetUInteger(ch_it->second);
if (!line || !ch)
{
continue;
}
if (*line != red_pos.line || *ch != red_pos.character)
{
continue;
}
const auto& color = color_it->second.Get<protocol::LSPObject>();
auto red_it = color.find("red");
auto green_it = color.find("green");
auto blue_it = color.find("blue");
auto alpha_it = color.find("alpha");
assertTrue(red_it != color.end() && green_it != color.end() && blue_it != color.end() && alpha_it != color.end(),
"DocumentColor should include color components");
auto red = GetDecimal(red_it->second);
auto green = GetDecimal(green_it->second);
auto blue = GetDecimal(blue_it->second);
auto alpha = GetDecimal(alpha_it->second);
assertTrue(red && green && blue && alpha, "DocumentColor color components should be numeric");
assertTrue(std::abs(*red - 1.0) < 1e-6, "DocumentColor should parse red channel");
assertTrue(std::abs(*green) < 1e-6, "DocumentColor should parse green channel");
assertTrue(std::abs(*blue) < 1e-6, "DocumentColor should parse blue channel");
assertTrue(std::abs(*alpha - 1.0) < 1e-6, "DocumentColor should parse alpha channel");
found_red = true;
break;
}
assertTrue(found_red, "DocumentColor should include #ff0000");
return result;
}
TestResult ProviderMiscTests::TestColorPresentationProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("color_literals.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto pos = FindPosition(content, "#ff0000");
protocol::Range range;
range.start = pos;
range.end = pos;
range.end.character += 7;
protocol::ColorPresentationParams params;
params.textDocument.uri = uri;
params.range = range;
params.color.red = 1.0;
params.color.green = 0.0;
params.color.blue = 0.0;
params.color.alpha = 1.0;
protocol::RequestMessage request;
request.id = "color_presentation";
request.method = "textDocument/colorPresentation";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::ColorPresentation provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "ColorPresentation should not return error");
assertTrue(response.result.has_value(), "ColorPresentation should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "ColorPresentation result should be array");
const auto& presentations = response.result->Get<protocol::LSPArray>();
assertTrue(!presentations.empty(), "ColorPresentation should return entries");
assertTrue(presentations[0].Is<protocol::LSPObject>(), "ColorPresentation entry should be object");
const auto& entry = presentations[0].Get<protocol::LSPObject>();
auto label_it = entry.find("label");
assertTrue(label_it != entry.end(), "ColorPresentation should include label");
assertTrue(label_it->second.Is<protocol::string>(), "ColorPresentation label should be string");
assertEqual(std::string("#ff0000"), label_it->second.Get<protocol::string>(), "ColorPresentation should format hex");
auto edit_it = entry.find("textEdit");
assertTrue(edit_it != entry.end(), "ColorPresentation should include textEdit");
assertTrue(edit_it->second.Is<protocol::LSPObject>(), "ColorPresentation textEdit should be object");
const auto& edit_obj = edit_it->second.Get<protocol::LSPObject>();
auto new_text_it = edit_obj.find("newText");
assertTrue(new_text_it != edit_obj.end(), "ColorPresentation textEdit should include newText");
assertTrue(new_text_it->second.Is<protocol::string>(), "ColorPresentation textEdit.newText should be string");
assertEqual(std::string("#ff0000"), new_text_it->second.Get<protocol::string>(), "ColorPresentation textEdit should match label");
return result;
}
TestResult ProviderMiscTests::TestTextDocumentDiagnosticProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("code_action_missing_semicolon.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DiagnosticParams params;
params.textDocument.uri = uri;
protocol::RequestMessage request;
request.id = "doc_diag";
request.method = "textDocument/diagnostic";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Diagnostic provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Diagnostic should not return error");
assertTrue(response.result.has_value(), "Diagnostic should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "Diagnostic result should be object");
const auto& report = response.result->Get<protocol::LSPObject>();
auto kind_it = report.find("kind");
assertTrue(kind_it != report.end(), "Diagnostic report should include kind");
assertTrue(kind_it->second.Is<protocol::string>(), "Diagnostic kind should be string");
assertEqual(std::string("full"), kind_it->second.Get<protocol::string>(), "Diagnostic kind should be full");
auto items_it = report.find("items");
assertTrue(items_it != report.end(), "Diagnostic report should include items");
assertTrue(items_it->second.Is<protocol::LSPArray>(), "Diagnostic items should be array");
assertTrue(!items_it->second.Get<protocol::LSPArray>().empty(), "Diagnostic items should not be empty");
return result;
}
TestResult ProviderMiscTests::TestInlayHintProvider()
{
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);
protocol::LSPObject params;
params["textDocument"] = protocol::LSPObject{ { "uri", uri } };
params["range"] = ToRangeObject(FullDocumentRange(content));
protocol::RequestMessage request;
request.id = "inlay";
request.method = "textDocument/inlayHint";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::text_document::InlayHint provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "InlayHint should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "InlayHint result should be array");
const auto& hints = response.result->Get<protocol::LSPArray>();
bool found_param = false;
for (const auto& hint_any : hints)
{
if (!hint_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& hint = hint_any.Get<protocol::LSPObject>();
auto label_it = hint.find("label");
auto kind_it = hint.find("kind");
if (label_it == hint.end() || kind_it == hint.end())
{
continue;
}
if (!label_it->second.Is<protocol::string>())
{
continue;
}
auto kind = GetUInteger(kind_it->second);
if (!kind)
{
continue;
}
if (label_it->second.Get<protocol::string>() == "a:" &&
*kind == static_cast<protocol::uinteger>(protocol::InlayHintKind::Parameter))
{
found_param = true;
break;
}
}
assertTrue(found_param, "InlayHint should include UnitFunc parameter name hint");
auto type_path = FixturePath("inlay_hint_case.tsl");
auto type_content = ReadTextFile(type_path);
auto type_uri = ToUri(type_path);
OpenDocument(env.hub, type_uri, type_content, 1);
protocol::LSPObject type_params;
type_params["textDocument"] = protocol::LSPObject{ { "uri", type_uri } };
type_params["range"] = ToRangeObject(FullDocumentRange(type_content));
protocol::RequestMessage type_request;
type_request.id = "inlay_type";
type_request.method = "textDocument/inlayHint";
type_request.params = protocol::LSPAny(std::move(type_params));
auto type_json = provider.ProvideResponse(type_request, env.context);
auto type_response = ParseResponse(type_json);
assertTrue(type_response.result.has_value(), "InlayHint (type) should return result");
assertTrue(type_response.result->Is<protocol::LSPArray>(), "InlayHint (type) result should be array");
const auto& type_hints = type_response.result->Get<protocol::LSPArray>();
bool found_type = false;
for (const auto& hint_any : type_hints)
{
if (!hint_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& hint = hint_any.Get<protocol::LSPObject>();
auto kind_it = hint.find("kind");
auto label_it = hint.find("label");
if (kind_it == hint.end() || label_it == hint.end())
{
continue;
}
auto kind = GetUInteger(kind_it->second);
if (!kind)
{
continue;
}
if (*kind == static_cast<protocol::uinteger>(protocol::InlayHintKind::Type))
{
found_type = true;
break;
}
}
assertTrue(found_type, "InlayHint should include type hints for inferred variables");
return result;
}
TestResult ProviderMiscTests::TestInlayHintResolveProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject data;
data["detail"] = protocol::string("param: int");
protocol::Position position{};
position.line = 0;
position.character = 0;
protocol::LSPObject hint;
hint["position"] = ToPositionObject(position);
hint["label"] = protocol::string("param:");
hint["kind"] = static_cast<protocol::integer>(protocol::InlayHintKind::Parameter);
hint["data"] = protocol::LSPAny(std::move(data));
protocol::RequestMessage request;
request.id = "inlay_resolve";
request.method = "inlayHint/resolve";
request.params = protocol::LSPAny(std::move(hint));
::lsp::provider::inlay_hint::Resolve provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "InlayHint resolve should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "InlayHint resolve result should be object");
const auto& resolved = response.result->Get<protocol::LSPObject>();
auto tooltip_it = resolved.find("tooltip");
assertTrue(tooltip_it != resolved.end(), "InlayHint resolve should set tooltip");
assertTrue(tooltip_it->second.Is<protocol::string>(), "InlayHint tooltip should be string");
assertEqual(std::string("param: int"), tooltip_it->second.Get<protocol::string>(),
"InlayHint tooltip should use detail");
return result;
}
TestResult ProviderMiscTests::TestCodeLensProvider()
{
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);
protocol::CodeLensParams params;
params.textDocument.uri = uri;
protocol::RequestMessage request;
request.id = "codelens";
request.method = "textDocument/codeLens";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::CodeLens provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "CodeLens should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "CodeLens result should be array");
const auto& lenses = response.result->Get<protocol::LSPArray>();
bool found = false;
for (const auto& lens_any : lenses)
{
if (!lens_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& lens = lens_any.Get<protocol::LSPObject>();
auto data_it = lens.find("data");
if (data_it == lens.end() || !data_it->second.Is<protocol::LSPObject>())
{
continue;
}
const auto& data = data_it->second.Get<protocol::LSPObject>();
auto kind_it = data.find("kind");
auto name_it = data.find("name");
if (kind_it == data.end() || name_it == data.end())
{
continue;
}
if (!kind_it->second.Is<protocol::string>() || !name_it->second.Is<protocol::string>())
{
continue;
}
if (kind_it->second.Get<protocol::string>() == "references" &&
name_it->second.Get<protocol::string>() == "UnitFunc")
{
found = true;
break;
}
}
assertTrue(found, "CodeLens should include reference lens for UnitFunc");
return result;
}
TestResult ProviderMiscTests::TestCodeLensResolveProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::Range range{};
range.start.line = 0;
range.start.character = 0;
range.end.line = 0;
range.end.character = 0;
protocol::LSPObject data;
data["kind"] = protocol::string("references");
data["count"] = static_cast<protocol::integer>(2);
protocol::LSPObject lens;
lens["range"] = ToRangeObject(range);
lens["data"] = protocol::LSPAny(std::move(data));
protocol::RequestMessage request;
request.id = "codelens_resolve";
request.method = "codeLens/resolve";
request.params = protocol::LSPAny(std::move(lens));
::lsp::provider::code_lens::Resolve provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "CodeLens resolve should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "CodeLens resolve result should be object");
const auto& resolved = response.result->Get<protocol::LSPObject>();
auto command_it = resolved.find("command");
assertTrue(command_it != resolved.end(), "CodeLens resolve should set command");
assertTrue(command_it->second.Is<protocol::LSPObject>(), "CodeLens command should be object");
const auto& command = command_it->second.Get<protocol::LSPObject>();
auto title_it = command.find("title");
auto cmd_it = command.find("command");
assertTrue(title_it != command.end(), "CodeLens command should include title");
assertTrue(cmd_it != command.end(), "CodeLens command should include command");
assertTrue(title_it->second.Is<protocol::string>(), "CodeLens title should be string");
assertTrue(cmd_it->second.Is<protocol::string>(), "CodeLens command should be string");
assertEqual(std::string("2 references"), title_it->second.Get<protocol::string>(), "CodeLens title should use count");
assertEqual(std::string("tsl.showReferences"), cmd_it->second.Get<protocol::string>(), "CodeLens command should match");
return result;
}
TestResult ProviderMiscTests::TestPrepareCallHierarchyProvider()
{
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);
protocol::CallHierarchyParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "UnitFunc(1);");
protocol::RequestMessage request;
request.id = "call_prepare";
request.method = "textDocument/prepareCallHierarchy";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::PrepareCallHierarchy provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "PrepareCallHierarchy should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "PrepareCallHierarchy result should be array");
const auto& items = response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "PrepareCallHierarchy item should be object");
const auto& item = items[0].Get<protocol::LSPObject>();
auto name_it = item.find("name");
assertTrue(name_it != item.end(), "CallHierarchyItem should include name");
assertTrue(name_it->second.Is<protocol::string>(), "CallHierarchyItem name should be string");
assertEqual(std::string("UnitFunc"), name_it->second.Get<protocol::string>(), "PrepareCallHierarchy should target UnitFunc");
auto data_it = item.find("data");
assertTrue(data_it != item.end(), "CallHierarchyItem should include data");
assertTrue(data_it->second.Is<protocol::LSPObject>(), "CallHierarchyItem data should be object");
const auto& data = data_it->second.Get<protocol::LSPObject>();
auto symbol_id_it = data.find("symbolId");
assertTrue(symbol_id_it != data.end(), "CallHierarchyItem data should include symbolId");
assertTrue(symbol_id_it->second.Is<protocol::string>(), "CallHierarchyItem symbolId should be string");
assertTrue(!symbol_id_it->second.Get<protocol::string>().empty(), "CallHierarchyItem symbolId should not be empty");
return result;
}
TestResult ProviderMiscTests::TestCallHierarchyIncomingCallsProvider()
{
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);
protocol::CallHierarchyParams prepare_params;
prepare_params.textDocument.uri = uri;
prepare_params.position = FindPosition(content, "UnitFunc(1);");
protocol::RequestMessage prepare_request;
prepare_request.id = "call_prepare_in";
prepare_request.method = "textDocument/prepareCallHierarchy";
prepare_request.params = codec::ToLSPAny(prepare_params);
::lsp::provider::text_document::PrepareCallHierarchy prepare_provider;
auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context);
auto prepare_response = ParseResponse(prepare_json);
assertTrue(prepare_response.result.has_value(), "PrepareCallHierarchy should return result");
assertTrue(prepare_response.result->Is<protocol::LSPArray>(), "PrepareCallHierarchy result should be array");
const auto& items = prepare_response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "PrepareCallHierarchy item should be object");
protocol::LSPObject params;
params["item"] = items[0];
protocol::RequestMessage request;
request.id = "call_incoming";
request.method = "callHierarchy/incomingCalls";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::call_hierarchy::IncomingCalls provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "IncomingCalls should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "IncomingCalls result should be array");
const auto& calls = response.result->Get<protocol::LSPArray>();
bool found = false;
for (const auto& call_any : calls)
{
if (!call_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& call = call_any.Get<protocol::LSPObject>();
auto from_it = call.find("from");
auto ranges_it = call.find("fromRanges");
if (from_it == call.end() || ranges_it == call.end())
{
continue;
}
if (!from_it->second.Is<protocol::LSPObject>())
{
continue;
}
const auto& from = from_it->second.Get<protocol::LSPObject>();
auto name_it = from.find("name");
if (name_it == from.end() || !name_it->second.Is<protocol::string>())
{
continue;
}
if (name_it->second.Get<protocol::string>() == "TestDefinitions")
{
found = true;
assertTrue(ranges_it->second.Is<protocol::LSPArray>(), "IncomingCalls fromRanges should be array");
break;
}
}
assertTrue(found, "IncomingCalls should include TestDefinitions -> UnitFunc");
return result;
}
TestResult ProviderMiscTests::TestCallHierarchyOutgoingCallsProvider()
{
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);
protocol::CallHierarchyParams prepare_params;
prepare_params.textDocument.uri = uri;
prepare_params.position = FindPosition(content, "TestDefinitions();");
protocol::RequestMessage prepare_request;
prepare_request.id = "call_prepare_out";
prepare_request.method = "textDocument/prepareCallHierarchy";
prepare_request.params = codec::ToLSPAny(prepare_params);
::lsp::provider::text_document::PrepareCallHierarchy prepare_provider;
auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context);
auto prepare_response = ParseResponse(prepare_json);
assertTrue(prepare_response.result.has_value(), "PrepareCallHierarchy should return result");
assertTrue(prepare_response.result->Is<protocol::LSPArray>(), "PrepareCallHierarchy result should be array");
const auto& items = prepare_response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "PrepareCallHierarchy item should be object");
protocol::LSPObject params;
params["item"] = items[0];
protocol::RequestMessage request;
request.id = "call_outgoing";
request.method = "callHierarchy/outgoingCalls";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::call_hierarchy::OutgoingCalls provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "OutgoingCalls should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "OutgoingCalls result should be array");
const auto& calls = response.result->Get<protocol::LSPArray>();
bool found = false;
for (const auto& call_any : calls)
{
if (!call_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& call = call_any.Get<protocol::LSPObject>();
auto to_it = call.find("to");
if (to_it == call.end() || !to_it->second.Is<protocol::LSPObject>())
{
continue;
}
const auto& to = to_it->second.Get<protocol::LSPObject>();
auto name_it = to.find("name");
if (name_it == to.end() || !name_it->second.Is<protocol::string>())
{
continue;
}
if (name_it->second.Get<protocol::string>() == "UnitFunc")
{
found = true;
break;
}
}
assertTrue(found, "OutgoingCalls should include TestDefinitions -> UnitFunc");
return result;
}
TestResult ProviderMiscTests::TestPrepareTypeHierarchyProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("type_hierarchy_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::TypeHierarchyPrepareParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "Derived = class(Mid)");
protocol::RequestMessage request;
request.id = "type_prepare";
request.method = "textDocument/prepareTypeHierarchy";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::PrepareTypeHierarchy provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "PrepareTypeHierarchy should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "PrepareTypeHierarchy result should be array");
const auto& items = response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "TypeHierarchyItem should be object");
const auto& item = items[0].Get<protocol::LSPObject>();
auto name_it = item.find("name");
assertTrue(name_it != item.end(), "TypeHierarchyItem should include name");
assertTrue(name_it->second.Is<protocol::string>(), "TypeHierarchyItem name should be string");
assertEqual(std::string("Derived"), name_it->second.Get<protocol::string>(), "PrepareTypeHierarchy should target Derived");
auto data_it = item.find("data");
assertTrue(data_it != item.end(), "TypeHierarchyItem should include data");
assertTrue(data_it->second.Is<protocol::LSPObject>(), "TypeHierarchyItem data should be object");
const auto& data = data_it->second.Get<protocol::LSPObject>();
auto symbol_id_it = data.find("symbolId");
assertTrue(symbol_id_it != data.end(), "TypeHierarchyItem data should include symbolId");
assertTrue(symbol_id_it->second.Is<protocol::string>(), "TypeHierarchyItem symbolId should be string");
assertTrue(!symbol_id_it->second.Get<protocol::string>().empty(), "TypeHierarchyItem symbolId should not be empty");
return result;
}
TestResult ProviderMiscTests::TestTypeHierarchySupertypesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("type_hierarchy_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::TypeHierarchyPrepareParams prepare_params;
prepare_params.textDocument.uri = uri;
prepare_params.position = FindPosition(content, "Derived = class(Mid)");
protocol::RequestMessage prepare_request;
prepare_request.id = "type_prepare_super";
prepare_request.method = "textDocument/prepareTypeHierarchy";
prepare_request.params = codec::ToLSPAny(prepare_params);
::lsp::provider::text_document::PrepareTypeHierarchy prepare_provider;
auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context);
auto prepare_response = ParseResponse(prepare_json);
assertTrue(prepare_response.result.has_value(), "PrepareTypeHierarchy should return result");
assertTrue(prepare_response.result->Is<protocol::LSPArray>(), "PrepareTypeHierarchy result should be array");
const auto& items = prepare_response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "PrepareTypeHierarchy item should be object");
protocol::LSPObject params;
params["item"] = items[0];
protocol::RequestMessage request;
request.id = "type_supertypes";
request.method = "typeHierarchy/supertypes";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::type_hierarchy::Supertypes provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Supertypes should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Supertypes result should be array");
const auto& types = response.result->Get<protocol::LSPArray>();
bool found = false;
for (const auto& type_any : types)
{
if (!type_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& type_item = type_any.Get<protocol::LSPObject>();
auto name_it = type_item.find("name");
if (name_it == type_item.end() || !name_it->second.Is<protocol::string>())
{
continue;
}
if (name_it->second.Get<protocol::string>() == "Mid")
{
found = true;
break;
}
}
assertTrue(found, "Supertypes should include Derived -> Mid");
return result;
}
TestResult ProviderMiscTests::TestTypeHierarchySubtypesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("type_hierarchy_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::TypeHierarchyPrepareParams prepare_params;
prepare_params.textDocument.uri = uri;
prepare_params.position = FindPosition(content, "Mid = class(Base)");
protocol::RequestMessage prepare_request;
prepare_request.id = "type_prepare_sub";
prepare_request.method = "textDocument/prepareTypeHierarchy";
prepare_request.params = codec::ToLSPAny(prepare_params);
::lsp::provider::text_document::PrepareTypeHierarchy prepare_provider;
auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context);
auto prepare_response = ParseResponse(prepare_json);
assertTrue(prepare_response.result.has_value(), "PrepareTypeHierarchy should return result");
assertTrue(prepare_response.result->Is<protocol::LSPArray>(), "PrepareTypeHierarchy result should be array");
const auto& items = prepare_response.result->Get<protocol::LSPArray>();
assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item");
assertTrue(items[0].Is<protocol::LSPObject>(), "PrepareTypeHierarchy item should be object");
protocol::LSPObject params;
params["item"] = items[0];
protocol::RequestMessage request;
request.id = "type_subtypes";
request.method = "typeHierarchy/subtypes";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::type_hierarchy::Subtypes provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Subtypes should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Subtypes result should be array");
const auto& types = response.result->Get<protocol::LSPArray>();
bool found = false;
for (const auto& type_any : types)
{
if (!type_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& type_item = type_any.Get<protocol::LSPObject>();
auto name_it = type_item.find("name");
if (name_it == type_item.end() || !name_it->second.Is<protocol::string>())
{
continue;
}
if (name_it->second.Get<protocol::string>() == "Derived")
{
found = true;
break;
}
}
assertTrue(found, "Subtypes should include Mid -> Derived");
return result;
}
namespace
{
struct TempDirGuard
{
std::filesystem::path path;
~TempDirGuard()
{
if (path.empty())
{
return;
}
std::error_code ec;
std::filesystem::remove_all(path, ec);
}
};
std::filesystem::path MakeTempDir(std::string_view prefix)
{
auto now = std::chrono::steady_clock::now().time_since_epoch().count();
std::filesystem::path dir = std::filesystem::temp_directory_path() /
(std::string(prefix) + "-" + std::to_string(now));
std::filesystem::create_directories(dir);
return dir;
}
void WriteTextFile(const std::filesystem::path& path, const std::string& content)
{
std::filesystem::create_directories(path.parent_path());
std::ofstream file(path, std::ios::binary);
if (!file.is_open())
{
throw std::runtime_error("Failed to write file: " + path.string());
}
file << content;
}
std::string SimpleWorkspaceFunction(std::string_view name)
{
std::string result;
result += "function ";
result += name;
result += "(): integer;\n\n";
result += "function ";
result += name;
result += "(): integer;\n";
result += "begin\n";
result += " return 1;\n";
result += "end;\n";
return result;
}
protocol::LSPAny BuildDidCreateFilesParams(const std::vector<std::string>& uris)
{
protocol::LSPArray files;
files.reserve(uris.size());
for (const auto& uri : uris)
{
protocol::LSPObject file;
file["uri"] = protocol::string(uri);
files.emplace_back(std::move(file));
}
protocol::LSPObject params;
params["files"] = std::move(files);
return protocol::LSPAny(std::move(params));
}
protocol::LSPAny BuildDidDeleteFilesParams(const std::vector<std::string>& uris)
{
return BuildDidCreateFilesParams(uris);
}
protocol::LSPAny BuildDidRenameFilesParams(const std::vector<std::pair<std::string, std::string>>& files)
{
protocol::LSPArray entries;
entries.reserve(files.size());
for (const auto& [old_uri, new_uri] : files)
{
protocol::LSPObject entry;
entry["oldUri"] = protocol::string(old_uri);
entry["newUri"] = protocol::string(new_uri);
entries.emplace_back(std::move(entry));
}
protocol::LSPObject params;
params["files"] = std::move(entries);
return protocol::LSPAny(std::move(params));
}
protocol::LSPAny BuildDidChangeWatchedFilesParams(const std::vector<std::pair<std::string, protocol::integer>>& changes)
{
protocol::LSPArray entries;
entries.reserve(changes.size());
for (const auto& [uri, type] : changes)
{
protocol::LSPObject entry;
entry["uri"] = protocol::string(uri);
entry["type"] = type;
entries.emplace_back(std::move(entry));
}
protocol::LSPObject params;
params["changes"] = std::move(entries);
return protocol::LSPAny(std::move(params));
}
}
TestResult ProviderMiscTests::TestDidCreateFilesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto dir = MakeTempDir("tsl-didCreateFiles");
TempDirGuard guard{ dir };
env.hub.symbols().LoadWorkspace(ToUri(dir));
auto file_path = dir / "created_script.tsl";
WriteTextFile(file_path, SimpleWorkspaceFunction("CreatedFunc"));
auto file_uri = ToUri(file_path);
protocol::NotificationMessage notification;
notification.method = "workspace/didCreateFiles";
notification.params = BuildDidCreateFilesParams({ file_uri });
::lsp::provider::workspace::DidCreateFiles provider;
provider.HandleNotification(notification, env.context);
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "CreatedFunc" && item.uri == file_uri;
});
assertTrue(found, "didCreateFiles should index CreatedFunc");
return result;
}
TestResult ProviderMiscTests::TestDidDeleteFilesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto dir = MakeTempDir("tsl-didDeleteFiles");
TempDirGuard guard{ dir };
auto file_path = dir / "delete_script.tsl";
WriteTextFile(file_path, SimpleWorkspaceFunction("DeletedFunc"));
auto file_uri = ToUri(file_path);
env.hub.symbols().LoadWorkspace(ToUri(dir));
{
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "DeletedFunc" && item.uri == file_uri;
});
assertTrue(found, "Workspace should index DeletedFunc before deletion");
}
std::filesystem::remove(file_path);
protocol::NotificationMessage notification;
notification.method = "workspace/didDeleteFiles";
notification.params = BuildDidDeleteFilesParams({ file_uri });
::lsp::provider::workspace::DidDeleteFiles provider;
provider.HandleNotification(notification, env.context);
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "DeletedFunc";
});
assertFalse(found, "didDeleteFiles should remove DeletedFunc from index");
return result;
}
TestResult ProviderMiscTests::TestDidRenameFilesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto dir = MakeTempDir("tsl-didRenameFiles");
TempDirGuard guard{ dir };
auto old_path = dir / "old_name.tsl";
auto new_path = dir / "new_name.tsl";
WriteTextFile(old_path, SimpleWorkspaceFunction("RenamedFunc"));
auto old_uri = ToUri(old_path);
auto new_uri = ToUri(new_path);
env.hub.symbols().LoadWorkspace(ToUri(dir));
std::filesystem::rename(old_path, new_path);
protocol::NotificationMessage notification;
notification.method = "workspace/didRenameFiles";
notification.params = BuildDidRenameFilesParams({ { old_uri, new_uri } });
::lsp::provider::workspace::DidRenameFiles provider;
provider.HandleNotification(notification, env.context);
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found_old = false;
bool found_new = false;
for (const auto& item : indexed)
{
if (item.name != "RenamedFunc")
{
continue;
}
if (item.uri == old_uri)
{
found_old = true;
}
if (item.uri == new_uri)
{
found_new = true;
}
}
assertFalse(found_old, "didRenameFiles should remove old URI entry");
assertTrue(found_new, "didRenameFiles should index new URI entry");
return result;
}
TestResult ProviderMiscTests::TestDidChangeWatchedFilesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto dir = MakeTempDir("tsl-didChangeWatchedFiles");
TempDirGuard guard{ dir };
auto file_path = dir / "watched_script.tsl";
WriteTextFile(file_path, SimpleWorkspaceFunction("WatchedFunc"));
auto file_uri = ToUri(file_path);
env.hub.symbols().LoadWorkspace(ToUri(dir));
{
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WatchedFunc" && item.uri == file_uri;
});
assertTrue(found, "Workspace should index WatchedFunc before change");
}
WriteTextFile(file_path, SimpleWorkspaceFunction("WatchedChangedFunc"));
protocol::NotificationMessage change_notification;
change_notification.method = "workspace/didChangeWatchedFiles";
change_notification.params = BuildDidChangeWatchedFilesParams({ { file_uri, 2 } });
::lsp::provider::workspace::DidChangeWatchedFiles provider;
provider.HandleNotification(change_notification, env.context);
{
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found_old = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WatchedFunc";
});
bool found_new = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WatchedChangedFunc" && item.uri == file_uri;
});
assertFalse(found_old, "didChangeWatchedFiles should remove WatchedFunc after reindex");
assertTrue(found_new, "didChangeWatchedFiles should reindex WatchedChangedFunc");
}
std::filesystem::remove(file_path);
protocol::NotificationMessage delete_notification;
delete_notification.method = "workspace/didChangeWatchedFiles";
delete_notification.params = BuildDidChangeWatchedFilesParams({ { file_uri, 3 } });
provider.HandleNotification(delete_notification, env.context);
{
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function);
bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WatchedChangedFunc";
});
assertFalse(found, "didChangeWatchedFiles should remove WatchedChangedFunc on delete");
}
return result;
}
TestResult ProviderMiscTests::TestDidChangeConfigurationProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject tsl_settings;
tsl_settings["format"] = true;
protocol::LSPObject settings;
settings["tsl"] = std::move(tsl_settings);
protocol::LSPObject params;
params["settings"] = std::move(settings);
protocol::NotificationMessage notification;
notification.method = "workspace/didChangeConfiguration";
notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::workspace::DidChangeConfiguration provider;
provider.HandleNotification(notification, env.context);
auto stored = env.hub.GetConfiguration();
assertTrue(stored.Is<protocol::LSPObject>(), "didChangeConfiguration should store object settings");
const auto& stored_obj = stored.Get<protocol::LSPObject>();
assertTrue(stored_obj.contains("tsl"), "stored settings should include tsl section");
assertTrue(env.events.empty(), "didChangeConfiguration should not trigger lifecycle events");
return result;
}
TestResult ProviderMiscTests::TestDidChangeWorkspaceFoldersProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto workspace_uri = ToUri(FixturePath("workspace"));
assertTrue(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "Workspace symbols should start empty");
protocol::LSPArray added;
added.emplace_back(protocol::LSPObject{
{ "uri", workspace_uri },
{ "name", "workspace" },
});
protocol::LSPObject event;
event["added"] = std::move(added);
event["removed"] = protocol::LSPArray{};
protocol::LSPObject params;
params["event"] = std::move(event);
protocol::NotificationMessage add_notification;
add_notification.method = "workspace/didChangeWorkspaceFolders";
add_notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::workspace::DidChangeWorkspaceFolders provider;
provider.HandleNotification(add_notification, env.context);
env.scheduler.WaitAll();
assertFalse(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "didChangeWorkspaceFolders should index workspace folder");
assertEqual(static_cast<std::size_t>(1), env.hub.GetWorkspaceFolders().size(), "didChangeWorkspaceFolders should track added folders");
protocol::LSPArray removed;
removed.emplace_back(protocol::LSPObject{
{ "uri", workspace_uri },
{ "name", "workspace" },
});
protocol::LSPObject remove_event;
remove_event["added"] = protocol::LSPArray{};
remove_event["removed"] = std::move(removed);
protocol::LSPObject remove_params;
remove_params["event"] = std::move(remove_event);
protocol::NotificationMessage remove_notification;
remove_notification.method = "workspace/didChangeWorkspaceFolders";
remove_notification.params = protocol::LSPAny(std::move(remove_params));
provider.HandleNotification(remove_notification, env.context);
env.scheduler.WaitAll();
assertTrue(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "didChangeWorkspaceFolders should remove workspace folder symbols");
assertTrue(env.hub.GetWorkspaceFolders().empty(), "didChangeWorkspaceFolders should track removed folders");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceSymbolProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
protocol::LSPObject params;
params["query"] = "Workspace";
protocol::RequestMessage request;
request.id = "ws_symbol";
request.method = "workspace/symbol";
request.params = protocol::LSPAny(params);
::lsp::provider::workspace::Symbol provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Workspace symbol should not return error");
assertTrue(response.result.has_value(), "Workspace symbol should return result");
auto symbols = codec::FromLSPAny.template operator()<std::vector<protocol::WorkspaceSymbol>>(response.result.value());
assertTrue(!symbols.empty(), "Workspace symbol should return matches");
bool found_unit = std::any_of(symbols.begin(), symbols.end(), [](const protocol::WorkspaceSymbol& symbol) {
return symbol.name == "WorkspaceUnit";
});
bool found_func = std::any_of(symbols.begin(), symbols.end(), [](const protocol::WorkspaceSymbol& symbol) {
return symbol.name == "WorkspaceFunc";
});
assertTrue(found_unit, "Workspace symbol should include WorkspaceUnit");
assertTrue(found_func, "Workspace symbol should include WorkspaceFunc");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceDiagnosticProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto error_path = FixturePath("code_action_missing_semicolon.tsl");
auto error_content = ReadTextFile(error_path);
auto error_uri = ToUri(error_path);
OpenDocument(env.hub, error_uri, error_content, 1);
auto ok_path = FixturePath("main_unit.tsf");
auto ok_content = ReadTextFile(ok_path);
auto ok_uri = ToUri(ok_path);
OpenDocument(env.hub, ok_uri, ok_content, 1);
protocol::WorkspaceDiagnosticParams params;
protocol::RequestMessage request;
request.id = "ws_diag";
request.method = "workspace/diagnostic";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::Diagnostic provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Workspace diagnostic should not return error");
assertTrue(response.result.has_value(), "Workspace diagnostic should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "Workspace diagnostic result should be object");
const auto& report = response.result->Get<protocol::LSPObject>();
auto items_it = report.find("items");
assertTrue(items_it != report.end(), "Workspace diagnostic report should include items");
assertTrue(items_it->second.Is<protocol::LSPArray>(), "Workspace diagnostic items should be array");
bool found_error = false;
for (const auto& item_any : items_it->second.Get<protocol::LSPArray>())
{
if (!item_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& item = item_any.Get<protocol::LSPObject>();
auto uri_it = item.find("uri");
if (uri_it == item.end() || !uri_it->second.Is<protocol::string>())
{
continue;
}
if (uri_it->second.Get<protocol::string>() != error_uri)
{
continue;
}
found_error = true;
auto kind_it = item.find("kind");
assertTrue(kind_it != item.end(), "Workspace diagnostic item should include kind");
assertTrue(kind_it->second.Is<protocol::string>(), "Workspace diagnostic kind should be string");
assertEqual(std::string("full"), kind_it->second.Get<protocol::string>(), "Workspace diagnostic kind should be full");
auto diags_it = item.find("items");
assertTrue(diags_it != item.end(), "Workspace diagnostic item should include items");
assertTrue(diags_it->second.Is<protocol::LSPArray>(), "Workspace diagnostic items should be array");
assertTrue(!diags_it->second.Get<protocol::LSPArray>().empty(),
"Workspace diagnostic should include diagnostics for error document");
break;
}
assertTrue(found_error, "Workspace diagnostic should include opened documents");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceConfigurationProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject nested;
nested["b"] = protocol::string("x");
protocol::LSPObject tsl;
tsl["foo"] = static_cast<protocol::integer>(42);
tsl["nested"] = std::move(nested);
protocol::LSPObject settings;
settings["tsl"] = std::move(tsl);
protocol::LSPObject dc_params;
dc_params["settings"] = std::move(settings);
protocol::NotificationMessage did_change;
did_change.method = "workspace/didChangeConfiguration";
did_change.params = protocol::LSPAny(std::move(dc_params));
::lsp::provider::workspace::DidChangeConfiguration dc_provider;
dc_provider.HandleNotification(did_change, env.context);
protocol::LSPArray items;
items.emplace_back(protocol::LSPObject{
{ "scopeUri", protocol::string(ToUri(FixturePath("main_unit.tsf"))) },
{ "section", protocol::string("tsl.foo") },
});
items.emplace_back(protocol::LSPObject{
{ "section", protocol::string("tsl.nested.b") },
});
items.emplace_back(protocol::LSPObject{
{ "section", protocol::string("missing") },
});
protocol::LSPObject params;
params["items"] = std::move(items);
protocol::RequestMessage request;
request.id = "cfg";
request.method = "workspace/configuration";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::workspace::Configuration provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "workspace/configuration should not return error");
assertTrue(response.result.has_value(), "workspace/configuration should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "workspace/configuration result should be array");
const auto& values = response.result->Get<protocol::LSPArray>();
assertEqual(static_cast<std::size_t>(3), values.size(), "workspace/configuration result size should match items");
assertTrue(values[0].Is<protocol::integer>(), "workspace/configuration should resolve tsl.foo");
assertEqual(static_cast<protocol::integer>(42), values[0].Get<protocol::integer>(), "tsl.foo should equal 42");
assertTrue(values[1].Is<protocol::string>(), "workspace/configuration should resolve tsl.nested.b");
assertEqual(std::string("x"), values[1].Get<protocol::string>(), "tsl.nested.b should equal x");
assertTrue(values[2].Is<std::nullptr_t>(), "workspace/configuration should return null for missing section");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceApplyEditProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto uri = ToUri(FixturePath("inlay_hint_case.tsl"));
std::string content = "var count := 1;\n";
OpenDocument(env.hub, uri, content, 1);
auto pos = FindPosition(content, "1");
protocol::Range range;
range.start = pos;
range.end = pos;
range.end.character = pos.character + 1;
protocol::LSPObject edit_item;
edit_item["range"] = ToRangeObject(range);
edit_item["newText"] = protocol::string("2");
protocol::LSPArray edits;
edits.emplace_back(std::move(edit_item));
protocol::LSPObject changes;
changes[uri] = protocol::LSPAny(std::move(edits));
protocol::LSPObject edit;
edit["changes"] = std::move(changes);
protocol::LSPObject params;
params["edit"] = std::move(edit);
protocol::RequestMessage request;
request.id = "apply";
request.method = "workspace/applyEdit";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::workspace::ApplyEdit provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "workspace/applyEdit should not return error");
assertTrue(response.result.has_value(), "workspace/applyEdit should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "workspace/applyEdit result should be object");
const auto& obj = response.result->Get<protocol::LSPObject>();
auto applied_it = obj.find("applied");
assertTrue(applied_it != obj.end(), "workspace/applyEdit result should include applied");
assertTrue(applied_it->second.Is<protocol::boolean>(), "workspace/applyEdit applied should be bool");
assertTrue(applied_it->second.Get<protocol::boolean>(), "workspace/applyEdit should return applied=true");
auto updated = env.hub.documents().GetContent(uri);
assertTrue(updated.has_value(), "workspace/applyEdit should update open document");
assertEqual(std::string("var count := 2;\n"), updated.value(), "workspace/applyEdit should apply edits");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceWorkspaceFoldersProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::WorkspaceFolder folder;
folder.uri = ToUri(FixturePath("workspace"));
folder.name = "workspace";
env.hub.SetWorkspaceFolders({ folder });
protocol::RequestMessage request;
request.id = "folders";
request.method = "workspace/workspaceFolders";
request.params = std::nullopt;
::lsp::provider::workspace::WorkspaceFolders provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "workspace/workspaceFolders should not return error");
assertTrue(response.result.has_value(), "workspace/workspaceFolders should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "workspace/workspaceFolders result should be array");
const auto& folders = response.result->Get<protocol::LSPArray>();
assertEqual(static_cast<std::size_t>(1), folders.size(), "workspace/workspaceFolders should return configured folders");
assertTrue(folders.front().Is<protocol::LSPObject>(), "workspace/workspaceFolders item should be object");
const auto& item = folders.front().Get<protocol::LSPObject>();
auto uri_it = item.find("uri");
auto name_it = item.find("name");
assertTrue(uri_it != item.end(), "workspace/workspaceFolders item should include uri");
assertTrue(uri_it->second.Is<protocol::string>(), "workspace/workspaceFolders uri should be string");
assertEqual(folder.uri, uri_it->second.Get<protocol::string>(), "workspace/workspaceFolders uri should match");
assertTrue(name_it != item.end(), "workspace/workspaceFolders item should include name");
assertTrue(name_it->second.Is<protocol::string>(), "workspace/workspaceFolders name should be string");
assertEqual(folder.name, name_it->second.Get<protocol::string>(), "workspace/workspaceFolders name should match");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceRefreshProviders()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto check_null_result = [&](auto& provider, std::string_view method) {
protocol::RequestMessage request;
request.id = "refresh";
request.method = std::string(method);
request.params = std::nullopt;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), std::string(method) + " should not return error");
assertTrue(response.result.has_value(), std::string(method) + " should return result");
assertTrue(response.result->template Is<std::nullptr_t>(), std::string(method) + " result should be null");
};
::lsp::provider::workspace::CodeLensRefresh code_lens;
check_null_result(code_lens, "workspace/codeLens/refresh");
::lsp::provider::workspace::DiagnosticRefresh diagnostic;
check_null_result(diagnostic, "workspace/diagnostic/refresh");
::lsp::provider::workspace::InlayHintRefresh inlay;
check_null_result(inlay, "workspace/inlayHint/refresh");
::lsp::provider::workspace::InlineValueRefresh inline_value;
check_null_result(inline_value, "workspace/inlineValue/refresh");
::lsp::provider::workspace::SemanticTokensRefresh semantic;
check_null_result(semantic, "workspace/semanticTokens/refresh");
return result;
}
TestResult ProviderMiscTests::TestClientCapabilityProviders()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
{
protocol::LSPArray registrations;
registrations.emplace_back(protocol::LSPObject{
{ "id", protocol::string("reg_1") },
{ "method", protocol::string("workspace/didChangeConfiguration") },
});
protocol::LSPObject params;
params["registrations"] = std::move(registrations);
protocol::RequestMessage request;
request.id = "reg";
request.method = "client/registerCapability";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::client::RegisterCapability provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "client/registerCapability should not return error");
assertTrue(response.result.has_value(), "client/registerCapability should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "client/registerCapability result should be null");
}
{
protocol::LSPArray unregistrations;
unregistrations.emplace_back(protocol::LSPObject{
{ "id", protocol::string("reg_1") },
{ "method", protocol::string("workspace/didChangeConfiguration") },
});
protocol::LSPObject params;
params["unregistrations"] = std::move(unregistrations);
protocol::RequestMessage request;
request.id = "unreg";
request.method = "client/unregisterCapability";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::client::UnregisterCapability provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "client/unregisterCapability should not return error");
assertTrue(response.result.has_value(), "client/unregisterCapability should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "client/unregisterCapability result should be null");
}
return result;
}
TestResult ProviderMiscTests::TestWindowWorkDoneProgressCreateProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject params;
params["token"] = protocol::string("progress_token");
protocol::RequestMessage request;
request.id = "progress";
request.method = "window/workDoneProgress/create";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::window::WorkDoneProgressCreate provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "window/workDoneProgress/create should not return error");
assertTrue(response.result.has_value(), "window/workDoneProgress/create should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "window/workDoneProgress/create result should be null");
return result;
}
TestResult ProviderMiscTests::TestWindowShowMessageRequestProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPArray actions;
actions.emplace_back(protocol::LSPObject{
{ "title", protocol::string("OK") },
});
protocol::LSPObject params;
params["type"] = static_cast<protocol::integer>(protocol::MessageType::Info);
params["message"] = protocol::string("Test showMessageRequest");
params["actions"] = std::move(actions);
protocol::RequestMessage request;
request.id = "msgreq";
request.method = "window/showMessageRequest";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::window::ShowMessageRequest provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "window/showMessageRequest should not return error");
assertTrue(response.result.has_value(), "window/showMessageRequest should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "window/showMessageRequest result should be object");
const auto& obj = response.result->Get<protocol::LSPObject>();
auto title_it = obj.find("title");
assertTrue(title_it != obj.end(), "window/showMessageRequest result should include title");
assertTrue(title_it->second.Is<protocol::string>(), "window/showMessageRequest title should be string");
assertEqual(std::string("OK"), title_it->second.Get<protocol::string>(), "window/showMessageRequest should return first action");
return result;
}
TestResult ProviderMiscTests::TestWindowShowDocumentProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject params;
params["uri"] = protocol::string(ToUri(FixturePath("main_unit.tsf")));
params["takeFocus"] = true;
protocol::RequestMessage request;
request.id = "showdoc";
request.method = "window/showDocument";
request.params = protocol::LSPAny(std::move(params));
::lsp::provider::window::ShowDocument provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "window/showDocument should not return error");
assertTrue(response.result.has_value(), "window/showDocument should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "window/showDocument result should be object");
const auto& obj = response.result->Get<protocol::LSPObject>();
auto success_it = obj.find("success");
assertTrue(success_it != obj.end(), "window/showDocument result should include success");
assertTrue(success_it->second.Is<protocol::boolean>(), "window/showDocument success should be bool");
assertFalse(success_it->second.Get<protocol::boolean>(), "window/showDocument should return success=false");
return result;
}
TestResult ProviderMiscTests::TestWindowMessageNotifications()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
{
protocol::LSPObject params;
params["type"] = static_cast<protocol::integer>(protocol::MessageType::Log);
params["message"] = protocol::string("Test logMessage");
protocol::NotificationMessage notification;
notification.method = "window/logMessage";
notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::window::LogMessage provider;
provider.HandleNotification(notification, env.context);
}
{
protocol::LSPObject params;
params["type"] = static_cast<protocol::integer>(protocol::MessageType::Info);
params["message"] = protocol::string("Test showMessage");
protocol::NotificationMessage notification;
notification.method = "window/showMessage";
notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::window::ShowMessage provider;
provider.HandleNotification(notification, env.context);
}
assertTrue(env.events.empty(), "window message notifications should not trigger lifecycle events");
return result;
}
TestResult ProviderMiscTests::TestTelemetryEventNotification()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject params;
params["event"] = protocol::string("test_event");
params["value"] = static_cast<protocol::integer>(1);
protocol::NotificationMessage notification;
notification.method = "telemetry/event";
notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::telemetry::Event provider;
provider.HandleNotification(notification, env.context);
assertTrue(env.events.empty(), "telemetry/event should not trigger lifecycle events");
return result;
}
TestResult ProviderMiscTests::TestPublishDiagnosticsNotification()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto uri = ToUri(FixturePath("main_unit.tsf"));
protocol::Range range{};
range.start.line = 0;
range.start.character = 0;
range.end.line = 0;
range.end.character = 1;
protocol::LSPArray diagnostics;
diagnostics.emplace_back(protocol::LSPObject{
{ "range", ToRangeObject(range) },
{ "message", protocol::string("Test diagnostic") },
});
protocol::LSPObject params;
params["uri"] = protocol::string(uri);
params["version"] = static_cast<protocol::integer>(1);
params["diagnostics"] = std::move(diagnostics);
protocol::NotificationMessage notification;
notification.method = "textDocument/publishDiagnostics";
notification.params = protocol::LSPAny(std::move(params));
::lsp::provider::text_document::PublishDiagnostics provider;
provider.HandleNotification(notification, env.context);
assertTrue(env.events.empty(), "publishDiagnostics should not trigger lifecycle events");
return result;
}
TestResult ProviderMiscTests::TestSemanticTokensProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RequestMessage request;
request.id = "sem";
request.method = "textDocument/semanticTokens/full";
protocol::SemanticTokensParams params;
params.textDocument.uri = uri;
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::SemanticTokensFull provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Semantic tokens should not return error");
assertTrue(response.result.has_value(), "Semantic tokens should return result");
auto tokens = codec::FromLSPAny.template operator()<protocol::SemanticTokens>(response.result.value());
assertTrue(tokens.resultId.has_value(), "Semantic tokens should include resultId");
assertTrue(!tokens.data.empty(), "Semantic tokens data should not be empty");
assertTrue(tokens.data.size() % 5 == 0, "Semantic tokens data should be in 5-tuples");
return result;
}
TestResult ProviderMiscTests::TestSignatureHelpProvider()
{
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);
protocol::TextDocumentPositionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "UnitFunc(1);");
params.position.character += static_cast<protocol::uinteger>(std::string("UnitFunc(").size());
protocol::RequestMessage request;
request.id = "sig";
request.method = "textDocument/signatureHelp";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::SignatureHelp provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Signature help should not return error");
assertTrue(response.result.has_value(), "Signature help should return result");
auto sig_help = codec::FromLSPAny.template operator()<std::optional<protocol::SignatureHelp>>(response.result.value());
assertTrue(sig_help.has_value(), "Signature help should return data");
assertTrue(!sig_help->signatures.empty(), "Signature help should include signatures");
assertTrue(sig_help->signatures.front().label.find("UnitFunc") != std::string::npos,
"Signature help label should mention UnitFunc");
assertTrue(sig_help->activeSignature.has_value(), "Signature help should include activeSignature");
assertTrue(sig_help->activeParameter.has_value(), "Signature help should include activeParameter");
return result;
}
TestResult ProviderMiscTests::TestCodeActionProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("code_action_missing_semicolon.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto tree = env.hub.parser().GetTree(uri);
assertTrue(tree != nullptr, "Parser should produce syntax tree");
auto errors = language::ast::Deserializer::DiagnoseSyntax(ts_tree_root_node(tree), content);
assertTrue(!errors.empty(), "Fixture should produce syntax errors");
protocol::LSPArray diagnostics;
for (const auto& error : errors)
{
protocol::LSPObject diagnostic;
diagnostic["range"] = protocol::LSPObject{
{ "start", protocol::LSPObject{ { "line", static_cast<protocol::integer>(error.location.start_line) },
{ "character", static_cast<protocol::integer>(error.location.start_column) } } },
{ "end", protocol::LSPObject{ { "line", static_cast<protocol::integer>(error.location.end_line) },
{ "character", static_cast<protocol::integer>(error.location.end_column) } } },
};
diagnostic["message"] = error.message;
diagnostics.emplace_back(std::move(diagnostic));
}
protocol::LSPObject params;
params["textDocument"] = protocol::LSPObject{ { "uri", uri } };
params["range"] = protocol::LSPObject{
{ "start", protocol::LSPObject{ { "line", 0 }, { "character", 0 } } },
{ "end", protocol::LSPObject{ { "line", 9999 }, { "character", 0 } } },
};
params["context"] = protocol::LSPObject{ { "diagnostics", std::move(diagnostics) } };
protocol::RequestMessage request;
request.id = "code_action";
request.method = "textDocument/codeAction";
request.params = protocol::LSPAny(params);
::lsp::provider::text_document::CodeAction provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Code action should not return error");
assertTrue(response.result.has_value(), "Code action should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Code action result should be an array");
const auto& actions = response.result->Get<protocol::LSPArray>();
assertTrue(!actions.empty(), "Code action should return actions");
bool found_insert = false;
bool found_fix_all = false;
for (const auto& action_any : actions)
{
if (!action_any.Is<protocol::LSPObject>())
{
continue;
}
const auto& action = action_any.Get<protocol::LSPObject>();
auto title_it = action.find("title");
auto kind_it = action.find("kind");
if (title_it == action.end() || kind_it == action.end())
{
continue;
}
if (!title_it->second.Is<protocol::string>() || !kind_it->second.Is<protocol::string>())
{
continue;
}
const auto& title = title_it->second.Get<protocol::string>();
const auto& kind = kind_it->second.Get<protocol::string>();
if (kind == protocol::CodeActionKindLiterals::QuickFix && title.find("Insert") != std::string::npos)
{
found_insert = true;
}
if (kind == protocol::CodeActionKindLiterals::SourceFixAll && title.find("semicolons") != std::string::npos)
{
found_fix_all = true;
}
}
assertTrue(found_insert, "Code action should include insert fix");
assertTrue(found_fix_all, "Code action should include fixAll missing semicolons");
return result;
}
TestResult ProviderMiscTests::TestCodeActionResolveProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject action;
action["title"] = protocol::string("Resolve me");
action["kind"] = protocol::string(protocol::CodeActionKindLiterals::QuickFix);
action["data"] = protocol::LSPAny(protocol::LSPObject{
{ "kind", protocol::string("noop") },
});
protocol::RequestMessage request;
request.id = "code_action_resolve";
request.method = "codeAction/resolve";
request.params = protocol::LSPAny(std::move(action));
::lsp::provider::code_action::Resolve provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "CodeAction resolve should not return error");
assertTrue(response.result.has_value(), "CodeAction resolve should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "CodeAction resolve result should be object");
const auto& resolved = response.result->Get<protocol::LSPObject>();
auto title_it = resolved.find("title");
assertTrue(title_it != resolved.end(), "Resolved code action should include title");
assertTrue(title_it->second.Is<protocol::string>(), "Resolved code action title should be string");
assertEqual(std::string("Resolve me"), title_it->second.Get<protocol::string>(), "Resolved title should match");
return result;
}
TestResult ProviderMiscTests::TestDocumentFormattingProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto uri = ToUri(FixturePath("inlay_hint_case.tsl"));
std::string content = "var count := 1; \nvar name := \"alpha\";\n";
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentFormattingParams params;
params.textDocument.uri = uri;
params.options.tabSize = 4;
params.options.insertSpaces = true;
protocol::RequestMessage request;
request.id = "fmt";
request.method = "textDocument/formatting";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Formatting provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Formatting should not return error");
assertTrue(response.result.has_value(), "Formatting should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Formatting result should be array");
const auto& edits = response.result->Get<protocol::LSPArray>();
assertTrue(!edits.empty(), "Formatting should return edits when content differs");
assertTrue(edits.front().Is<protocol::LSPObject>(), "Formatting edit should be object");
const auto& edit = edits.front().Get<protocol::LSPObject>();
auto new_text_it = edit.find("newText");
assertTrue(new_text_it != edit.end(), "Formatting edit should include newText");
assertTrue(new_text_it->second.Is<protocol::string>(), "Formatting newText should be string");
const auto& new_text = new_text_it->second.Get<protocol::string>();
assertTrue(new_text.find("1; ") == std::string::npos, "Formatting should trim trailing whitespace");
return result;
}
TestResult ProviderMiscTests::TestDocumentRangeFormattingProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto uri = ToUri(FixturePath("inlay_hint_case.tsl"));
std::string content = "var count := 1; \nvar name := \"alpha\";\n";
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentRangeFormattingParams params;
params.textDocument.uri = uri;
params.range.start.line = 0;
params.range.start.character = 0;
params.range.end.line = 0;
params.range.end.character = 9999;
params.options.tabSize = 4;
params.options.insertSpaces = true;
protocol::RequestMessage request;
request.id = "range_fmt";
request.method = "textDocument/rangeFormatting";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::RangeFormatting provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Range formatting should not return error");
assertTrue(response.result.has_value(), "Range formatting should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Range formatting result should be array");
const auto& edits = response.result->Get<protocol::LSPArray>();
assertTrue(!edits.empty(), "Range formatting should return edits");
assertTrue(edits.front().Is<protocol::LSPObject>(), "Range formatting edit should be object");
const auto& edit = edits.front().Get<protocol::LSPObject>();
auto new_text_it = edit.find("newText");
assertTrue(new_text_it != edit.end(), "Range formatting edit should include newText");
assertTrue(new_text_it->second.Is<protocol::string>(), "Range formatting newText should be string");
const auto& new_text = new_text_it->second.Get<protocol::string>();
assertTrue(new_text.find("1; ") == std::string::npos, "Range formatting should trim trailing whitespace");
return result;
}
TestResult ProviderMiscTests::TestDocumentOnTypeFormattingProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto uri = ToUri(FixturePath("inlay_hint_case.tsl"));
std::string content = "var count := 1; \nvar name := \"alpha\";\n";
OpenDocument(env.hub, uri, content, 1);
protocol::DocumentOnTypeFormattingParams params;
params.textDocument.uri = uri;
params.position.line = 0;
params.position.character = 0;
params.ch = ";";
params.options.tabSize = 4;
params.options.insertSpaces = true;
protocol::RequestMessage request;
request.id = "on_type_fmt";
request.method = "textDocument/onTypeFormatting";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::OnTypeFormatting provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "OnType formatting should not return error");
assertTrue(response.result.has_value(), "OnType formatting should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "OnType formatting result should be array");
const auto& edits = response.result->Get<protocol::LSPArray>();
assertTrue(!edits.empty(), "OnType formatting should return edits");
assertTrue(edits.front().Is<protocol::LSPObject>(), "OnType formatting edit should be object");
const auto& edit = edits.front().Get<protocol::LSPObject>();
auto new_text_it = edit.find("newText");
assertTrue(new_text_it != edit.end(), "OnType formatting edit should include newText");
assertTrue(new_text_it->second.Is<protocol::string>(), "OnType formatting newText should be string");
assertEqual(std::string(""), new_text_it->second.Get<protocol::string>(), "OnType formatting should delete whitespace");
return result;
}
TestResult ProviderMiscTests::TestInlineValueProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("inlay_hint_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::InlineValueParams params;
params.textDocument.uri = uri;
params.range.start.line = 0;
params.range.start.character = 0;
params.range.end.line = 9999;
params.range.end.character = 0;
params.context.frameId = 0;
params.context.stoppedLocation = params.range;
protocol::RequestMessage request;
request.id = "inline_value";
request.method = "textDocument/inlineValue";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::InlineValue provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "InlineValue should not return error");
assertTrue(response.result.has_value(), "InlineValue should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "InlineValue result should be array");
return result;
}
TestResult ProviderMiscTests::TestMonikerProvider()
{
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);
protocol::MonikerParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "UnitFunc(a: integer): integer;");
protocol::RequestMessage request;
request.id = "moniker";
request.method = "textDocument/moniker";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Moniker provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "Moniker should not return error");
assertTrue(response.result.has_value(), "Moniker should return result");
assertTrue(response.result->Is<protocol::LSPArray>(), "Moniker result should be array");
const auto& monikers = response.result->Get<protocol::LSPArray>();
assertTrue(!monikers.empty(), "Moniker should return at least one entry");
assertTrue(monikers.front().Is<protocol::LSPObject>(), "Moniker entry should be object");
const auto& moniker = monikers.front().Get<protocol::LSPObject>();
auto scheme_it = moniker.find("scheme");
auto ident_it = moniker.find("identifier");
assertTrue(scheme_it != moniker.end(), "Moniker should include scheme");
assertTrue(ident_it != moniker.end(), "Moniker should include identifier");
assertTrue(scheme_it->second.Is<protocol::string>(), "Moniker scheme should be string");
assertTrue(ident_it->second.Is<protocol::string>(), "Moniker identifier should be string");
assertEqual(std::string("tsl"), scheme_it->second.Get<protocol::string>(), "Moniker scheme should be tsl");
assertTrue(ident_it->second.Get<protocol::string>().find(uri) != std::string::npos, "Moniker identifier should include uri");
return result;
}
TestResult ProviderMiscTests::TestExecuteCommandProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
{
protocol::ExecuteCommandParams params;
params.command = "tsl.noop";
protocol::RequestMessage request;
request.id = "exec_noop";
request.method = "workspace/executeCommand";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::ExecuteCommand provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "ExecuteCommand noop should not return error");
assertTrue(response.result.has_value(), "ExecuteCommand noop should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "ExecuteCommand noop should return null");
}
{
auto workspace_uri = ToUri(FixturePath("workspace"));
protocol::ExecuteCommandParams params;
params.command = "tsl.loadWorkspace";
params.arguments = std::vector<protocol::LSPAny>{ protocol::string(workspace_uri) };
protocol::RequestMessage request;
request.id = "exec_load_ws";
request.method = "workspace/executeCommand";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::ExecuteCommand provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "ExecuteCommand loadWorkspace should not return error");
assertTrue(response.result.has_value(), "ExecuteCommand loadWorkspace should return result");
assertTrue(response.result->Is<protocol::string>(), "ExecuteCommand loadWorkspace result should be string");
assertEqual(std::string("scheduled"), response.result->Get<protocol::string>(), "ExecuteCommand should schedule workspace load");
env.scheduler.WaitAll();
auto modules = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module);
assertTrue(!modules.empty(), "Workspace load should populate module index");
}
return result;
}
TestResult ProviderMiscTests::TestWillFileOperationsProviders()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl"));
{
protocol::CreateFilesParams params;
params.files = std::vector<protocol::FileCreate>{ { .uri = file_uri } };
protocol::RequestMessage request;
request.id = "will_create";
request.method = "workspace/willCreateFiles";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::WillCreateFiles provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "WillCreateFiles should not return error");
assertTrue(response.result.has_value(), "WillCreateFiles should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "WillCreateFiles should return null");
}
{
protocol::DeleteFilesParams params;
params.files = std::vector<protocol::FileDelete>{ { .uri = file_uri } };
protocol::RequestMessage request;
request.id = "will_delete";
request.method = "workspace/willDeleteFiles";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::WillDeleteFiles provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "WillDeleteFiles should not return error");
assertTrue(response.result.has_value(), "WillDeleteFiles should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "WillDeleteFiles should return null");
}
{
protocol::RenameFilesParams params;
params.files = std::vector<protocol::FileRename>{ { .oldUri = file_uri, .newUri = file_uri } };
protocol::RequestMessage request;
request.id = "will_rename";
request.method = "workspace/willRenameFiles";
request.params = codec::ToLSPAny(params);
::lsp::provider::workspace::WillRenameFiles provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "WillRenameFiles should not return error");
assertTrue(response.result.has_value(), "WillRenameFiles should return result");
assertTrue(response.result->Is<std::nullptr_t>(), "WillRenameFiles should return null");
}
return result;
}
TestResult ProviderMiscTests::TestWorkspaceSymbolResolveProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
protocol::LSPObject params;
params["query"] = protocol::string("Workspace");
protocol::RequestMessage query_request;
query_request.id = "ws_symbol_resolve_seed";
query_request.method = "workspace/symbol";
query_request.params = protocol::LSPAny(std::move(params));
::lsp::provider::workspace::Symbol symbol_provider;
auto query_json = symbol_provider.ProvideResponse(query_request, env.context);
auto query_response = ParseResponse(query_json);
assertFalse(query_response.error.has_value(), "Workspace symbol should not return error");
assertTrue(query_response.result.has_value(), "Workspace symbol should return result");
auto symbols = codec::FromLSPAny.template operator()<std::vector<protocol::WorkspaceSymbol>>(query_response.result.value());
assertTrue(!symbols.empty(), "Workspace symbol should return symbols");
auto symbol_any = codec::ToLSPAny(symbols.front());
assertTrue(symbol_any.Is<protocol::LSPObject>(), "Workspace symbol should serialize to object");
auto symbol_obj = symbol_any.Get<protocol::LSPObject>();
auto location_it = symbol_obj.find("location");
assertTrue(location_it != symbol_obj.end(), "Workspace symbol should include location");
assertTrue(location_it->second.Is<protocol::LSPObject>(), "Workspace symbol location should be object");
auto location_obj = location_it->second.Get<protocol::LSPObject>();
location_obj.erase("range");
location_it->second = protocol::LSPAny(std::move(location_obj));
protocol::RequestMessage resolve_request;
resolve_request.id = "ws_symbol_resolve";
resolve_request.method = "workspaceSymbol/resolve";
resolve_request.params = protocol::LSPAny(std::move(symbol_obj));
::lsp::provider::workspace_symbol::Resolve provider;
auto json = provider.ProvideResponse(resolve_request, env.context);
auto response = ParseResponse(json);
assertFalse(response.error.has_value(), "workspaceSymbol/resolve should not return error");
assertTrue(response.result.has_value(), "workspaceSymbol/resolve should return result");
assertTrue(response.result->Is<protocol::LSPObject>(), "workspaceSymbol/resolve result should be object");
const auto& resolved = response.result->Get<protocol::LSPObject>();
auto resolved_location_it = resolved.find("location");
assertTrue(resolved_location_it != resolved.end(), "Resolved symbol should include location");
assertTrue(resolved_location_it->second.Is<protocol::LSPObject>(), "Resolved location should be object");
const auto& resolved_location = resolved_location_it->second.Get<protocol::LSPObject>();
assertTrue(resolved_location.find("range") != resolved_location.end(), "Resolved location should include range");
return result;
}
TestResult ProviderMiscTests::TestShutdownProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RequestMessage request;
request.id = "shutdown";
request.method = "shutdown";
::lsp::provider::Shutdown provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(!response.error.has_value(), "Shutdown should not return error");
assertTrue(env.events.size() >= 1, "Shutdown should emit lifecycle event");
assertTrue(env.events.back() == core::ServerLifecycleEvent::kShuttingDown, "Shutdown should emit shutting down");
assertFalse(env.hub.documents().GetContent(uri).has_value(), "Shutdown should clear documents");
return result;
}
TestResult ProviderMiscTests::TestCancelRequestProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
std::atomic<bool> started{ false };
env.scheduler.Submit("cancel_me", [&started]() -> std::optional<std::string> {
started.store(true);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return std::string("done");
});
while (!started.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
protocol::CancelParams params;
params.id = std::string("cancel_me");
protocol::NotificationMessage notification;
notification.method = "$/cancelRequest";
notification.params = codec::ToLSPAny(params);
::lsp::provider::CancelRequest provider;
provider.HandleNotification(notification, env.context);
env.scheduler.WaitAll();
auto stats = env.scheduler.GetStatistics();
assertEqual(std::size_t(1), static_cast<std::size_t>(stats.cancelled),
"CancelRequest should mark task cancelled");
return result;
}
TestResult ProviderMiscTests::TestSetTraceProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto prev = spdlog::get_level();
protocol::NotificationMessage notification;
notification.method = "$/setTrace";
protocol::SetTraceParams params;
params.value = protocol::TraceValueLiterals::Messages;
notification.params = codec::ToLSPAny(params);
::lsp::provider::SetTrace provider;
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::debug, "SetTrace messages should set debug level");
params.value = protocol::TraceValueLiterals::Verbose;
notification.params = codec::ToLSPAny(params);
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::trace, "SetTrace verbose should set trace level");
params.value = protocol::TraceValueLiterals::Off;
notification.params = codec::ToLSPAny(params);
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::info, "SetTrace off should set info level");
spdlog::set_level(prev);
return result;
}
TestResult ProviderMiscTests::TestExitProvider()
{
TestResult result{ "", true, "ok" };
auto exe = ExecutablePath();
assertTrue(!exe.empty(), "ExecutablePath should be set");
std::string command = "\"" + exe + "\" --exit-provider";
int code = std::system(command.c_str());
assertEqual(0, code, "Exit should return code 0");
return result;
}
int RunExitProviderChild()
{
ProviderEnv env;
::lsp::provider::Exit provider;
protocol::NotificationMessage notification;
notification.method = "exit";
provider.HandleNotification(notification, env.context);
return 1;
}
}