Step 259: cross-file symbol resolution and import graph
getInScopeSymbols gains crossFile parameter to include exported symbols from all open buffers. Import graph tracks file dependencies, auto-maintained on openFile/closeFile. Text-based fallback for parsers without Import AST nodes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,8 @@ struct AgentPermissionPolicy {
|
||||
method == "batchQuery" ||
|
||||
method == "listBuffers" ||
|
||||
method == "setActiveBuffer" ||
|
||||
method == "indexWorkspace") {
|
||||
method == "indexWorkspace" ||
|
||||
method == "getImportGraph") {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -396,6 +396,7 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing nodeId parameter");
|
||||
bool detailed = params.value("detailed", false);
|
||||
bool crossFile = params.value("crossFile", false);
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto symbols = ctx.getInScopeSymbols(nodeId);
|
||||
@@ -412,9 +413,31 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
{"nodeId", s.nodeId}});
|
||||
}
|
||||
}
|
||||
// Cross-file: add exported symbols from other open buffers
|
||||
if (crossFile) {
|
||||
std::string activePath = state.activeBuffer
|
||||
? state.activeBuffer->path : "";
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
if (path == activePath) continue;
|
||||
Module* otherAST = buf->sync.getAST();
|
||||
if (!otherAST) continue;
|
||||
auto exports = collectExportedSymbols(otherAST, path);
|
||||
for (const auto& ex : exports) {
|
||||
json entry = {{"name", ex.name}, {"kind", ex.kind},
|
||||
{"nodeId", ex.nodeId},
|
||||
{"file", ex.filePath}};
|
||||
if (detailed) {
|
||||
ASTNode* node = findNodeById(otherAST, ex.nodeId);
|
||||
if (node) entry["node"] = toJson(node);
|
||||
}
|
||||
arr.push_back(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
json result = {{"symbols", arr},
|
||||
{"count", (int)arr.size()},
|
||||
{"mode", detailed ? "detailed" : "symbols"}};
|
||||
if (crossFile) result["crossFile"] = true;
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
@@ -961,6 +984,13 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
auto* buf = state.openBuffer(path, content, language);
|
||||
if (!buf)
|
||||
return headlessRpcError(id, -32041, "Failed to open buffer");
|
||||
// Update import graph from the new buffer's AST + source
|
||||
Module* bufAST = buf->sync.getAST();
|
||||
if (bufAST)
|
||||
state.project.importGraph.updateFromAST(path, bufAST);
|
||||
// Fallback: scan source text for imports not captured by parser
|
||||
if (!content.empty())
|
||||
state.project.importGraph.updateFromSource(path, content);
|
||||
return headlessRpcResult(id, {
|
||||
{"path", path}, {"language", language},
|
||||
{"bufferCount", (int)state.bufferStates.size()}
|
||||
@@ -979,6 +1009,7 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
auto it = state.bufferStates.find(path);
|
||||
if (it == state.bufferStates.end())
|
||||
return headlessRpcError(id, -32002, "Buffer not found: " + path);
|
||||
state.project.importGraph.clearFile(path);
|
||||
state.closeBuffer(path);
|
||||
return headlessRpcResult(id, {
|
||||
{"closed", path},
|
||||
@@ -1040,6 +1071,40 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
});
|
||||
}
|
||||
|
||||
// --- getImportGraph ---
|
||||
if (method == "getImportGraph") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string filePath = params.value("file", "");
|
||||
const auto& graph = state.project.importGraph;
|
||||
if (!filePath.empty()) {
|
||||
// Imports for a specific file
|
||||
auto imports = graph.importsOf(filePath);
|
||||
json importArr = json::array();
|
||||
for (const auto& m : imports) importArr.push_back(m);
|
||||
auto importers = graph.importedBy(
|
||||
fs::path(filePath).stem().string());
|
||||
json importerArr = json::array();
|
||||
for (const auto& f : importers) importerArr.push_back(f);
|
||||
return headlessRpcResult(id, {
|
||||
{"file", filePath},
|
||||
{"imports", importArr},
|
||||
{"importedBy", importerArr}
|
||||
});
|
||||
}
|
||||
// Full import graph
|
||||
json edges = json::object();
|
||||
for (const auto& [file, imports] : graph.edges()) {
|
||||
json importArr = json::array();
|
||||
for (const auto& m : imports) importArr.push_back(m);
|
||||
edges[file] = importArr;
|
||||
}
|
||||
return headlessRpcResult(id, {
|
||||
{"edges", edges},
|
||||
{"fileCount", (int)graph.edges().size()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- batchQuery ---
|
||||
if (method == "batchQuery") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
|
||||
@@ -9,8 +9,15 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include "FileTree.h"
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Import.h"
|
||||
#include "CompactAST.h" // getNodeName
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -136,11 +143,134 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Step 259: ImportGraph — tracks which files import which
|
||||
// -----------------------------------------------------------------------
|
||||
class ImportGraph {
|
||||
public:
|
||||
// Record that `fromFile` imports `moduleName`
|
||||
void addEdge(const std::string& fromFile,
|
||||
const std::string& moduleName) {
|
||||
edges_[fromFile].insert(moduleName);
|
||||
}
|
||||
|
||||
// Clear edges for a file (call before re-scanning)
|
||||
void clearFile(const std::string& filePath) {
|
||||
edges_.erase(filePath);
|
||||
}
|
||||
|
||||
// Get modules imported by a file
|
||||
std::set<std::string> importsOf(const std::string& filePath) const {
|
||||
auto it = edges_.find(filePath);
|
||||
if (it != edges_.end()) return it->second;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get files that import a given module name
|
||||
std::vector<std::string> importedBy(
|
||||
const std::string& moduleName) const {
|
||||
std::vector<std::string> result;
|
||||
for (const auto& [file, imports] : edges_) {
|
||||
if (imports.count(moduleName)) result.push_back(file);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Rebuild import edges from an AST's Import nodes
|
||||
void updateFromAST(const std::string& filePath, Module* ast) {
|
||||
clearFile(filePath);
|
||||
if (!ast) return;
|
||||
for (auto* child : ast->allChildren()) {
|
||||
if (child->conceptType == "Import") {
|
||||
auto* imp = static_cast<const Import*>(child);
|
||||
if (!imp->moduleName.empty())
|
||||
addEdge(filePath, imp->moduleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild import edges from source text (fallback for parsers
|
||||
// that don't generate Import AST nodes, e.g. Python)
|
||||
void updateFromSource(const std::string& filePath,
|
||||
const std::string& source) {
|
||||
// Simple line-by-line scan for import statements
|
||||
std::istringstream iss(source);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
// Python: "import foo" or "from foo import bar"
|
||||
if (line.substr(0, 7) == "import ") {
|
||||
std::string mod = line.substr(7);
|
||||
// Trim whitespace
|
||||
while (!mod.empty() && mod.back() == ' ')
|
||||
mod.pop_back();
|
||||
// Handle "import foo, bar"
|
||||
size_t comma = mod.find(',');
|
||||
if (comma != std::string::npos)
|
||||
mod = mod.substr(0, comma);
|
||||
if (!mod.empty()) addEdge(filePath, mod);
|
||||
} else if (line.substr(0, 5) == "from ") {
|
||||
size_t space = line.find(' ', 5);
|
||||
if (space != std::string::npos) {
|
||||
std::string mod = line.substr(5, space - 5);
|
||||
if (!mod.empty()) addEdge(filePath, mod);
|
||||
}
|
||||
}
|
||||
// JS/TS: "import ... from 'foo'" handled by AST
|
||||
// C++: "#include" handled by AST
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::string, std::set<std::string>>& edges() const {
|
||||
return edges_;
|
||||
}
|
||||
|
||||
private:
|
||||
// filePath -> set of imported module names
|
||||
std::map<std::string, std::set<std::string>> edges_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ExportedSymbol — a symbol visible across files
|
||||
// -----------------------------------------------------------------------
|
||||
struct ExportedSymbol {
|
||||
std::string name;
|
||||
std::string kind; // "function", "variable", "class"
|
||||
std::string nodeId;
|
||||
std::string filePath; // source file
|
||||
};
|
||||
|
||||
// Collect exported (module-level) symbols from an AST
|
||||
inline std::vector<ExportedSymbol> collectExportedSymbols(
|
||||
Module* ast, const std::string& filePath) {
|
||||
std::vector<ExportedSymbol> symbols;
|
||||
if (!ast) return symbols;
|
||||
for (auto* child : ast->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
symbols.push_back({fn->name, "function", fn->id, filePath});
|
||||
}
|
||||
}
|
||||
for (auto* child : ast->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(child);
|
||||
symbols.push_back({v->name, "variable", v->id, filePath});
|
||||
}
|
||||
}
|
||||
for (auto* child : ast->getChildren("classes")) {
|
||||
if (child->conceptType == "Class") {
|
||||
symbols.push_back({getNodeName(child), "class",
|
||||
child->id, filePath});
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ProjectState — aggregates workspace index and open buffer tracking
|
||||
// -----------------------------------------------------------------------
|
||||
struct ProjectState {
|
||||
WorkspaceIndex index;
|
||||
ImportGraph importGraph;
|
||||
|
||||
void scanWorkspace(const std::string& workspaceRoot) {
|
||||
index.scan(workspaceRoot);
|
||||
|
||||
Reference in New Issue
Block a user