#include "server/compiler/indexer.h" #include #include #include #include #include "index/tu_index.h" #include "server/compiler/compiler.h" #include "server/protocol/worker.h" #include "server/service/session.h" #include "server/worker/worker_pool.h" #include "support/filesystem.h" #include "support/logging.h" #include "kota/ipc/lsp/position.h" #include "kota/ipc/lsp/protocol.h" #include "kota/ipc/lsp/uri.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" namespace clice { namespace lsp = kota::ipc::lsp; 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()) { LOG_WARN("Ignoring TUIndex with empty path graph"); return; } auto file_ids_map = workspace.project_index.merge(tu_index); auto main_tu_path_id = static_cast(tu_index.graph.paths.size() - 1); auto merge_file_index = [&](std::uint32_t tu_path_id, index::FileIndex& file_idx) { auto global_path_id = file_ids_map[tu_path_id]; auto& shard = workspace.merged_indices[global_path_id]; if(tu_path_id == main_tu_path_id) { std::vector include_locs; for(auto& loc: tu_index.graph.locations) { index::IncludeLocation remapped = loc; remapped.path_id = file_ids_map[loc.path_id]; include_locs.push_back(remapped); } auto file_path = workspace.project_index.path_pool.path(global_path_id); llvm::StringRef file_content; std::string file_content_storage; auto buf = llvm::MemoryBuffer::getFile(file_path); if(buf) { file_content_storage = (*buf)->getBuffer().str(); file_content = file_content_storage; } shard.index.merge(global_path_id, tu_index.built_at, std::move(include_locs), file_idx, file_content); } else { std::optional include_id; for(std::uint32_t i = 0; i < tu_index.graph.locations.size(); ++i) { if(tu_index.graph.locations[i].path_id == tu_path_id) { include_id = i; break; } } if(!include_id) { LOG_WARN("Skip merge for path {}: include location not found", global_path_id); return; } auto header_path = workspace.project_index.path_pool.path(global_path_id); llvm::StringRef header_content; std::string header_content_storage; auto header_buf = llvm::MemoryBuffer::getFile(header_path); if(header_buf) { header_content_storage = (*header_buf)->getBuffer().str(); header_content = header_content_storage; } shard.index.merge(global_path_id, *include_id, file_idx, header_content); } shard.invalidate_mapper(); }; for(auto& [tu_path_id, file_idx]: tu_index.path_file_indices) { merge_file_index(tu_path_id, file_idx); } merge_file_index(main_tu_path_id, tu_index.main_file_index); LOG_INFO("Merged TUIndex: {} paths, {} symbols, {} merged_shards", tu_index.graph.paths.size(), tu_index.symbols.size(), workspace.merged_indices.size()); } void Indexer::save(llvm::StringRef index_dir) { if(index_dir.empty()) return; auto ec = llvm::sys::fs::create_directories(index_dir); if(ec) { LOG_WARN("Failed to create index directory {}: {}", std::string(index_dir), ec.message()); return; } auto project_path = path::join(index_dir, "project.idx"); { std::error_code write_ec; llvm::raw_fd_ostream os(project_path, write_ec); if(!write_ec) { workspace.project_index.serialize(os); LOG_INFO("Saved ProjectIndex to {}", project_path); } else { LOG_WARN("Failed to save ProjectIndex: {}", write_ec.message()); } } auto shards_dir = path::join(index_dir, "shards"); ec = llvm::sys::fs::create_directories(shards_dir); if(ec) { LOG_WARN("Failed to create shards directory: {}", ec.message()); return; } std::size_t saved = 0; for(auto& [path_id, shard]: workspace.merged_indices) { if(!shard.index.need_rewrite()) continue; auto shard_path = path::join(shards_dir, std::to_string(path_id) + ".idx"); std::error_code write_ec; llvm::raw_fd_ostream os(shard_path, write_ec); if(!write_ec) { shard.index.serialize(os); ++saved; } } LOG_INFO("Saved {} MergedIndex shards (of {} total)", saved, workspace.merged_indices.size()); } void Indexer::load(llvm::StringRef index_dir) { if(index_dir.empty()) return; auto project_path = path::join(index_dir, "project.idx"); auto buf = llvm::MemoryBuffer::getFile(project_path); if(buf) { workspace.project_index = index::ProjectIndex::from((*buf)->getBufferStart()); LOG_INFO("Loaded ProjectIndex: {} symbols", workspace.project_index.symbols.size()); } auto shards_dir = path::join(index_dir, "shards"); std::error_code ec; for(auto it = llvm::sys::fs::directory_iterator(shards_dir, ec); !ec && it != llvm::sys::fs::directory_iterator(); it.increment(ec)) { auto filename = llvm::sys::path::filename(it->path()); if(!filename.ends_with(".idx")) continue; auto stem = filename.drop_back(4); std::uint32_t path_id = 0; if(stem.getAsInteger(10, path_id)) continue; workspace.merged_indices[path_id] = MergedIndexShard{index::MergedIndex::load(it->path())}; } if(!workspace.merged_indices.empty()) { LOG_INFO("Loaded {} MergedIndex shards", workspace.merged_indices.size()); } } bool Indexer::need_update(llvm::StringRef file_path) { auto cache_it = workspace.project_index.path_pool.find(file_path); if(cache_it == workspace.project_index.path_pool.cache.end()) return true; auto merged_it = workspace.merged_indices.find(cache_it->second); if(merged_it == workspace.merged_indices.end()) return true; llvm::SmallVector path_mapping; for(auto& p: workspace.project_index.path_pool.paths) { path_mapping.push_back(p); } return merged_it->second.index.need_update(path_mapping); } bool Indexer::find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const { for(auto& [_, session]: sessions) { if(!session.file_index) continue; auto it = session.file_index->symbols.find(hash); if(it != session.file_index->symbols.end()) { name = it->second.name; kind = it->second.kind; return true; } } auto it = workspace.project_index.symbols.find(hash); if(it != workspace.project_index.symbols.end()) { name = it->second.name; kind = it->second.kind; return true; } return false; } Indexer::CursorHit Indexer::resolve_cursor(llvm::StringRef path, const protocol::Position& position, Session* session) { // Try the session's open file index first. if(session && session->file_index) { auto& index = *session->file_index; if(!index.mapper) return {}; auto offset = index.mapper->to_offset(position); if(!offset) return {}; if(auto found = index.find_occurrence(*offset)) return {found->first, found->second}; return {}; } // Fallback to MergedIndex, using session text (or reading from disk) for position -> offset. const std::string* doc_text = session ? &session->text : nullptr; if(!doc_text) return {}; lsp::PositionMapper doc_mapper(*doc_text, lsp::PositionEncoding::UTF16); auto offset = doc_mapper.to_offset(position); if(!offset) return {}; auto proj_it = workspace.project_index.path_pool.find(path); if(proj_it == workspace.project_index.path_pool.cache.end()) return {}; auto shard_it = workspace.merged_indices.find(proj_it->second); if(shard_it == workspace.merged_indices.end()) return {}; if(auto found = shard_it->second.find_occurrence(*offset)) return {found->first, found->second}; return {}; } std::vector Indexer::query_relations(llvm::StringRef path, const protocol::Position& position, RelationKind kind, Session* session) { auto hit = resolve_cursor(path, position, session); if(hit.hash == 0) return {}; std::vector locations; auto sym_it = workspace.project_index.symbols.find(hit.hash); if(sym_it != workspace.project_index.symbols.end()) { for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id)); if(!uri) continue; shard_it->second.find_relations(hit.hash, kind, [&](const auto&, protocol::Range range) { locations.push_back({uri->str(), range}); return true; }); } } for(auto& [id, sess]: sessions) { if(!sess.file_index) continue; auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id))); if(!uri) continue; sess.file_index->find_relations(hit.hash, kind, [&](const auto&, protocol::Range range) { locations.push_back({uri->str(), range}); return true; }); } return locations; } std::optional Indexer::lookup_symbol(const std::string& uri, llvm::StringRef path, const protocol::Position& position, Session* session) { auto hit = resolve_cursor(path, position, session); if(hit.hash == 0) return std::nullopt; std::string name; SymbolKind sym_kind; if(!find_symbol_info(hit.hash, name, sym_kind)) return std::nullopt; return SymbolInfo{hit.hash, std::move(name), sym_kind, uri, hit.range}; } std::optional Indexer::find_definition_location(index::SymbolHash hash) { // Open file indices first (fresher data for actively-edited files). for(auto& [id, sess]: sessions) { if(!sess.file_index) continue; auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id))); if(!uri) continue; std::optional result; sess.file_index->find_relations(hash, RelationKind::Definition, [&](const auto&, protocol::Range range) { result = protocol::Location{uri->str(), range}; return false; }); if(result) return result; } // Fall back to ProjectIndex reference files. auto sym_it = workspace.project_index.symbols.find(hash); if(sym_it == workspace.project_index.symbols.end()) return std::nullopt; for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id)); if(!uri) continue; std::optional result; shard_it->second.find_relations(hash, RelationKind::Definition, [&](const auto&, protocol::Range range) { result = protocol::Location{uri->str(), range}; return false; }); if(result) return result; } return std::nullopt; } std::optional Indexer::resolve_hierarchy_item(const std::string& uri, llvm::StringRef path, const protocol::Range& range, const std::optional& data, Session* session) { if(data) { if(auto* int_val = std::get_if(&*data)) { auto hash = static_cast(*int_val); std::string name; SymbolKind kind; if(find_symbol_info(hash, name, kind)) { return SymbolInfo{hash, std::move(name), kind, uri, range}; } } } return lookup_symbol(uri, path, range.start, session); } void Indexer::collect_grouped_relations( index::SymbolHash hash, RelationKind kind, llvm::DenseMap>& target_ranges) { auto sym_it = workspace.project_index.symbols.find(hash); if(sym_it != workspace.project_index.symbols.end()) { for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; shard_it->second.find_relations(hash, kind, [&](const auto& r, protocol::Range range) { target_ranges[r.target_symbol].push_back(range); return true; }); } } for(auto& [_, sess]: sessions) { if(!sess.file_index) continue; sess.file_index->find_relations(hash, kind, [&](const auto& r, protocol::Range range) { target_ranges[r.target_symbol].push_back(range); return true; }); } } void Indexer::collect_unique_targets(index::SymbolHash hash, RelationKind kind, llvm::SmallVectorImpl& targets) { llvm::DenseSet seen; auto sym_it = workspace.project_index.symbols.find(hash); if(sym_it != workspace.project_index.symbols.end()) { for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; /// No position conversion needed -- just collect target symbol hashes. shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) { if(seen.insert(r.target_symbol).second) { targets.push_back(r.target_symbol); } return true; }); } } for(auto& [_, sess]: sessions) { if(!sess.file_index) continue; auto rel_it = sess.file_index->file_index.relations.find(hash); if(rel_it == sess.file_index->file_index.relations.end()) continue; for(auto& r: rel_it->second) { if(r.kind & kind) { if(seen.insert(r.target_symbol).second) { targets.push_back(r.target_symbol); } } } } } /// Resolve a symbol hash into a SymbolInfo with definition location. /// Returns nullopt if the symbol or its definition cannot be found. std::optional Indexer::resolve_symbol(index::SymbolHash hash) { std::string name; SymbolKind kind; if(!find_symbol_info(hash, name, kind)) return std::nullopt; auto def_loc = find_definition_location(hash); if(!def_loc) return std::nullopt; return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range}; } static std::string extract_line(llvm::StringRef content, std::uint32_t offset) { if(content.empty() || offset >= content.size()) return {}; std::size_t line_start = 0; if(offset > 0) { auto pos = content.rfind('\n', offset - 1); if(pos != llvm::StringRef::npos) line_start = pos + 1; } auto line_end = content.find('\n', offset); if(line_end == llvm::StringRef::npos) line_end = content.size(); return content.slice(line_start, line_end).str(); } std::optional Indexer::get_definition_text(index::SymbolHash hash) { for(auto& [id, sess]: sessions) { if(!sess.file_index || !sess.file_index->mapper) continue; auto it = sess.file_index->file_index.relations.find(hash); if(it == sess.file_index->file_index.relations.end()) continue; for(auto& rel: it->second) { if(rel.kind.value() != RelationKind::Definition) continue; auto def_range = std::bit_cast(rel.target_symbol); if(def_range.begin >= def_range.end) continue; llvm::StringRef content = sess.file_index->content; if(def_range.end > content.size()) continue; auto start = sess.file_index->mapper->to_position(def_range.begin); auto end = sess.file_index->mapper->to_position(def_range.end); if(!start || !end) continue; return DefinitionText{ .file = std::string(workspace.path_pool.resolve(id)), .start_line = static_cast(start->line) + 1, .end_line = static_cast(end->line) + 1, .text = std::string(content.substr(def_range.begin, def_range.end - def_range.begin)), }; } } auto sym_it = workspace.project_index.symbols.find(hash); if(sym_it == workspace.project_index.symbols.end()) return std::nullopt; for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; auto* m = shard_it->second.mapper(); if(!m) continue; auto content = shard_it->second.index.content(); std::optional result; shard_it->second.index.lookup( hash, RelationKind::Definition, [&](const index::Relation& r) { auto def_range = std::bit_cast(r.target_symbol); if(def_range.begin >= def_range.end || def_range.end > content.size()) return true; auto start = m->to_position(def_range.begin); auto end = m->to_position(def_range.end); if(!start || !end) return true; result = DefinitionText{ .file = workspace.project_index.path_pool.path(file_id).str(), .start_line = static_cast(start->line) + 1, .end_line = static_cast(end->line) + 1, .text = std::string( content.substr(def_range.begin, def_range.end - def_range.begin)), }; return false; }); if(result) return result; } return std::nullopt; } std::vector Indexer::collect_references(index::SymbolHash hash, RelationKind kind) { std::vector results; auto sym_it = workspace.project_index.symbols.find(hash); if(sym_it != workspace.project_index.symbols.end()) { for(auto file_id: sym_it->second.reference_files) { if(is_proj_path_open(file_id)) continue; auto shard_it = workspace.merged_indices.find(file_id); if(shard_it == workspace.merged_indices.end()) continue; auto* m = shard_it->second.mapper(); if(!m) continue; auto content = shard_it->second.index.content(); auto file_path = workspace.project_index.path_pool.path(file_id); shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) { auto start = m->to_position(r.range.begin); if(!start) return true; results.push_back(ReferenceWithContext{ .file = file_path.str(), .line = static_cast(start->line) + 1, .context = extract_line(content, r.range.begin), }); return true; }); } } for(auto& [id, sess]: sessions) { if(!sess.file_index || !sess.file_index->mapper) continue; auto it = sess.file_index->file_index.relations.find(hash); if(it == sess.file_index->file_index.relations.end()) continue; auto file_path = workspace.path_pool.resolve(id); llvm::StringRef content = sess.file_index->content; for(auto& rel: it->second) { if(rel.kind != kind) continue; auto start = sess.file_index->mapper->to_position(rel.range.begin); if(!start) continue; results.push_back(ReferenceWithContext{ .file = file_path.str(), .line = static_cast(start->line) + 1, .context = extract_line(content, rel.range.begin), }); } } return results; } std::vector Indexer::find_incoming_calls(index::SymbolHash hash) { llvm::DenseMap> caller_ranges; collect_grouped_relations(hash, RelationKind::Caller, caller_ranges); std::vector results; for(auto& [caller_hash, ranges]: caller_ranges) { auto info = resolve_symbol(caller_hash); if(!info) continue; results.push_back({build_call_hierarchy_item(*info), std::move(ranges)}); } return results; } std::vector Indexer::find_outgoing_calls(index::SymbolHash hash) { llvm::DenseMap> callee_ranges; collect_grouped_relations(hash, RelationKind::Callee, callee_ranges); std::vector results; for(auto& [callee_hash, ranges]: callee_ranges) { auto info = resolve_symbol(callee_hash); if(!info) continue; results.push_back({build_call_hierarchy_item(*info), std::move(ranges)}); } return results; } std::vector Indexer::find_supertypes(index::SymbolHash hash) { llvm::SmallVector base_hashes; collect_unique_targets(hash, RelationKind::Base, base_hashes); std::vector results; for(auto target_hash: base_hashes) { auto info = resolve_symbol(target_hash); if(!info) continue; results.push_back(build_type_hierarchy_item(*info)); } return results; } std::vector Indexer::find_subtypes(index::SymbolHash hash) { llvm::SmallVector derived_hashes; collect_unique_targets(hash, RelationKind::Derived, derived_hashes); std::vector results; for(auto target_hash: derived_hashes) { auto info = resolve_symbol(target_hash); if(!info) continue; results.push_back(build_type_hierarchy_item(*info)); } return results; } std::vector Indexer::search_symbols(llvm::StringRef query, std::size_t max_results) { std::string query_lower = query.lower(); auto is_indexable_kind = [](SymbolKind sk) { return sk == SymbolKind::Namespace || sk == SymbolKind::Class || sk == SymbolKind::Struct || sk == SymbolKind::Union || sk == SymbolKind::Enum || sk == SymbolKind::Type || sk == SymbolKind::Field || sk == SymbolKind::EnumMember || sk == SymbolKind::Function || sk == SymbolKind::Method || sk == SymbolKind::Variable || sk == SymbolKind::Parameter || sk == SymbolKind::Macro || sk == SymbolKind::Concept || sk == SymbolKind::Module || sk == SymbolKind::Operator || sk == SymbolKind::MacroParameter || sk == SymbolKind::Label || sk == SymbolKind::Attribute; }; auto matches_query = [&](llvm::StringRef name) { if(query_lower.empty()) return true; return llvm::StringRef(name).lower().find(query_lower) != std::string::npos; }; std::vector results; llvm::DenseSet seen; for(auto& [hash, symbol]: workspace.project_index.symbols) { if(results.size() >= max_results) break; if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) continue; if(!matches_query(symbol.name)) continue; auto def_loc = find_definition_location(hash); if(!def_loc) continue; protocol::SymbolInformation info; info.name = symbol.name; info.kind = to_lsp_symbol_kind(symbol.kind); info.location = std::move(*def_loc); results.push_back(std::move(info)); seen.insert(hash); } for(auto& [_, sess]: sessions) { if(results.size() >= max_results) break; if(!sess.file_index) continue; for(auto& [hash, symbol]: sess.file_index->symbols) { if(results.size() >= max_results) break; if(seen.contains(hash)) continue; if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) continue; if(!matches_query(symbol.name)) continue; auto def_loc = find_definition_location(hash); if(!def_loc) continue; protocol::SymbolInformation info; info.name = symbol.name; info.kind = to_lsp_symbol_kind(symbol.kind); info.location = std::move(*def_loc); results.push_back(std::move(info)); seen.insert(hash); } } return results; } protocol::SymbolKind Indexer::to_lsp_symbol_kind(SymbolKind kind) { switch(kind) { case SymbolKind::Namespace: return protocol::SymbolKind::Namespace; case SymbolKind::Class: return protocol::SymbolKind::Class; case SymbolKind::Struct: return protocol::SymbolKind::Struct; case SymbolKind::Union: return protocol::SymbolKind::Class; case SymbolKind::Enum: return protocol::SymbolKind::Enum; case SymbolKind::Type: return protocol::SymbolKind::TypeParameter; case SymbolKind::Field: return protocol::SymbolKind::Field; case SymbolKind::EnumMember: return protocol::SymbolKind::EnumMember; case SymbolKind::Function: return protocol::SymbolKind::Function; case SymbolKind::Method: return protocol::SymbolKind::Method; case SymbolKind::Variable: return protocol::SymbolKind::Variable; case SymbolKind::Parameter: return protocol::SymbolKind::Variable; case SymbolKind::Macro: return protocol::SymbolKind::Function; case SymbolKind::Concept: return protocol::SymbolKind::Interface; case SymbolKind::Module: return protocol::SymbolKind::Module; case SymbolKind::Operator: return protocol::SymbolKind::Operator; default: return protocol::SymbolKind::Variable; } } protocol::CallHierarchyItem Indexer::build_call_hierarchy_item(const SymbolInfo& info) { protocol::CallHierarchyItem item; item.name = info.name; item.kind = to_lsp_symbol_kind(info.kind); item.uri = info.uri; item.range = info.range; item.selection_range = info.range; item.data = protocol::LSPAny(static_cast(info.hash)); return item; } protocol::TypeHierarchyItem Indexer::build_type_hierarchy_item(const SymbolInfo& info) { protocol::TypeHierarchyItem item; item.name = info.name; item.kind = to_lsp_symbol_kind(info.kind); item.uri = info.uri; item.range = info.range; item.selection_range = info.range; item.data = protocol::LSPAny(static_cast(info.hash)); return item; } void Indexer::enqueue(std::uint32_t server_path_id) { index_queue.push_back(server_path_id); } void Indexer::pause_indexing() { ++pause_depth; if(pause_depth == 1) { resume_event.reset(); LOG_DEBUG("Background indexing paused"); } } void Indexer::resume_indexing() { if(pause_depth > 0) --pause_depth; if(pause_depth == 0) { resume_event.set(); LOG_DEBUG("Background indexing resumed"); } } kota::task<> Indexer::stop() { bg_tasks.cancel(); co_await bg_tasks.join(); } void Indexer::schedule() { if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled) return; indexing_scheduled = true; if(!index_idle_timer) { index_idle_timer = std::make_shared(kota::timer::create(loop)); } index_idle_timer->start(std::chrono::milliseconds(*workspace.config.project.idle_timeout_ms)); if(!bg_tasks.spawn(run_background_indexing())) { indexing_scheduled = false; LOG_WARN("Failed to spawn background indexing task (task group stopped)"); } } kota::task<> Indexer::index_one(std::uint32_t server_path_id) { auto file_path = std::string(workspace.path_pool.resolve(server_path_id)); if(sessions.contains(server_path_id)) co_return; if(!need_update(file_path)) co_return; // For module interface units, compile their PCM (and transitive deps) // first so the stateless worker has the artifacts it needs. if(workspace.compile_graph && workspace.path_to_module.contains(server_path_id)) { co_await workspace.compile_graph->compile(server_path_id); } worker::BuildParams params; params.kind = worker::BuildKind::Index; params.file = file_path; if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr)) co_return; workspace.fill_pcm_deps(params.pcms); LOG_INFO("Background indexing: {}", file_path); auto result = co_await pool.send_stateless(params); if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) { LOG_INFO("Background indexing got TUIndex for {}: {} bytes", file_path, result.value().tu_index_data.size()); merge(result.value().tu_index_data.data(), result.value().tu_index_data.size()); } else if(result.has_value() && !result.value().success) { LOG_WARN("Background index failed for {}: {}", file_path, result.value().error); } else if(result.has_value() && result.value().tu_index_data.empty()) { LOG_WARN("Background index returned empty TUIndex for {}", file_path); } else { LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message); } } kota::task<> Indexer::monitor_resources() { while(true) { co_await kota::sleep(std::chrono::milliseconds(3000)); auto mem = kota::sys::memory(); if(mem.total == 0) continue; auto effective_total = (mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total; auto ratio = static_cast(mem.available) / static_cast(effective_total); if(ratio < 0.15 && max_concurrent > 1) { --max_concurrent; LOG_INFO("Index concurrency -> {} (memory pressure: {:.0f}% available)", max_concurrent, ratio * 100); } else if(ratio > 0.30 && max_concurrent < baseline_concurrent) { ++max_concurrent; LOG_DEBUG("Index concurrency -> {} (memory OK: {:.0f}% available)", max_concurrent, ratio * 100); } } } kota::task<> Indexer::run_background_indexing() { if(index_idle_timer) { co_await index_idle_timer->wait(); } indexing_scheduled = false; if(index_queue_pos >= index_queue.size()) { LOG_DEBUG("Background indexing: queue exhausted"); co_return; } indexing_active = true; kota::cancellation_source monitor_cancel; bg_tasks.spawn(kota::with_token(monitor_resources(), monitor_cancel.token())); std::stable_partition( index_queue.begin() + index_queue_pos, index_queue.end(), [this](std::uint32_t id) { return workspace.path_to_module.contains(id); }); auto total = index_queue.size() - index_queue_pos; std::size_t dispatched = 0; std::size_t completed = 0; std::optional> progress; if(peer) { progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex"))); auto create_result = co_await progress->create(); if(!create_result.has_error()) { progress->begin("Indexing", std::format("0/{} files", total), 0); } else { progress.reset(); } } kota::task_group<> workers(loop); std::size_t in_flight = 0; kota::event slot_available; while(index_queue_pos < index_queue.size()) { if(pause_depth > 0) co_await resume_event.wait(); auto server_path_id = index_queue[index_queue_pos++]; auto file_path = std::string(workspace.path_pool.resolve(server_path_id)); if(sessions.contains(server_path_id) || !need_update(file_path)) { ++completed; continue; } while(in_flight >= max_concurrent) { slot_available.reset(); co_await slot_available.wait(); } ++in_flight; ++dispatched; workers.spawn([&, server_path_id]() -> kota::task<> { co_await index_one(server_path_id); --in_flight; ++completed; if(progress) { auto pct = total > 0 ? static_cast(completed * 100 / total) : 100; progress->report(std::format("{}/{} files", completed, total), pct); } slot_available.set(); }()); } co_await workers.join(); if(progress) { progress->end(std::format("Indexed {} files", dispatched)); } monitor_cancel.cancel(); indexing_active = false; LOG_INFO("Background indexing complete: {} files dispatched", dispatched); save(workspace.config.project.index_dir); } } // namespace clice