Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 42 additions & 32 deletions src/feature/document_links.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,49 +23,59 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
PositionMapper converter(content, encoding);
auto& directives = directives_it->second;

links.reserve(directives.includes.size() + directives.has_includes.size());

for(const auto& include: directives.includes) {
auto [fid, range] = unit.decompose_range(include.filename_range);
if(fid != interested || !range.valid()) {
continue;
// Scan forward from offset to find a quoted/angled filename range.
auto find_filename_range = [&](std::uint32_t offset) -> std::optional<LocalSourceRange> {
auto tail = content.substr(offset);
auto quote_pos = tail.find_first_of("<\"");
if(quote_pos == llvm::StringRef::npos) {
return std::nullopt;
}
char open = tail[quote_pos];
char close = open == '<' ? '>' : '"';
auto close_pos = tail.find(close, quote_pos + 1);
if(close_pos == llvm::StringRef::npos) {
return std::nullopt;
}
return LocalSourceRange(offset + static_cast<std::uint32_t>(quote_pos),
offset + static_cast<std::uint32_t>(close_pos + 1));
};

protocol::DocumentLink link{
.range = to_range(converter, range),
};
link.target = std::string(unit.file_path(include.fid));
auto add_link_by_location = [&](clang::SourceLocation loc, llvm::StringRef target) {
auto [fid, offset] = unit.decompose_location(loc);
if(fid != interested || offset >= content.size()) {
return;
}
auto range = find_filename_range(offset);
if(!range) {
return;
}
protocol::DocumentLink link{.range = to_range(converter, *range)};
link.target = target.str();
links.push_back(std::move(link));
}
};

for(const auto& has_include: directives.has_includes) {
if(has_include.fid.isInvalid()) {
continue;
for(const auto& include: directives.includes) {
if(include.fid.isValid()) {
add_link_by_location(include.location, unit.file_path(include.fid));
}
}

auto [fid, offset] = unit.decompose_location(has_include.location);
if(fid != interested || offset >= content.size()) {
continue;
for(const auto& has_include: directives.has_includes) {
if(has_include.fid.isValid()) {
add_link_by_location(has_include.location, unit.file_path(has_include.fid));
}
}

auto tail = content.substr(offset);
char open = tail.front();
if(open != '<' && open != '"') {
continue;
for(const auto& embed: directives.embeds) {
if(embed.file) {
add_link_by_location(embed.loc, embed.file->getName());
}
}

char close = open == '<' ? '>' : '"';
auto close_index = tail.find(close, 1);
if(close_index == llvm::StringRef::npos) {
continue;
for(const auto& has_embed: directives.has_embeds) {
if(has_embed.file) {
add_link_by_location(has_embed.loc, has_embed.file->getName());
}

LocalSourceRange range(offset, offset + static_cast<std::uint32_t>(close_index + 1));
protocol::DocumentLink link{
.range = to_range(converter, range),
};
link.target = std::string(unit.file_path(has_include.fid));
links.push_back(std::move(link));
}

return links;
Expand Down
1 change: 1 addition & 0 deletions src/server/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ et::task<bool> Compiler::ensure_pch(Session& session,
st.bound = bound;
st.hash = preamble_hash;
st.deps = capture_deps_snapshot(workspace.path_pool, result.value().deps);
st.document_links_json = std::move(result.value().pch_links_json);
st.building.reset();

session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
Expand Down
41 changes: 32 additions & 9 deletions src/server/master_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,38 @@ void MasterServer::register_handlers() {
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
});

peer.on_request(
[this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::DocumentLink, sit->second);
});
peer.on_request([this](RequestContext& ctx,
const protocol::DocumentLinkParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
auto& session = sit->second;
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
if(!result.has_value())
co_return serde_raw{"null"};
// Merge document links from PCH if available.
auto& links = result.value();
// Re-lookup session after co_await since iterators may be invalidated.
auto sit2 = sessions.find(path_id);
if(sit2 != sessions.end() && sit2->second.pch_ref) {
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
auto& pch_json = pch_it->second.document_links_json;
// Merge two JSON arrays.
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
links.data.pop_back(); // remove trailing ']'
links.data += ',';
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
} else {
links.data = pch_json;
}
Comment thread
16bit-ykiko marked this conversation as resolved.
}
}
co_return std::move(links);
});

peer.on_request(
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
Expand Down
1 change: 1 addition & 0 deletions src/server/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ struct BuildResult {
std::string output_path; ///< PCH or PCM path
std::vector<std::string> deps;
std::string tu_index_data;
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
};

Expand Down
8 changes: 7 additions & 1 deletion src/server/stateless_worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
errors = collect_errors(unit);

std::string tu_index_data;
if(success)
std::string pch_links_json;
if(success) {
tu_index_data = serialize_tu_index(unit);
auto links = feature::document_links(unit);
auto raw = to_raw(links);
pch_links_json = std::move(raw.data);
}

// Destroy CompilationUnit to flush PCH to disk.
unit = CompilationUnit(nullptr);
Expand All @@ -110,6 +115,7 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
result.output_path = std::move(final_path);
result.deps = pch_info.deps;
result.tu_index_data = std::move(tu_index_data);
result.pch_links_json = std::move(pch_links_json);
return result;
} else {
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
Expand Down
1 change: 1 addition & 0 deletions src/server/workspace.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ struct PCHState {
std::uint32_t bound = 0;
std::uint64_t hash = 0;
DepsSnapshot deps;
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
std::shared_ptr<eventide::event> building;
};

Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ def _entry(directory: Path, source: Path, extra_args: list[str] | None = None):
if ic_main.exists():
_write(ic_dir, [_entry(ic_dir, ic_main, ["-I."])])

# document_links
dl_dir = data_dir / "document_links"
dl_main = dl_dir / "main.cpp"
if dl_main.exists():
_write(
dl_dir, [_entry(dl_dir, dl_main, [f"-I{dl_dir.as_posix()}", "-std=c++23"])]
)

# pch_test
pt_dir = data_dir / "pch_test"
if pt_dir.exists():
Expand Down
1 change: 1 addition & 0 deletions tests/data/document_links/data.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0123456789
3 changes: 3 additions & 0 deletions tests/data/document_links/header_a.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

int a = 1;
3 changes: 3 additions & 0 deletions tests/data/document_links/header_b.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

int b = 2;
3 changes: 3 additions & 0 deletions tests/data/document_links/header_c.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

int c = 3;
20 changes: 20 additions & 0 deletions tests/data/document_links/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "header_a.h"
#include "header_b.h"
int x = 1;
#include "header_c.h"

const char data[] = {
#embed "data.bin"
};

#if __has_embed("data.bin")
int has_embed_found = 1;
#endif

#if __has_embed("no_such_file.bin")
int has_embed_not_found = 1;
#endif

int main() {
return a + b + c;
}
103 changes: 103 additions & 0 deletions tests/integration/features/test_document_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from pathlib import Path

import pytest


@pytest.mark.workspace("document_links")
async def test_document_links_with_pch(client, workspace):
uri, content = await client.open_and_wait(workspace / "main.cpp")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
links = await client.document_links(uri)

assert links is not None, "document_links returned None"

targets = sorted(Path(link.target).name for link in links)
assert targets == [
"data.bin",
"data.bin",
"header_a.h",
"header_b.h",
"header_c.h",
], f"Unexpected targets: {targets}"

client.close(uri)


@pytest.mark.workspace("document_links")
async def test_document_links_pch_portion(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
links = await client.document_links(uri)

pch_links = [link for link in links if link.range.start.line < 2]
assert len(pch_links) == 2, (
f"Expected 2 PCH links (lines 0-1), got {len(pch_links)}"
)

pch_targets = sorted(Path(link.target).name for link in pch_links)
assert pch_targets == ["header_a.h", "header_b.h"]

client.close(uri)


@pytest.mark.workspace("document_links")
async def test_document_links_main_portion(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
links = await client.document_links(uri)

main_links = [link for link in links if link.range.start.line >= 2]
assert len(main_links) == 3, (
f"Expected 3 main-file links (lines 3, 6, 9), got {len(main_links)}"
)

main_targets = sorted(Path(link.target).name for link in main_links)
assert main_targets == ["data.bin", "data.bin", "header_c.h"]

client.close(uri)


@pytest.mark.workspace("document_links")
async def test_document_links_embed(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
links = await client.document_links(uri)

embed_links = [
link
for link in links
if Path(link.target).name == "data.bin" and link.range.start.line == 6
]
assert len(embed_links) == 1, (
f"Expected 1 embed link at line 6, got {len(embed_links)}"
)

client.close(uri)


@pytest.mark.workspace("document_links")
async def test_document_links_has_embed_exists(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
links = await client.document_links(uri)

has_embed_links = [
link
for link in links
if Path(link.target).name == "data.bin" and link.range.start.line == 9
]
assert len(has_embed_links) == 1, (
f"Expected 1 has_embed link at line 9, got {len(has_embed_links)}"
)

client.close(uri)


@pytest.mark.workspace("document_links")
async def test_document_links_has_embed_missing(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
links = await client.document_links(uri)

missing_links = [
link for link in links if Path(link.target).name == "no_such_file.bin"
]
assert len(missing_links) == 0, (
f"Expected 0 links for non-existent file, got {len(missing_links)}"
)

client.close(uri)
38 changes: 36 additions & 2 deletions tests/unit/feature/document_link_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ TEST_SUITE(DocumentLink, Tester) {

std::vector<protocol::DocumentLink> links;

void run(llvm::StringRef source) {
void run(llvm::StringRef source, llvm::StringRef standard = "-std=c++17") {
add_files("main.cpp", source);
ASSERT_TRUE(compile());
ASSERT_TRUE(compile(standard));
links = feature::document_links(*unit, feature::PositionEncoding::UTF8);
}

Expand Down Expand Up @@ -89,6 +89,40 @@ TEST_CASE(HasInclude) {
EXPECT_LINK(1, "1", TestVFS::path("test.h"));
}

TEST_CASE(Embed) {
run(R"cpp(
#[bytes.bin]
0123456789

#[main.cpp]
const char e[] = {
#embed @0["bytes.bin"$]
};
)cpp",
"-std=c++23");

ASSERT_EQ(links.size(), 1U);
EXPECT_LINK(0, "0", TestVFS::path("bytes.bin"));
}

TEST_CASE(HasEmbed) {
run(R"cpp(
#[data.bin]
ABCDE

#[main.cpp]
#if __has_embed(@0["data.bin"$])
#endif

#if __has_embed("non_existent.bin")
#endif
)cpp",
"-std=c++23");

ASSERT_EQ(links.size(), 1U);
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
}

}; // TEST_SUITE(DocumentLink)

} // namespace
Expand Down
Loading