module; export module lsp.test.provider.json_provider_coverage; import std; import lsp.test.framework; import lsp.codec.facade; import lsp.core.dispacther; import lsp.language.ast; import lsp.manager.manager_hub; import lsp.protocol; import lsp.provider.manifest; import lsp.scheduler.async_executor; import lsp.test.provider.fixtures; import tree_sitter; export namespace lsp::test::provider { class JsonProviderCoverageTests { public: static void Register(TestRunner& runner); private: static TestResult TestAllProvidersJsonCoverage(); }; } namespace lsp::test::provider { namespace { namespace codec = lsp::codec; namespace provider = lsp::provider; struct SeededRequestParams { std::optional code_lens; std::optional document_link; std::optional inlay_hint; std::optional call_hierarchy_incoming_item; std::optional call_hierarchy_outgoing_item; std::optional type_hierarchy_item; std::optional code_action; std::optional workspace_symbol; }; struct ProviderEnv { scheduler::AsyncExecutor scheduler{ 1 }; manager::ManagerHub hub{}; core::RequestDispatcher dispatcher{}; ProviderEnv() { hub.Initialize(); dispatcher.SetRequestScheduler(&scheduler); dispatcher.SetManagerHub(&hub); provider::RegisterAllProviders(dispatcher); } }; template std::string SerializeOrThrow(const T& obj) { auto json = codec::Serialize(obj); assertTrue(json.has_value(), "Failed to serialize LSP JSON"); return json.value(); } template T DeserializeOrThrow(const std::string& json) { auto parsed = codec::Deserialize(json); assertTrue(parsed.has_value(), "Failed to deserialize LSP JSON"); return parsed.value(); } protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker = false) { auto pos = content.find(marker); assertTrue(pos != std::string::npos, "Marker not found in fixture"); protocol::Position result{}; for (std::size_t i = 0; i < pos; ++i) { if (content[i] == '\n') { result.line++; result.character = 0; } else { result.character++; } } if (after_marker) { result.character += static_cast(marker.size()); } return result; } protocol::LSPObject ToTextDocument(const std::string& uri) { return protocol::LSPObject{ { "uri", uri } }; } protocol::LSPObject ToPosition(const protocol::Position& pos) { return protocol::LSPObject{ { "line", static_cast(pos.line) }, { "character", static_cast(pos.character) }, }; } protocol::LSPObject ToRange(const protocol::Range& range) { return protocol::LSPObject{ { "start", ToPosition(range.start) }, { "end", ToPosition(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 FirstArrayItem(const protocol::LSPAny& any) { if (!any.Is()) { return std::nullopt; } const auto& array = any.Get(); if (array.empty()) { return std::nullopt; } return array.front(); } protocol::LSPAny BuildCompletionResolveItem(const std::string& uri) { protocol::LSPObject data; data["ctx"] = "new"; data["class"] = "Widget"; data["unit"] = "MainUnit"; data["uri"] = uri; protocol::LSPObject item; item["label"] = "Widget"; item["data"] = std::move(data); return protocol::LSPAny(std::move(item)); } std::optional BuildRequestParams(std::string_view method, const SeededRequestParams& seeded, const std::string& main_uri, const std::string& main_content, const std::string& rename_uri, const std::string& rename_content, const std::string& code_action_uri, const std::string& code_action_content, const protocol::LSPArray& code_action_diagnostics) { if (method == "textDocument/completion") { auto completion_pos = FindPosition(main_content, "new Wid", true); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(completion_pos); return protocol::LSPAny(std::move(params)); } if (method == "completionItem/resolve") { return BuildCompletionResolveItem(main_uri); } if (method == "textDocument/definition") { auto def_pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(def_pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/hover") { auto hover_pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(hover_pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/signatureHelp") { auto sig_pos = FindPosition(main_content, "UnitFunc(1);", false); sig_pos.character += static_cast(std::string("UnitFunc(").size()); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(sig_pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/references") { auto pos = FindPosition(rename_content, "target := target", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(rename_uri); params["position"] = ToPosition(pos); params["context"] = protocol::LSPObject{ { "includeDeclaration", true } }; return protocol::LSPAny(std::move(params)); } if (method == "textDocument/linkedEditingRange") { auto pos = FindPosition(rename_content, "target := target", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(rename_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/documentHighlight") { auto pos = FindPosition(rename_content, "target := target", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(rename_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/prepareRename") { auto pos = FindPosition(rename_content, "target := target", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(rename_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/rename") { auto pos = FindPosition(rename_content, "target := target", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(rename_uri); params["position"] = ToPosition(pos); params["newName"] = "renamed_target"; return protocol::LSPAny(std::move(params)); } if (method == "textDocument/documentSymbol") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "workspace/symbol") { protocol::LSPObject params; params["query"] = protocol::string("Workspace"); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/semanticTokens/full") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/semanticTokens/full/delta") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["previousResultId"] = "0"; return protocol::LSPAny(std::move(params)); } if (method == "textDocument/semanticTokens/range") { protocol::Range range{}; range.start.line = 0; range.start.character = 0; range.end.line = 5; range.end.character = 0; protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["range"] = ToRange(range); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/codeAction") { auto range = FullDocumentRange(code_action_content); protocol::LSPObject params; params["textDocument"] = ToTextDocument(code_action_uri); params["range"] = ToRange(range); params["context"] = protocol::LSPObject{ { "diagnostics", code_action_diagnostics }, }; return protocol::LSPAny(std::move(params)); } if (method == "textDocument/typeDefinition") { auto pos = FindPosition(main_content, "obj: Widget", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/implementation") { auto pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/documentLink") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/codeLens") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "codeLens/resolve") { if (seeded.code_lens) { return *seeded.code_lens; } 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"] = ToRange(range); lens["data"] = protocol::LSPAny(std::move(data)); return protocol::LSPAny(std::move(lens)); } if (method == "documentLink/resolve") { if (seeded.document_link) { return *seeded.document_link; } auto pos = FindPosition(main_content, "WorkspaceUnit", false); 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(main_uri); protocol::LSPObject link; link["range"] = ToRange(range); link["data"] = protocol::LSPAny(std::move(data)); return protocol::LSPAny(std::move(link)); } if (method == "textDocument/foldingRange") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/selectionRange") { auto pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPArray positions; positions.emplace_back(ToPosition(pos)); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["positions"] = std::move(positions); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/prepareCallHierarchy") { auto pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "callHierarchy/incomingCalls") { if (seeded.call_hierarchy_incoming_item) { protocol::LSPObject params; params["item"] = *seeded.call_hierarchy_incoming_item; return protocol::LSPAny(std::move(params)); } auto pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::Range range{}; range.start = pos; range.end = pos; range.end.character += static_cast(std::string("UnitFunc").size()); protocol::LSPObject item; item["name"] = protocol::string("UnitFunc"); item["kind"] = static_cast(protocol::SymbolKind::Function); item["tags"] = protocol::LSPArray{}; item["uri"] = protocol::string(main_uri); item["range"] = ToRange(range); item["selectionRange"] = ToRange(range); protocol::LSPObject params; params["item"] = protocol::LSPAny(std::move(item)); return protocol::LSPAny(std::move(params)); } if (method == "callHierarchy/outgoingCalls") { if (seeded.call_hierarchy_outgoing_item) { protocol::LSPObject params; params["item"] = *seeded.call_hierarchy_outgoing_item; return protocol::LSPAny(std::move(params)); } auto pos = FindPosition(main_content, "TestDefinitions();", false); protocol::Range range{}; range.start = pos; range.end = pos; range.end.character += static_cast(std::string("TestDefinitions").size()); protocol::LSPObject item; item["name"] = protocol::string("TestDefinitions"); item["kind"] = static_cast(protocol::SymbolKind::Function); item["tags"] = protocol::LSPArray{}; item["uri"] = protocol::string(main_uri); item["range"] = ToRange(range); item["selectionRange"] = ToRange(range); protocol::LSPObject params; params["item"] = protocol::LSPAny(std::move(item)); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/prepareTypeHierarchy") { auto pos = FindPosition(main_content, "Widget = class", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "typeHierarchy/supertypes") { if (seeded.type_hierarchy_item) { protocol::LSPObject params; params["item"] = *seeded.type_hierarchy_item; return protocol::LSPAny(std::move(params)); } auto pos = FindPosition(main_content, "Widget = class", false); protocol::Range range{}; range.start = pos; range.end = pos; range.end.character += static_cast(std::string("Widget").size()); protocol::LSPObject item; item["name"] = protocol::string("Widget"); item["kind"] = static_cast(protocol::SymbolKind::Class); item["tags"] = protocol::LSPArray{}; item["uri"] = protocol::string(main_uri); item["range"] = ToRange(range); item["selectionRange"] = ToRange(range); protocol::LSPObject params; params["item"] = protocol::LSPAny(std::move(item)); return protocol::LSPAny(std::move(params)); } if (method == "typeHierarchy/subtypes") { if (seeded.type_hierarchy_item) { protocol::LSPObject params; params["item"] = *seeded.type_hierarchy_item; return protocol::LSPAny(std::move(params)); } auto pos = FindPosition(main_content, "Widget = class", false); protocol::Range range{}; range.start = pos; range.end = pos; range.end.character += static_cast(std::string("Widget").size()); protocol::LSPObject item; item["name"] = protocol::string("Widget"); item["kind"] = static_cast(protocol::SymbolKind::Class); item["tags"] = protocol::LSPArray{}; item["uri"] = protocol::string(main_uri); item["range"] = ToRange(range); item["selectionRange"] = ToRange(range); protocol::LSPObject params; params["item"] = protocol::LSPAny(std::move(item)); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/inlayHint") { auto range = FullDocumentRange(main_content); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["range"] = ToRange(range); return protocol::LSPAny(std::move(params)); } if (method == "inlayHint/resolve") { if (seeded.inlay_hint) { return *seeded.inlay_hint; } protocol::Position pos{}; pos.line = 0; pos.character = 0; protocol::LSPObject data; data["detail"] = protocol::string("param: int"); protocol::LSPObject hint; hint["position"] = ToPosition(pos); hint["label"] = protocol::string("param:"); hint["kind"] = static_cast(protocol::InlayHintKind::Parameter); hint["data"] = protocol::LSPAny(std::move(data)); return protocol::LSPAny(std::move(hint)); } if (method == "textDocument/documentColor") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/colorPresentation") { protocol::Range range{}; range.start.line = 0; range.start.character = 0; range.end.line = 0; range.end.character = 7; protocol::LSPObject color; color["red"] = protocol::decimal(1.0); color["green"] = protocol::decimal(0.0); color["blue"] = protocol::decimal(0.0); color["alpha"] = protocol::decimal(1.0); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["color"] = std::move(color); params["range"] = ToRange(range); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/diagnostic") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(code_action_uri); return protocol::LSPAny(std::move(params)); } if (method == "workspace/diagnostic") { protocol::LSPObject params; params["previousResultIds"] = protocol::LSPArray{}; return protocol::LSPAny(std::move(params)); } if (method == "codeAction/resolve") { if (seeded.code_action) { return *seeded.code_action; } protocol::LSPObject action; action["title"] = protocol::string("Fix"); action["kind"] = protocol::string(protocol::CodeActionKindLiterals::QuickFix); action["data"] = protocol::LSPAny(protocol::LSPObject{ { "kind", protocol::string("quickfix") } }); return protocol::LSPAny(std::move(action)); } if (method == "workspaceSymbol/resolve") { if (seeded.workspace_symbol) { return *seeded.workspace_symbol; } protocol::LSPObject symbol; symbol["name"] = protocol::string("WorkspaceUnit"); symbol["kind"] = static_cast(protocol::SymbolKind::Module); symbol["location"] = protocol::LSPObject{ { "uri", protocol::string(main_uri) }, { "range", ToRange(FullDocumentRange(main_content)) }, }; return protocol::LSPAny(std::move(symbol)); } if (method == "workspace/executeCommand") { protocol::LSPObject params; params["command"] = protocol::string("tsl.noop"); params["arguments"] = protocol::LSPArray{}; return protocol::LSPAny(std::move(params)); } if (method == "workspace/willCreateFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "uri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "workspace/willDeleteFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "uri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "workspace/willRenameFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "oldUri", file_uri }, { "newUri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/formatting") { protocol::LSPObject options; options["tabSize"] = static_cast(4); options["insertSpaces"] = true; protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["options"] = std::move(options); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/rangeFormatting") { protocol::LSPObject options; options["tabSize"] = static_cast(4); options["insertSpaces"] = true; auto range = FullDocumentRange(main_content); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["range"] = ToRange(range); params["options"] = std::move(options); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/onTypeFormatting") { protocol::LSPObject options; options["tabSize"] = static_cast(4); options["insertSpaces"] = true; auto pos = FindPosition(main_content, "return", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); params["ch"] = protocol::string(";"); params["options"] = std::move(options); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/inlineValue") { protocol::LSPObject context; context["frameId"] = static_cast(0); context["stoppedLocation"] = ToRange(FullDocumentRange(main_content)); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["range"] = ToRange(FullDocumentRange(main_content)); params["context"] = std::move(context); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/moniker") { auto pos = FindPosition(main_content, "UnitFunc(1);", false); protocol::LSPObject params; params["textDocument"] = ToTextDocument(main_uri); params["position"] = ToPosition(pos); return protocol::LSPAny(std::move(params)); } if (method == "client/registerCapability") { 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); return protocol::LSPAny(std::move(params)); } if (method == "client/unregisterCapability") { 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); return protocol::LSPAny(std::move(params)); } if (method == "window/workDoneProgress/create") { protocol::LSPObject params; params["token"] = protocol::string("progress_token"); return protocol::LSPAny(std::move(params)); } if (method == "window/showMessageRequest") { 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); return protocol::LSPAny(std::move(params)); } if (method == "window/showDocument") { protocol::LSPObject params; params["uri"] = protocol::string(main_uri); params["takeFocus"] = true; return protocol::LSPAny(std::move(params)); } if (method == "workspace/configuration") { protocol::LSPArray items; items.emplace_back(protocol::LSPObject{ { "scopeUri", protocol::string(main_uri) }, { "section", protocol::string("tsl") }, }); protocol::LSPObject params; params["items"] = std::move(items); return protocol::LSPAny(std::move(params)); } if (method == "workspace/applyEdit") { protocol::Range range{}; range.start.line = 0; range.start.character = 0; range.end = range.start; protocol::LSPObject text_edit; text_edit["range"] = ToRange(range); text_edit["newText"] = protocol::string(""); protocol::LSPArray edits; edits.emplace_back(std::move(text_edit)); protocol::LSPObject changes; changes[main_uri] = protocol::LSPAny(std::move(edits)); protocol::LSPObject edit; edit["changes"] = std::move(changes); protocol::LSPObject params; params["edit"] = std::move(edit); return protocol::LSPAny(std::move(params)); } if (method == "workspace/workspaceFolders") { return protocol::LSPAny(protocol::LSPObject{}); } if (method == "workspace/codeLens/refresh" || method == "workspace/diagnostic/refresh" || method == "workspace/inlayHint/refresh" || method == "workspace/inlineValue/refresh" || method == "workspace/semanticTokens/refresh") { return protocol::LSPAny(protocol::LSPObject{}); } return std::nullopt; } std::optional BuildNotificationParams(std::string_view method, const std::string& uri, const std::string& content, int version) { if (method == "textDocument/didOpen") { protocol::LSPObject params; params["textDocument"] = protocol::LSPObject{ { "uri", uri }, { "languageId", "tsl" }, { "version", version }, { "text", content }, }; return protocol::LSPAny(std::move(params)); } if (method == "textDocument/didChange") { protocol::LSPObject change; protocol::Range full_range{}; full_range.start.line = 0; full_range.start.character = 0; full_range.end.line = 9999; full_range.end.character = 0; change["range"] = ToRange(full_range); change["text"] = content; protocol::LSPArray changes; changes.emplace_back(std::move(change)); protocol::LSPObject params; params["textDocument"] = protocol::LSPObject{ { "uri", uri }, { "version", version }, }; params["contentChanges"] = std::move(changes); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/didClose") { protocol::LSPObject params; params["textDocument"] = ToTextDocument(uri); return protocol::LSPAny(std::move(params)); } if (method == "$/setTrace") { protocol::LSPObject params; params["value"] = protocol::TraceValueLiterals::Off; return protocol::LSPAny(std::move(params)); } if (method == "$/cancelRequest") { protocol::LSPObject params; params["id"] = "json_cancel_me"; return protocol::LSPAny(std::move(params)); } if (method == "workspace/didCreateFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "uri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "workspace/didDeleteFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "uri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "workspace/didRenameFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPArray files; files.emplace_back(protocol::LSPObject{ { "oldUri", file_uri }, { "newUri", file_uri }, }); protocol::LSPObject params; params["files"] = std::move(files); return protocol::LSPAny(std::move(params)); } if (method == "workspace/didChangeWatchedFiles") { auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); protocol::LSPObject change; change["uri"] = file_uri; change["type"] = static_cast(protocol::FileChangeType::Changed); protocol::LSPArray changes; changes.emplace_back(std::move(change)); protocol::LSPObject params; params["changes"] = std::move(changes); return protocol::LSPAny(std::move(params)); } if (method == "workspace/didChangeConfiguration") { protocol::LSPObject tsl; tsl["format"] = true; protocol::LSPObject settings; settings["tsl"] = std::move(tsl); protocol::LSPObject params; params["settings"] = std::move(settings); return protocol::LSPAny(std::move(params)); } if (method == "workspace/didChangeWorkspaceFolders") { auto workspace_uri = ToUri(FixturePath("workspace")); 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); return protocol::LSPAny(std::move(params)); } if (method == "window/logMessage") { protocol::LSPObject params; params["type"] = static_cast(protocol::MessageType::Log); params["message"] = protocol::string("Test logMessage"); return protocol::LSPAny(std::move(params)); } if (method == "window/showMessage") { protocol::LSPObject params; params["type"] = static_cast(protocol::MessageType::Info); params["message"] = protocol::string("Test showMessage"); return protocol::LSPAny(std::move(params)); } if (method == "telemetry/event") { protocol::LSPObject params; params["event"] = protocol::string("test_event"); params["value"] = static_cast(1); return protocol::LSPAny(std::move(params)); } if (method == "textDocument/publishDiagnostics") { 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", ToRange(range) }, { "message", protocol::string("Test diagnostic") }, }); protocol::LSPObject params; params["uri"] = protocol::string(uri); params["version"] = static_cast(version); params["diagnostics"] = std::move(diagnostics); return protocol::LSPAny(std::move(params)); } if (method == "initialized") { return protocol::LSPAny(protocol::LSPObject{}); } if (method == "exit") { return protocol::LSPAny(protocol::LSPObject{}); } return std::nullopt; } protocol::LSPArray BuildDiagnosticsFromSyntaxErrors(TSTree* tree, const std::string& content) { protocol::LSPArray diagnostics; if (!tree) { return diagnostics; } auto errors = language::ast::Deserializer::DiagnoseSyntax(ts_tree_root_node(tree), content); diagnostics.reserve(errors.size()); for (const auto& error : errors) { protocol::LSPObject diagnostic; protocol::Range range{}; range.start.line = error.location.start_line; range.start.character = error.location.start_column; range.end.line = error.location.end_line; range.end.character = error.location.end_column; diagnostic["range"] = ToRange(range); diagnostic["message"] = error.message; diagnostics.emplace_back(std::move(diagnostic)); } return diagnostics; } } void JsonProviderCoverageTests::Register(TestRunner& runner) { runner.addTest("json provider coverage (all providers)", TestAllProvidersJsonCoverage); } TestResult JsonProviderCoverageTests::TestAllProvidersJsonCoverage() { TestResult result{ "", true, "ok" }; ProviderEnv env; auto workspace_uri = ToUri(FixturePath("workspace")); auto main_path = FixturePath("main_unit.tsf"); auto main_content = ReadTextFile(main_path); auto main_uri = ToUri(main_path); auto type_hierarchy_path = FixturePath("type_hierarchy_unit.tsf"); auto type_hierarchy_content = ReadTextFile(type_hierarchy_path); auto type_hierarchy_uri = ToUri(type_hierarchy_path); auto inlay_hint_path = FixturePath("inlay_hint_case.tsl"); auto inlay_hint_content = ReadTextFile(inlay_hint_path); auto inlay_hint_uri = ToUri(inlay_hint_path); auto rename_path = FixturePath("rename_case.tsl"); auto rename_content = ReadTextFile(rename_path); auto rename_uri = ToUri(rename_path); auto code_action_path = FixturePath("code_action_missing_semicolon.tsl"); auto code_action_content = ReadTextFile(code_action_path); auto code_action_uri = ToUri(code_action_path); { protocol::LSPArray folders; folders.emplace_back(protocol::LSPObject{ { "uri", workspace_uri }, { "name", "workspace" }, }); protocol::LSPObject init_params; init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off); init_params["workspaceFolders"] = std::move(folders); protocol::RequestMessage init_request; init_request.id = "init"; init_request.method = "initialize"; init_request.params = protocol::LSPAny(std::move(init_params)); auto init_json = SerializeOrThrow(init_request); auto parsed_init = DeserializeOrThrow(init_json); auto init_response_json = env.dispatcher.Dispatch(parsed_init); auto init_response_any = codec::Deserialize(init_response_json); assertTrue(init_response_any.has_value(), "Initialize response should be valid JSON"); env.scheduler.WaitAll(); } { protocol::NotificationMessage initialized; initialized.method = "initialized"; initialized.params = protocol::LSPAny(protocol::LSPObject{}); auto json = SerializeOrThrow(initialized); auto parsed = DeserializeOrThrow(json); env.dispatcher.Dispatch(parsed); } auto send_notification = [&](std::string_view method, const std::string& uri, const std::string& content, int version) { protocol::NotificationMessage notification; notification.method = std::string(method); auto params = BuildNotificationParams(method, uri, content, version); assertTrue(params.has_value(), "Missing notification params builder for: " + std::string(method)); notification.params = *params; auto json = SerializeOrThrow(notification); auto parsed = DeserializeOrThrow(json); env.dispatcher.Dispatch(parsed); }; send_notification("textDocument/didOpen", main_uri, main_content, 1); send_notification("textDocument/didOpen", type_hierarchy_uri, type_hierarchy_content, 1); send_notification("textDocument/didOpen", inlay_hint_uri, inlay_hint_content, 1); send_notification("textDocument/didOpen", rename_uri, rename_content, 1); send_notification("textDocument/didOpen", code_action_uri, code_action_content, 1); send_notification("textDocument/didChange", main_uri, main_content, 2); auto code_action_tree = env.hub.parser().GetTree(code_action_uri); auto code_action_diagnostics = BuildDiagnosticsFromSyntaxErrors(code_action_tree, code_action_content); env.scheduler.Submit("json_cancel_me", []() -> std::optional { std::this_thread::sleep_for(std::chrono::milliseconds(200)); return std::string("done"); }); { auto methods = env.dispatcher.GetSupportedNotifications(); std::sort(methods.begin(), methods.end()); for (const auto& method : methods) { if (method == "exit" || method == "textDocument/didOpen" || method == "textDocument/didChange" || method == "textDocument/didClose" || method == "initialized") { continue; } protocol::NotificationMessage notification; notification.method = method; auto params = BuildNotificationParams(method, main_uri, main_content, 1); assertTrue(params.has_value(), "Missing notification params builder for: " + method); notification.params = *params; try { auto json = SerializeOrThrow(notification); auto parsed = DeserializeOrThrow(json); env.dispatcher.Dispatch(parsed); } catch (const std::exception& e) { throw std::runtime_error("Notification method failed: " + method + " (" + e.what() + ")"); } } } env.scheduler.WaitAll(); SeededRequestParams seeded; auto dispatch_request_result = [&](std::string_view method, protocol::LSPAny params) -> std::optional { protocol::RequestMessage request; request.id = protocol::string("seed_" + std::string(method)); request.method = std::string(method); request.params = std::move(params); try { auto json = SerializeOrThrow(request); auto parsed = DeserializeOrThrow(json); auto response_json = env.dispatcher.Dispatch(parsed); auto response = codec::Deserialize(response_json); if (!response.has_value() || !response->result.has_value()) { return std::nullopt; } return response->result.value(); } catch (const std::exception&) { return std::nullopt; } }; if (auto result = dispatch_request_result("textDocument/codeLens", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(main_uri) }, }))) { seeded.code_lens = FirstArrayItem(*result); } if (auto result = dispatch_request_result("textDocument/documentLink", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(main_uri) }, }))) { seeded.document_link = FirstArrayItem(*result); } if (auto result = dispatch_request_result("textDocument/inlayHint", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(inlay_hint_uri) }, { "range", ToRange(FullDocumentRange(inlay_hint_content)) }, }))) { seeded.inlay_hint = FirstArrayItem(*result); } auto call_incoming_pos = FindPosition(main_content, "UnitFunc(1);", false); if (auto result = dispatch_request_result("textDocument/prepareCallHierarchy", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(main_uri) }, { "position", ToPosition(call_incoming_pos) }, }))) { seeded.call_hierarchy_incoming_item = FirstArrayItem(*result); } auto call_outgoing_pos = FindPosition(main_content, "TestDefinitions();", false); if (auto result = dispatch_request_result("textDocument/prepareCallHierarchy", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(main_uri) }, { "position", ToPosition(call_outgoing_pos) }, }))) { seeded.call_hierarchy_outgoing_item = FirstArrayItem(*result); } auto type_pos = FindPosition(type_hierarchy_content, "Mid = class", false); if (auto result = dispatch_request_result("textDocument/prepareTypeHierarchy", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(type_hierarchy_uri) }, { "position", ToPosition(type_pos) }, }))) { seeded.type_hierarchy_item = FirstArrayItem(*result); } if (auto result = dispatch_request_result("workspace/symbol", protocol::LSPAny(protocol::LSPObject{ { "query", protocol::string("Workspace") }, }))) { seeded.workspace_symbol = FirstArrayItem(*result); } if (auto result = dispatch_request_result("textDocument/codeAction", protocol::LSPAny(protocol::LSPObject{ { "textDocument", ToTextDocument(code_action_uri) }, { "range", ToRange(FullDocumentRange(code_action_content)) }, { "context", protocol::LSPObject{ { "diagnostics", code_action_diagnostics } } }, }))) { seeded.code_action = FirstArrayItem(*result); } { auto methods = env.dispatcher.GetSupportedRequests(); std::sort(methods.begin(), methods.end()); protocol::integer request_counter = 1; for (const auto& method : methods) { if (method == "initialize" || method == "shutdown") { continue; } protocol::RequestMessage request; request.id = "req_" + std::to_string(++request_counter); request.method = method; auto params = BuildRequestParams(method, seeded, main_uri, main_content, rename_uri, rename_content, code_action_uri, code_action_content, code_action_diagnostics); assertTrue(params.has_value(), "Missing request params builder for: " + method); request.params = *params; try { auto json = SerializeOrThrow(request); auto parsed = DeserializeOrThrow(json); auto response_json = env.dispatcher.Dispatch(parsed); auto response = codec::Deserialize(response_json); assertTrue(response.has_value(), "Request response should deserialize"); assertFalse(response->error.has_value(), "Request should not return error for: " + method); } catch (const std::exception& e) { throw std::runtime_error("Request method failed: " + method + " (" + e.what() + ")"); } } } send_notification("textDocument/didClose", main_uri, "", 0); send_notification("textDocument/didClose", rename_uri, "", 0); send_notification("textDocument/didClose", code_action_uri, "", 0); { protocol::RequestMessage shutdown; shutdown.id = "shutdown"; shutdown.method = "shutdown"; shutdown.params = protocol::LSPAny(protocol::LSPObject{}); auto json = SerializeOrThrow(shutdown); auto parsed = DeserializeOrThrow(json); auto response_json = env.dispatcher.Dispatch(parsed); auto response_any = codec::Deserialize(response_json); assertTrue(response_any.has_value(), "Shutdown response should be valid JSON"); } env.scheduler.WaitAll(); return result; } }