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 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(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(pos.line) }, { "character", static_cast(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 GetUInteger(const protocol::LSPAny& any) { if (any.Is()) { return any.Get(); } if (any.Is()) { return static_cast(any.Get()); } return std::nullopt; } std::optional GetDecimal(const protocol::LSPAny& any) { if (any.Is()) { return any.Get(); } if (any.Is()) { return static_cast(any.Get()); } if (any.Is()) { return static_cast(any.Get()); } 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{ { .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(), "Initialize result should be an object"); const auto& result_obj = response.result.value().Get(); 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(), "serverInfo should be an object"); const auto& server_info = server_info_it->second.Get(); auto name_it = server_info.find("name"); assertTrue(name_it != server_info.end(), "serverInfo should include name"); assertTrue(name_it->second.Is(), "serverInfo.name should be string"); assertTrue(!name_it->second.Get().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(), "capabilities should be an object"); const auto& capabilities = capabilities_it->second.Get(); 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()) { const auto& completion = completion_it->second.Get(); auto resolve_it = completion.find("resolveProvider"); assertTrue(resolve_it != completion.end(), "Initialize should include resolveProvider"); assertTrue(resolve_it->second.Is() && resolve_it->second.Get(), "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(), "Hover result should not be null"); auto hover = codec::FromLSPAny.template operator()(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()(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(), "PrepareRename result should not be null"); auto range = codec::FromLSPAny.template operator()(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(protocol::ErrorCodes::InvalidParams), static_cast(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()(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()>(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()>(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(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()>(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(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(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()>(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(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()>(response.result.value()); assertTrue(location.has_value(), "TypeDefinition should resolve class type definition"); auto expected = FindPosition(content, "type Widget = class"); expected.character += static_cast(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()>(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(), "DocumentLink result should be array"); const auto& links = response.result->Get(); bool found_workspace = false; bool found_system = false; for (const auto& link_any : links) { if (!link_any.Is()) { continue; } const auto& link = link_any.Get(); auto target_it = link.find("target"); if (target_it == link.end() || !target_it->second.Is()) { continue; } const auto& target = target_it->second.Get(); 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(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(), "DocumentLink resolve result should be object"); const auto& resolved = response.result->Get(); auto target_it = resolved.find("target"); assertTrue(target_it != resolved.end(), "DocumentLink resolve should set target"); assertTrue(target_it->second.Is(), "DocumentLink target should be string"); assertTrue(target_it->second.Get().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(), "FoldingRange result should be array"); const auto& ranges = response.result->Get(); assertTrue(!ranges.empty(), "FoldingRange should include ranges"); bool has_span = false; for (const auto& range_any : ranges) { if (!range_any.Is()) { continue; } const auto& range_obj = range_any.Get(); 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(), "SelectionRange result should be array"); const auto& ranges = response.result->Get(); assertEqual(std::size_t(1), ranges.size(), "SelectionRange should return one entry"); assertTrue(ranges[0].Is(), "SelectionRange entry should be object"); const auto& range_obj = ranges[0].Get(); auto range_it = range_obj.find("range"); assertTrue(range_it != range_obj.end(), "SelectionRange entry should include range"); assertTrue(range_it->second.Is(), "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(), "DocumentColor result should be array"); const auto& infos = response.result->Get(); 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()) { continue; } const auto& info = info_any.Get(); 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() || !color_it->second.Is()) { continue; } const auto& range = range_it->second.Get(); auto start_it = range.find("start"); if (start_it == range.end() || !start_it->second.Is()) { continue; } const auto& start = start_it->second.Get(); 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(); 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(), "ColorPresentation result should be array"); const auto& presentations = response.result->Get(); assertTrue(!presentations.empty(), "ColorPresentation should return entries"); assertTrue(presentations[0].Is(), "ColorPresentation entry should be object"); const auto& entry = presentations[0].Get(); auto label_it = entry.find("label"); assertTrue(label_it != entry.end(), "ColorPresentation should include label"); assertTrue(label_it->second.Is(), "ColorPresentation label should be string"); assertEqual(std::string("#ff0000"), label_it->second.Get(), "ColorPresentation should format hex"); auto edit_it = entry.find("textEdit"); assertTrue(edit_it != entry.end(), "ColorPresentation should include textEdit"); assertTrue(edit_it->second.Is(), "ColorPresentation textEdit should be object"); const auto& edit_obj = edit_it->second.Get(); 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(), "ColorPresentation textEdit.newText should be string"); assertEqual(std::string("#ff0000"), new_text_it->second.Get(), "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(), "Diagnostic result should be object"); const auto& report = response.result->Get(); auto kind_it = report.find("kind"); assertTrue(kind_it != report.end(), "Diagnostic report should include kind"); assertTrue(kind_it->second.Is(), "Diagnostic kind should be string"); assertEqual(std::string("full"), kind_it->second.Get(), "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(), "Diagnostic items should be array"); assertTrue(!items_it->second.Get().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(), "InlayHint result should be array"); const auto& hints = response.result->Get(); bool found_param = false; for (const auto& hint_any : hints) { if (!hint_any.Is()) { continue; } const auto& hint = hint_any.Get(); 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()) { continue; } auto kind = GetUInteger(kind_it->second); if (!kind) { continue; } if (label_it->second.Get() == "a:" && *kind == static_cast(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(), "InlayHint (type) result should be array"); const auto& type_hints = type_response.result->Get(); bool found_type = false; for (const auto& hint_any : type_hints) { if (!hint_any.Is()) { continue; } const auto& hint = hint_any.Get(); 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::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::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(), "InlayHint resolve result should be object"); const auto& resolved = response.result->Get(); auto tooltip_it = resolved.find("tooltip"); assertTrue(tooltip_it != resolved.end(), "InlayHint resolve should set tooltip"); assertTrue(tooltip_it->second.Is(), "InlayHint tooltip should be string"); assertEqual(std::string("param: int"), tooltip_it->second.Get(), "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(), "CodeLens result should be array"); const auto& lenses = response.result->Get(); bool found = false; for (const auto& lens_any : lenses) { if (!lens_any.Is()) { continue; } const auto& lens = lens_any.Get(); auto data_it = lens.find("data"); if (data_it == lens.end() || !data_it->second.Is()) { continue; } const auto& data = data_it->second.Get(); 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() || !name_it->second.Is()) { continue; } if (kind_it->second.Get() == "references" && name_it->second.Get() == "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(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(), "CodeLens resolve result should be object"); const auto& resolved = response.result->Get(); auto command_it = resolved.find("command"); assertTrue(command_it != resolved.end(), "CodeLens resolve should set command"); assertTrue(command_it->second.Is(), "CodeLens command should be object"); const auto& command = command_it->second.Get(); 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(), "CodeLens title should be string"); assertTrue(cmd_it->second.Is(), "CodeLens command should be string"); assertEqual(std::string("2 references"), title_it->second.Get(), "CodeLens title should use count"); assertEqual(std::string("tsl.showReferences"), cmd_it->second.Get(), "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(), "PrepareCallHierarchy result should be array"); const auto& items = response.result->Get(); assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); assertTrue(items[0].Is(), "PrepareCallHierarchy item should be object"); const auto& item = items[0].Get(); auto name_it = item.find("name"); assertTrue(name_it != item.end(), "CallHierarchyItem should include name"); assertTrue(name_it->second.Is(), "CallHierarchyItem name should be string"); assertEqual(std::string("UnitFunc"), name_it->second.Get(), "PrepareCallHierarchy should target UnitFunc"); auto data_it = item.find("data"); assertTrue(data_it != item.end(), "CallHierarchyItem should include data"); assertTrue(data_it->second.Is(), "CallHierarchyItem data should be object"); const auto& data = data_it->second.Get(); auto symbol_id_it = data.find("symbolId"); assertTrue(symbol_id_it != data.end(), "CallHierarchyItem data should include symbolId"); assertTrue(symbol_id_it->second.Is(), "CallHierarchyItem symbolId should be string"); assertTrue(!symbol_id_it->second.Get().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(), "PrepareCallHierarchy result should be array"); const auto& items = prepare_response.result->Get(); assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); assertTrue(items[0].Is(), "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(), "IncomingCalls result should be array"); const auto& calls = response.result->Get(); bool found = false; for (const auto& call_any : calls) { if (!call_any.Is()) { continue; } const auto& call = call_any.Get(); 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()) { continue; } const auto& from = from_it->second.Get(); auto name_it = from.find("name"); if (name_it == from.end() || !name_it->second.Is()) { continue; } if (name_it->second.Get() == "TestDefinitions") { found = true; assertTrue(ranges_it->second.Is(), "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(), "PrepareCallHierarchy result should be array"); const auto& items = prepare_response.result->Get(); assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); assertTrue(items[0].Is(), "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(), "OutgoingCalls result should be array"); const auto& calls = response.result->Get(); bool found = false; for (const auto& call_any : calls) { if (!call_any.Is()) { continue; } const auto& call = call_any.Get(); auto to_it = call.find("to"); if (to_it == call.end() || !to_it->second.Is()) { continue; } const auto& to = to_it->second.Get(); auto name_it = to.find("name"); if (name_it == to.end() || !name_it->second.Is()) { continue; } if (name_it->second.Get() == "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(), "PrepareTypeHierarchy result should be array"); const auto& items = response.result->Get(); assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); assertTrue(items[0].Is(), "TypeHierarchyItem should be object"); const auto& item = items[0].Get(); auto name_it = item.find("name"); assertTrue(name_it != item.end(), "TypeHierarchyItem should include name"); assertTrue(name_it->second.Is(), "TypeHierarchyItem name should be string"); assertEqual(std::string("Derived"), name_it->second.Get(), "PrepareTypeHierarchy should target Derived"); auto data_it = item.find("data"); assertTrue(data_it != item.end(), "TypeHierarchyItem should include data"); assertTrue(data_it->second.Is(), "TypeHierarchyItem data should be object"); const auto& data = data_it->second.Get(); auto symbol_id_it = data.find("symbolId"); assertTrue(symbol_id_it != data.end(), "TypeHierarchyItem data should include symbolId"); assertTrue(symbol_id_it->second.Is(), "TypeHierarchyItem symbolId should be string"); assertTrue(!symbol_id_it->second.Get().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(), "PrepareTypeHierarchy result should be array"); const auto& items = prepare_response.result->Get(); assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); assertTrue(items[0].Is(), "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(), "Supertypes result should be array"); const auto& types = response.result->Get(); bool found = false; for (const auto& type_any : types) { if (!type_any.Is()) { continue; } const auto& type_item = type_any.Get(); auto name_it = type_item.find("name"); if (name_it == type_item.end() || !name_it->second.Is()) { continue; } if (name_it->second.Get() == "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(), "PrepareTypeHierarchy result should be array"); const auto& items = prepare_response.result->Get(); assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); assertTrue(items[0].Is(), "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(), "Subtypes result should be array"); const auto& types = response.result->Get(); bool found = false; for (const auto& type_any : types) { if (!type_any.Is()) { continue; } const auto& type_item = type_any.Get(); auto name_it = type_item.find("name"); if (name_it == type_item.end() || !name_it->second.Is()) { continue; } if (name_it->second.Get() == "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& 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& uris) { return BuildDidCreateFilesParams(uris); } protocol::LSPAny BuildDidRenameFilesParams(const std::vector>& 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>& 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(), "didChangeConfiguration should store object settings"); const auto& stored_obj = stored.Get(); 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(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()>(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(), "Workspace diagnostic result should be object"); const auto& report = response.result->Get(); auto items_it = report.find("items"); assertTrue(items_it != report.end(), "Workspace diagnostic report should include items"); assertTrue(items_it->second.Is(), "Workspace diagnostic items should be array"); bool found_error = false; for (const auto& item_any : items_it->second.Get()) { if (!item_any.Is()) { continue; } const auto& item = item_any.Get(); auto uri_it = item.find("uri"); if (uri_it == item.end() || !uri_it->second.Is()) { continue; } if (uri_it->second.Get() != 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(), "Workspace diagnostic kind should be string"); assertEqual(std::string("full"), kind_it->second.Get(), "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(), "Workspace diagnostic items should be array"); assertTrue(!diags_it->second.Get().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(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(), "workspace/configuration result should be array"); const auto& values = response.result->Get(); assertEqual(static_cast(3), values.size(), "workspace/configuration result size should match items"); assertTrue(values[0].Is(), "workspace/configuration should resolve tsl.foo"); assertEqual(static_cast(42), values[0].Get(), "tsl.foo should equal 42"); assertTrue(values[1].Is(), "workspace/configuration should resolve tsl.nested.b"); assertEqual(std::string("x"), values[1].Get(), "tsl.nested.b should equal x"); assertTrue(values[2].Is(), "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(), "workspace/applyEdit result should be object"); const auto& obj = response.result->Get(); auto applied_it = obj.find("applied"); assertTrue(applied_it != obj.end(), "workspace/applyEdit result should include applied"); assertTrue(applied_it->second.Is(), "workspace/applyEdit applied should be bool"); assertTrue(applied_it->second.Get(), "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(), "workspace/workspaceFolders result should be array"); const auto& folders = response.result->Get(); assertEqual(static_cast(1), folders.size(), "workspace/workspaceFolders should return configured folders"); assertTrue(folders.front().Is(), "workspace/workspaceFolders item should be object"); const auto& item = folders.front().Get(); 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(), "workspace/workspaceFolders uri should be string"); assertEqual(folder.uri, uri_it->second.Get(), "workspace/workspaceFolders uri should match"); assertTrue(name_it != item.end(), "workspace/workspaceFolders item should include name"); assertTrue(name_it->second.Is(), "workspace/workspaceFolders name should be string"); assertEqual(folder.name, name_it->second.Get(), "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::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(), "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(), "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(), "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::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(), "window/showMessageRequest result should be object"); const auto& obj = response.result->Get(); auto title_it = obj.find("title"); assertTrue(title_it != obj.end(), "window/showMessageRequest result should include title"); assertTrue(title_it->second.Is(), "window/showMessageRequest title should be string"); assertEqual(std::string("OK"), title_it->second.Get(), "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(), "window/showDocument result should be object"); const auto& obj = response.result->Get(); auto success_it = obj.find("success"); assertTrue(success_it != obj.end(), "window/showDocument result should include success"); assertTrue(success_it->second.Is(), "window/showDocument success should be bool"); assertFalse(success_it->second.Get(), "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::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::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(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(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()(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(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()>(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(error.location.start_line) }, { "character", static_cast(error.location.start_column) } } }, { "end", protocol::LSPObject{ { "line", static_cast(error.location.end_line) }, { "character", static_cast(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(), "Code action result should be an array"); const auto& actions = response.result->Get(); 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()) { continue; } const auto& action = action_any.Get(); 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() || !kind_it->second.Is()) { continue; } const auto& title = title_it->second.Get(); const auto& kind = kind_it->second.Get(); 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(), "CodeAction resolve result should be object"); const auto& resolved = response.result->Get(); auto title_it = resolved.find("title"); assertTrue(title_it != resolved.end(), "Resolved code action should include title"); assertTrue(title_it->second.Is(), "Resolved code action title should be string"); assertEqual(std::string("Resolve me"), title_it->second.Get(), "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(), "Formatting result should be array"); const auto& edits = response.result->Get(); assertTrue(!edits.empty(), "Formatting should return edits when content differs"); assertTrue(edits.front().Is(), "Formatting edit should be object"); const auto& edit = edits.front().Get(); auto new_text_it = edit.find("newText"); assertTrue(new_text_it != edit.end(), "Formatting edit should include newText"); assertTrue(new_text_it->second.Is(), "Formatting newText should be string"); const auto& new_text = new_text_it->second.Get(); 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(), "Range formatting result should be array"); const auto& edits = response.result->Get(); assertTrue(!edits.empty(), "Range formatting should return edits"); assertTrue(edits.front().Is(), "Range formatting edit should be object"); const auto& edit = edits.front().Get(); 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(), "Range formatting newText should be string"); const auto& new_text = new_text_it->second.Get(); 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(), "OnType formatting result should be array"); const auto& edits = response.result->Get(); assertTrue(!edits.empty(), "OnType formatting should return edits"); assertTrue(edits.front().Is(), "OnType formatting edit should be object"); const auto& edit = edits.front().Get(); 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(), "OnType formatting newText should be string"); assertEqual(std::string(""), new_text_it->second.Get(), "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(), "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(), "Moniker result should be array"); const auto& monikers = response.result->Get(); assertTrue(!monikers.empty(), "Moniker should return at least one entry"); assertTrue(monikers.front().Is(), "Moniker entry should be object"); const auto& moniker = monikers.front().Get(); 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(), "Moniker scheme should be string"); assertTrue(ident_it->second.Is(), "Moniker identifier should be string"); assertEqual(std::string("tsl"), scheme_it->second.Get(), "Moniker scheme should be tsl"); assertTrue(ident_it->second.Get().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(), "ExecuteCommand noop should return null"); } { auto workspace_uri = ToUri(FixturePath("workspace")); protocol::ExecuteCommandParams params; params.command = "tsl.loadWorkspace"; params.arguments = std::vector{ 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(), "ExecuteCommand loadWorkspace result should be string"); assertEqual(std::string("scheduled"), response.result->Get(), "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{ { .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(), "WillCreateFiles should return null"); } { protocol::DeleteFilesParams params; params.files = std::vector{ { .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(), "WillDeleteFiles should return null"); } { protocol::RenameFilesParams params; params.files = std::vector{ { .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(), "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()>(query_response.result.value()); assertTrue(!symbols.empty(), "Workspace symbol should return symbols"); auto symbol_any = codec::ToLSPAny(symbols.front()); assertTrue(symbol_any.Is(), "Workspace symbol should serialize to object"); auto symbol_obj = symbol_any.Get(); auto location_it = symbol_obj.find("location"); assertTrue(location_it != symbol_obj.end(), "Workspace symbol should include location"); assertTrue(location_it->second.Is(), "Workspace symbol location should be object"); auto location_obj = location_it->second.Get(); 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(), "workspaceSymbol/resolve result should be object"); const auto& resolved = response.result->Get(); auto resolved_location_it = resolved.find("location"); assertTrue(resolved_location_it != resolved.end(), "Resolved symbol should include location"); assertTrue(resolved_location_it->second.Is(), "Resolved location should be object"); const auto& resolved_location = resolved_location_it->second.Get(); 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 started{ false }; env.scheduler.Submit("cancel_me", [&started]() -> std::optional { 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(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; } }