tsl-devkit/lsp-server/src/provider/text_document/document_link.cppm

436 lines
15 KiB
C++

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<DocumentLink, IRequestProvider>
{
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<decltype(value)>{}(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<protocol::integer>(pos.line) },
{ "character", static_cast<protocol::integer>(pos.character) },
};
}
protocol::LSPObject ToRangeObject(const protocol::Range& range)
{
return protocol::LSPObject{
{ "start", ToPositionObject(range.start) },
{ "end", ToPositionObject(range.end) },
};
}
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<char>(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<std::string> 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<std::string> {
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<std::string> ResolveUnitTarget(const manager::Symbol& symbols,
const std::string& unit_name,
const std::optional<std::filesystem::path>& 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<std::filesystem::path>& base_dir,
const protocol::DocumentUri& base_uri,
protocol::LSPArray& links,
std::unordered_set<RangeKey, RangeKeyHash>& 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<RangeKey, RangeKeyHash>& 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()<protocol::DocumentLinkParams>(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<RangeKey, RangeKeyHash> seen;
if (content && tree)
{
std::optional<std::filesystem::path> 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();
}
}