module; export module lsp.provider.text_document.document_link; import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; import lsp.manager.manager_hub; import lsp.manager.symbol; import lsp.provider.base.interface; import lsp.utils.string; namespace codec = lsp::codec; export namespace lsp::provider::text_document { class DocumentLink : public AutoRegisterProvider { public: static constexpr std::string_view kMethod = "textDocument/documentLink"; static constexpr std::string_view kProviderName = "TextDocumentDocumentLink"; DocumentLink() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; }; } namespace lsp::provider::text_document { namespace { namespace utils = lsp::utils; struct RangeKey { protocol::uinteger start_line = 0; protocol::uinteger start_character = 0; protocol::uinteger end_line = 0; protocol::uinteger end_character = 0; bool operator==(const RangeKey& other) const { return start_line == other.start_line && start_character == other.start_character && end_line == other.end_line && end_character == other.end_character; } }; struct RangeKeyHash { std::size_t operator()(const RangeKey& key) const { std::size_t seed = 0; auto hash_combine = [&seed](auto value) { seed ^= std::hash{}(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2); }; hash_combine(key.start_line); hash_combine(key.start_character); hash_combine(key.end_line); hash_combine(key.end_character); return seed; } }; 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) }, }; } RangeKey ToRangeKey(const protocol::Range& range) { return RangeKey{ range.start.line, range.start.character, range.end.line, range.end.character, }; } protocol::Range ToRange(TSNode node) { TSPoint start = ts_node_start_point(node); TSPoint end = ts_node_end_point(node); protocol::Range range; range.start.line = start.row; range.start.character = start.column; range.end.line = end.row; range.end.character = end.column; return range; } std::string StripQuotes(std::string_view text) { if (text.size() >= 2) { char front = text.front(); char back = text.back(); if ((front == '"' && back == '"') || (front == '\'' && back == '\'')) { return std::string(text.substr(1, text.size() - 2)); } } return std::string(text); } bool LooksLikePath(std::string_view text) { if (text.starts_with("file://")) { return true; } if (text.find('/') != std::string_view::npos || text.find('\\') != std::string_view::npos) { return true; } if (text.ends_with(".tsl") || text.ends_with(".tsf")) { return true; } return false; } std::string PathToUri(const std::filesystem::path& path) { auto absolute = std::filesystem::absolute(path).generic_string(); #ifdef _WIN32 std::replace(absolute.begin(), absolute.end(), '\\', '/'); #endif if (!absolute.starts_with("/")) absolute = "/" + absolute; return "file://" + absolute; } std::string UriToPath(const std::string& uri) { std::string path = uri; if (path.starts_with("file://")) path = path.substr(7); #ifdef _WIN32 if (!path.empty() && path[0] == '/') path = path.substr(1); std::replace(path.begin(), path.end(), '/', '\\'); #endif std::string decoded; decoded.reserve(path.size()); for (size_t i = 0; i < path.size(); ++i) { if (path[i] == '%' && i + 2 < path.size()) { std::string hex = path.substr(i + 1, 2); char ch = static_cast(std::stoi(hex, nullptr, 16)); decoded.push_back(ch); i += 2; } else if (path[i] == '+') { decoded.push_back(' '); } else { decoded.push_back(path[i]); } } return decoded; } std::optional ResolvePathTarget(std::string_view raw, const protocol::DocumentUri& base_uri) { if (raw.empty()) { return std::nullopt; } if (raw.starts_with("file://")) { return std::string(raw); } std::filesystem::path path(raw); if (path.is_relative()) { auto base_path = UriToPath(base_uri); std::filesystem::path base_dir = std::filesystem::path(base_path).parent_path(); path = base_dir / path; } auto try_candidate = [](const std::filesystem::path& candidate) -> std::optional { if (std::filesystem::exists(candidate)) { return PathToUri(candidate); } return std::nullopt; }; if (path.has_extension()) { if (auto uri = try_candidate(path)) { return uri; } } else { if (auto uri = try_candidate(path.string() + ".tsl")) { return uri; } if (auto uri = try_candidate(path.string() + ".tsf")) { return uri; } } return std::nullopt; } std::optional ResolveUnitTarget(const manager::Symbol& symbols, const std::string& unit_name, const std::optional& base_dir) { auto indexed = symbols.QueryIndexedSymbols(protocol::SymbolKind::Module); for (const auto& item : indexed) { if (utils::IEquals(item.name, unit_name)) { return item.uri; } } if (base_dir) { auto candidate = *base_dir / (unit_name + ".tsf"); if (std::filesystem::exists(candidate)) { return PathToUri(candidate); } } return std::nullopt; } void CollectUsesLinks(TSNode node, const protocol::string& content, const manager::Symbol& symbols, const std::optional& base_dir, const protocol::DocumentUri& base_uri, protocol::LSPArray& links, std::unordered_set& seen) { if (ts_node_is_null(node)) { return; } if (std::string_view(ts_node_type(node)) == "uses_statement") { uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; ++i) { const char* field = ts_node_field_name_for_child(node, i); if (!field || std::string_view(field) != "unit") { continue; } TSNode unit_node = ts_node_child(node, i); uint32_t start = ts_node_start_byte(unit_node); uint32_t end = ts_node_end_byte(unit_node); if (start >= content.size() || end > content.size() || start >= end) { continue; } std::string unit_name = std::string(content.substr(start, end - start)); auto range = ToRange(unit_node); auto key = ToRangeKey(range); if (!seen.insert(key).second) { continue; } protocol::LSPObject link; link["range"] = ToRangeObject(range); if (auto target = ResolveUnitTarget(symbols, unit_name, base_dir)) { link["target"] = protocol::string(*target); } else { protocol::LSPObject data; data["kind"] = protocol::string("unit"); data["name"] = protocol::string(unit_name); data["baseUri"] = protocol::string(base_uri); link["data"] = protocol::LSPAny(std::move(data)); } links.emplace_back(std::move(link)); } } uint32_t child_count = ts_node_child_count(node); for (uint32_t i = 0; i < child_count; ++i) { CollectUsesLinks(ts_node_child(node, i), content, symbols, base_dir, base_uri, links, seen); } } void CollectPathLinks(TSNode node, const protocol::string& content, const protocol::DocumentUri& base_uri, protocol::LSPArray& links, std::unordered_set& seen) { if (ts_node_is_null(node)) { return; } if (std::string_view(ts_node_type(node)) == "string") { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); if (start < content.size() && end <= content.size() && start < end) { std::string raw_text = std::string(content.substr(start, end - start)); std::string path_text = StripQuotes(raw_text); if (LooksLikePath(path_text)) { auto range = ToRange(node); auto key = ToRangeKey(range); if (seen.insert(key).second) { protocol::LSPObject link; link["range"] = ToRangeObject(range); if (auto target = ResolvePathTarget(path_text, base_uri)) { link["target"] = protocol::string(*target); } else { protocol::LSPObject data; data["kind"] = protocol::string("path"); data["path"] = protocol::string(path_text); data["baseUri"] = protocol::string(base_uri); link["data"] = protocol::LSPAny(std::move(data)); } links.emplace_back(std::move(link)); } } } } uint32_t child_count = ts_node_child_count(node); for (uint32_t i = 0; i < child_count; ++i) { CollectPathLinks(ts_node_child(node, i), content, base_uri, links, seen); } } } std::string DocumentLink::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentDocumentLinkProvider: Providing response for method {}", request.method); if (!request.params.has_value()) { spdlog::warn("{}: Missing params in request", GetProviderName()); return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); } auto params = codec::FromLSPAny.template operator()(request.params.value()); auto& hub = context.GetManagerHub(); auto content = hub.documents().GetContent(params.textDocument.uri); auto tree = hub.parser().GetTree(params.textDocument.uri); protocol::LSPArray links; std::unordered_set seen; if (content && tree) { std::optional base_dir; try { auto base_path = UriToPath(params.textDocument.uri); base_dir = std::filesystem::path(base_path).parent_path(); } catch (const std::exception&) { base_dir = std::nullopt; } auto root = ts_tree_root_node(tree); CollectUsesLinks(root, *content, hub.symbols(), base_dir, params.textDocument.uri, links, seen); CollectPathLinks(root, *content, params.textDocument.uri, links, seen); } protocol::ResponseMessage response; response.id = request.id; response.result = protocol::LSPAny(std::move(links)); auto json = codec::Serialize(response); if (!json.has_value()) { return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); } return json.value(); } }