3508 lines
151 KiB
C++
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;
|
|
}
|
|
}
|