diff --git a/src/index/merged_index.cpp b/src/index/merged_index.cpp index b8c2dbbf2..1aea60eee 100644 --- a/src/index/merged_index.cpp +++ b/src/index/merged_index.cpp @@ -5,6 +5,7 @@ #include "index/serialization.h" #include "support/filesystem.h" +#include "syntax/lexer.h" #include "llvm/ADT/DenseSet.h" #include "llvm/Support/raw_os_ostream.h" @@ -443,6 +444,44 @@ void MergedIndex::lookup(this const Self& self, } } +std::optional MergedIndex::find_include_definition(this const Self& self, + std::uint32_t offset) { + if(self.impl) { + auto& index = *self.impl; + auto argument = find_directive_argument_at(index.content, offset); + if(!argument) { + return std::nullopt; + } + for(auto& [_, context]: index.compilation_contexts) { + for(auto& location: context.include_locations) { + if(location.include == static_cast(-1) && + location.line == argument->line) { + return location.path_id; + } + } + } + } else if(self.buffer) { + auto index = fbs::GetRoot(self.buffer->getBufferStart()); + llvm::StringRef content; + if(auto* stored_content = index->content()) { + content = stored_content->string_view(); + } + auto argument = find_directive_argument_at(content, offset); + if(!argument) { + return std::nullopt; + } + for(auto context: *index->compilation_contexts()) { + for(auto location: *context->include_locations()) { + if(location->include_id() == static_cast(-1) && + location->line() == argument->line) { + return location->path_id(); + } + } + } + } + return std::nullopt; +} + void MergedIndex::lookup(this const Self& self, SymbolHash symbol, RelationKind kind, diff --git a/src/index/merged_index.h b/src/index/merged_index.h index 2e4bc375e..89c7bb905 100644 --- a/src/index/merged_index.h +++ b/src/index/merged_index.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "index/tu_index.h" @@ -48,6 +49,10 @@ class MergedIndex { std::uint32_t offset, llvm::function_ref callback); + /// Find the included path id for an include directive argument at `offset`. + std::optional find_include_definition(this const Self& self, + std::uint32_t offset); + /// Lookup the relations of given symbol. void lookup(this const Self& self, SymbolHash symbol, diff --git a/src/server/compiler/compiler.cpp b/src/server/compiler/compiler.cpp index 990d2bfaf..4d9349790 100644 --- a/src/server/compiler/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -761,6 +761,8 @@ kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptrtext; ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16); sess->file_index = std::move(ofi); diff --git a/src/server/compiler/indexer.cpp b/src/server/compiler/indexer.cpp index 2012ff9a4..4859cc78f 100644 --- a/src/server/compiler/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -26,6 +26,23 @@ namespace clice { namespace lsp = kota::ipc::lsp; +namespace { + +auto zero_range() -> protocol::Range { + auto pos = protocol::Position{.line = 0, .character = 0}; + return protocol::Range{.start = pos, .end = pos}; +} + +auto file_location(llvm::StringRef path) -> std::optional { + auto uri = lsp::URI::from_file_path(std::string(path)); + if(!uri) { + return std::nullopt; + } + return protocol::Location{.uri = uri->str(), .range = zero_range()}; +} + +} // namespace + void Indexer::merge(const void* tu_index_data, std::size_t size) { auto tu_index = index::TUIndex::from(tu_index_data); if(tu_index.graph.paths.empty()) { @@ -243,10 +260,57 @@ Indexer::CursorHit Indexer::resolve_cursor(llvm::StringRef path, return {}; } +std::optional + Indexer::find_include_definition(llvm::StringRef path, + const protocol::Position& position, + Session* session) { + if(session && session->file_index && session->file_index->mapper) { + auto offset = session->file_index->mapper->to_offset(position); + if(!offset) { + return std::nullopt; + } + auto target = session->file_index->find_include_definition(*offset); + if(target && *target < session->file_index->include_paths.size()) { + return file_location(session->file_index->include_paths[*target]); + } + } + + const std::string* doc_text = session ? &session->text : nullptr; + if(!doc_text) { + return std::nullopt; + } + lsp::PositionMapper doc_mapper(*doc_text, lsp::PositionEncoding::UTF16); + auto offset = doc_mapper.to_offset(position); + if(!offset) { + return std::nullopt; + } + + auto proj_it = workspace.project_index.path_pool.find(path); + if(proj_it == workspace.project_index.path_pool.cache.end()) { + return std::nullopt; + } + auto shard_it = workspace.merged_indices.find(proj_it->second); + if(shard_it == workspace.merged_indices.end()) { + return std::nullopt; + } + + auto target = shard_it->second.find_include_definition(*offset); + if(!target || *target >= workspace.project_index.path_pool.paths.size()) { + return std::nullopt; + } + return file_location(workspace.project_index.path_pool.path(*target)); +} + std::vector Indexer::query_relations(llvm::StringRef path, const protocol::Position& position, RelationKind kind, Session* session) { + if(kind.value() == RelationKind::Definition) { + if(auto include = find_include_definition(path, position, session)) { + return {*include}; + } + } + auto hit = resolve_cursor(path, position, session); if(hit.hash == 0) return {}; diff --git a/src/server/compiler/indexer.h b/src/server/compiler/indexer.h index d1779cb26..1fb1a8d1e 100644 --- a/src/server/compiler/indexer.h +++ b/src/server/compiler/indexer.h @@ -224,6 +224,11 @@ class Indexer { const protocol::Position& position, Session* session); + /// Resolve an include directive argument at (position), if any. + std::optional find_include_definition(llvm::StringRef path, + const protocol::Position& position, + Session* session); + /// Collect relations grouped by target symbol, across all index sources. void collect_grouped_relations( index::SymbolHash hash, diff --git a/src/server/workspace/workspace.cpp b/src/server/workspace/workspace.cpp index 5a9640595..f5297097b 100644 --- a/src/server/workspace/workspace.cpp +++ b/src/server/workspace/workspace.cpp @@ -5,6 +5,7 @@ #include "support/filesystem.h" #include "support/logging.h" +#include "syntax/lexer.h" #include "syntax/scan.h" #include "kota/codec/json/json.h" @@ -36,6 +37,24 @@ const static index::Occurrence* lookup_occurrence(const std::vector + lookup_include_definition(llvm::StringRef content, + llvm::ArrayRef includes, + std::uint32_t offset) { + auto argument = find_directive_argument_at(content, offset); + if(!argument) { + return std::nullopt; + } + + for(auto& include: includes) { + if(include.include == static_cast(-1) && + include.line == argument->line) { + return include.path_id; + } + } + return std::nullopt; +} + std::optional> OpenFileIndex::find_occurrence(std::uint32_t offset) const { if(!mapper) @@ -53,6 +72,10 @@ std::optional> }; } +std::optional OpenFileIndex::find_include_definition(std::uint32_t offset) const { + return lookup_include_definition(content, include_locations, offset); +} + std::optional> MergedIndexShard::find_occurrence(std::uint32_t offset) const { auto* m = mapper(); @@ -73,6 +96,10 @@ std::optional> return result; } +std::optional MergedIndexShard::find_include_definition(std::uint32_t offset) const { + return index.find_include_definition(offset); +} + llvm::SmallVector Workspace::on_file_saved(std::uint32_t path_id) { llvm::SmallVector dirtied; diff --git a/src/server/workspace/workspace.h b/src/server/workspace/workspace.h index 32bba802a..bc8ace2e4 100644 --- a/src/server/workspace/workspace.h +++ b/src/server/workspace/workspace.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "command/command.h" #include "command/toolchain.h" @@ -56,6 +57,8 @@ struct HeaderFileContext { struct OpenFileIndex { index::FileIndex file_index; index::SymbolTable symbols; + std::vector include_paths; + std::vector include_locations; std::string content; ///< Buffer text at index time (for position mapping). /// Cached PositionMapper built from `content`. Avoids re-scanning line @@ -67,6 +70,9 @@ struct OpenFileIndex { std::optional> find_occurrence(std::uint32_t offset) const; + /// Find an include definition target path id at byte offset. + std::optional find_include_definition(std::uint32_t offset) const; + /// Iterate relations matching `kind`, calling back with pre-converted ranges. /// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue). template @@ -115,6 +121,9 @@ struct MergedIndexShard { std::optional> find_occurrence(std::uint32_t offset) const; + /// Find an include definition target path id at byte offset. + std::optional find_include_definition(std::uint32_t offset) const; + /// Iterate relations matching `kind`, calling back with pre-converted ranges. /// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue). template diff --git a/src/syntax/lexer.cpp b/src/syntax/lexer.cpp index 42c2b1b4c..dc0ca0f0d 100644 --- a/src/syntax/lexer.cpp +++ b/src/syntax/lexer.cpp @@ -162,4 +162,33 @@ std::optional find_directive_argument(llvm::StringRef content, return std::nullopt; } +std::optional find_directive_argument_at(llvm::StringRef content, + std::uint32_t offset, + const clang::LangOptions* lang_opts) { + if(offset >= content.size()) { + return std::nullopt; + } + + std::uint32_t line_start = 0; + if(offset > 0) { + auto pos = content.rfind('\n', offset - 1); + if(pos != llvm::StringRef::npos) { + line_start = static_cast(pos + 1); + } + } + + auto range = find_directive_argument(content, line_start, lang_opts); + if(!range || !range->contains(offset)) { + return std::nullopt; + } + + std::uint32_t line = 1; + for(char c: content.take_front(line_start)) { + if(c == '\n') { + ++line; + } + } + return DirectiveArgument{*range, line}; +} + } // namespace clice diff --git a/src/syntax/lexer.h b/src/syntax/lexer.h index 1fa38d037..1e66682d0 100644 --- a/src/syntax/lexer.h +++ b/src/syntax/lexer.h @@ -82,4 +82,17 @@ std::optional std::uint32_t offset, const clang::LangOptions* lang_opts = nullptr); +struct DirectiveArgument { + LocalSourceRange range; + std::uint32_t line = 0; +}; + +/// Find the directive argument containing `offset`. +/// Returns its source range and 1-based line number, or nullopt if `offset` +/// is not inside a directive argument. +std::optional + find_directive_argument_at(llvm::StringRef content, + std::uint32_t offset, + const clang::LangOptions* lang_opts = nullptr); + } // namespace clice diff --git a/tests/corpus/definition/include_definition.test b/tests/corpus/definition/include_definition.test new file mode 100644 index 000000000..51930f3aa --- /dev/null +++ b/tests/corpus/definition/include_definition.test @@ -0,0 +1,11 @@ +#[test.h] +#pragma once +int from_test; + +#[macro.h] +int from_macro; + +#[main.cpp] +#define HEADER "macro.h" +#$(outside)include "$(quoted)test.h" +#include $(macro)HEADER diff --git a/tests/snapshots/include_definition/snapshot/definition/include_definition.test.snap.yml b/tests/snapshots/include_definition/snapshot/definition/include_definition.test.snap.yml new file mode 100644 index 000000000..58ed25b12 --- /dev/null +++ b/tests/snapshots/include_definition/snapshot/definition/include_definition.test.snap.yml @@ -0,0 +1,8 @@ +--- +source: include_definition_tests.cpp +created_at: 2026-06-18 +input_file: definition/include_definition.test +--- +- { point: "macro", target: "macro.h", range: "0:0-0:0" } +- { point: "outside" } +- { point: "quoted", target: "test.h", range: "0:0-0:0" } diff --git a/tests/unit/index/include_definition_tests.cpp b/tests/unit/index/include_definition_tests.cpp new file mode 100644 index 000000000..1d16aaa3f --- /dev/null +++ b/tests/unit/index/include_definition_tests.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include + +#include "test/test.h" +#include "test/tester.h" +#include "index/merged_index.h" +#include "index/tu_index.h" +#include "support/filesystem.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/raw_ostream.h" + +namespace clice::testing { + +namespace { + +TEST_SUITE(include_definition, Tester) { + +auto target_name(const index::TUIndex& idx, std::optional target) -> std::string { + if(!target || *target >= idx.graph.paths.size()) { + return {}; + } + return path::filename(idx.graph.paths[*target]).str(); +} + +auto format_point(llvm::StringRef name, + const index::TUIndex& idx, + std::optional target) -> std::string { + auto result = std::format("- {{ point: {}", yaml_str(name)); + auto file = target_name(idx, target); + if(!file.empty()) { + result += std::format(", target: {}, range: \"0:0-0:0\"", yaml_str(file)); + } + result += " }"; + return result; +} + +TEST_CASE(snapshot) { + ASSERT_SNAPSHOT_GLOB(corpus_dir, "definition/**/*.test", ([&](std::string_view path) { + auto content = fs::read(path); + if(!content) { + return std::string("READ_ERROR"); + } + + clear(); + add_files("main.cpp", *content); + if(!compile()) { + return std::string("COMPILE_ERROR"); + } + + auto idx = index::TUIndex::build(*unit, true); + auto locations = idx.graph.locations; + + index::MergedIndex merged; + merged.merge(0, + idx.built_at, + std::move(locations), + idx.main_file_index, + sources.all_files["main.cpp"].content); + + llvm::SmallString<4096> buffer; + llvm::raw_svector_ostream os(buffer); + merged.serialize(os); + auto restored = index::MergedIndex(buffer); + + std::vector> points; + for(auto& [name, offset]: sources.all_files["main.cpp"].offsets) { + points.emplace_back(name.str(), offset); + } + std::ranges::sort(points); + + std::string result; + for(auto& [name, offset]: points) { + if(!result.empty()) { + result += '\n'; + } + result += format_point(name, idx, restored.find_include_definition(offset)); + } + return result; + })); +} + +}; // TEST_SUITE(include_definition) + +} // namespace + +} // namespace clice::testing diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index 649ee5c16..872f13622 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -199,7 +199,6 @@ TEST_CASE(GoToDefinitionReturnsEmpty) { auto result = co_await w.peer->send_request(params); CO_ASSERT_TRUE(result.has_value()); - // Should return empty array "[]" (TODO stub) EXPECT_EQ(result.value().data, std::string("[]")); test_done = true; w.peer->close_output();