436 lines
15 KiB
C++
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();
|
|
}
|
|
}
|