Step 261: project-wide search and refactor (searchProject, renameSymbol)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,8 @@ struct AgentPermissionPolicy {
|
||||
method == "setActiveBuffer" ||
|
||||
method == "indexWorkspace" ||
|
||||
method == "getImportGraph" ||
|
||||
method == "getProjectDiagnostics") {
|
||||
method == "getProjectDiagnostics" ||
|
||||
method == "searchProject") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -68,7 +69,8 @@ struct AgentPermissionPolicy {
|
||||
method == "fileCreate" ||
|
||||
method == "applyQuickFix" ||
|
||||
method == "openFile" ||
|
||||
method == "closeFile") {
|
||||
method == "closeFile" ||
|
||||
method == "renameSymbol") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -1105,6 +1105,176 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
});
|
||||
}
|
||||
|
||||
// --- searchProject ---
|
||||
if (method == "searchProject") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string name = params.value("name", "");
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
|
||||
// If nodeId given, resolve name from the active buffer
|
||||
if (!name.empty()) {
|
||||
// use name as-is
|
||||
} else if (!nodeId.empty()) {
|
||||
// Search all buffers for a node with this ID
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
Module* ast = buf->sync.getAST();
|
||||
if (!ast) continue;
|
||||
ASTNode* node = findNodeById(ast, nodeId);
|
||||
if (node) {
|
||||
name = getNodeName(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name.empty())
|
||||
return headlessRpcError(id, -32002,
|
||||
"Node not found: " + nodeId);
|
||||
} else {
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing name or nodeId parameter");
|
||||
}
|
||||
|
||||
json results = json::array();
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
Module* ast = buf->sync.getAST();
|
||||
if (!ast) continue;
|
||||
auto refs = collectSymbolReferences(ast, name, path);
|
||||
for (const auto& ref : refs) {
|
||||
results.push_back({
|
||||
{"file", ref.file}, {"line", ref.line},
|
||||
{"col", ref.col}, {"nodeId", ref.nodeId},
|
||||
{"kind", ref.kind}, {"context", ref.context}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::set<std::string> searchFiles;
|
||||
for (const auto& r : results)
|
||||
searchFiles.insert(r.value("file", ""));
|
||||
|
||||
return headlessRpcResult(id, {
|
||||
{"name", name},
|
||||
{"references", results},
|
||||
{"count", (int)results.size()},
|
||||
{"fileCount", (int)searchFiles.size()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- renameSymbol ---
|
||||
if (method == "renameSymbol") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string oldName = params.value("oldName", "");
|
||||
std::string newName = params.value("newName", "");
|
||||
bool preview = params.value("preview", false);
|
||||
|
||||
if (oldName.empty() || newName.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing oldName or newName parameter");
|
||||
|
||||
// Collect changes across all open buffers
|
||||
std::vector<RenameChange> allChanges;
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
Module* ast = buf->sync.getAST();
|
||||
if (!ast) continue;
|
||||
auto changes = buildRenameChanges(
|
||||
ast, oldName, newName, path);
|
||||
allChanges.insert(allChanges.end(),
|
||||
changes.begin(), changes.end());
|
||||
}
|
||||
|
||||
if (allChanges.empty())
|
||||
return headlessRpcError(id, -32002,
|
||||
"No references found for '" + oldName + "'");
|
||||
|
||||
// Build preview JSON
|
||||
json changesJson = json::array();
|
||||
std::set<std::string> affectedFiles;
|
||||
for (const auto& c : allChanges) {
|
||||
changesJson.push_back({
|
||||
{"file", c.file}, {"nodeId", c.nodeId},
|
||||
{"property", c.property},
|
||||
{"oldValue", c.oldValue},
|
||||
{"newValue", c.newValue},
|
||||
{"kind", c.kind}
|
||||
});
|
||||
affectedFiles.insert(c.file);
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
return headlessRpcResult(id, {
|
||||
{"preview", true},
|
||||
{"changes", changesJson},
|
||||
{"changeCount", (int)allChanges.size()},
|
||||
{"fileCount", (int)affectedFiles.size()}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply changes: use setProperty on each node
|
||||
int applied = 0;
|
||||
std::vector<std::string> errors;
|
||||
for (const auto& c : allChanges) {
|
||||
auto it = state.bufferStates.find(c.file);
|
||||
if (it == state.bufferStates.end()) continue;
|
||||
Module* ast = it->second->sync.getAST();
|
||||
if (!ast) continue;
|
||||
ASTNode* node = findNodeById(ast, c.nodeId);
|
||||
if (!node) {
|
||||
errors.push_back("Node " + c.nodeId +
|
||||
" not found in " + c.file);
|
||||
continue;
|
||||
}
|
||||
// Apply the property change directly
|
||||
if (node->conceptType == "Function" &&
|
||||
c.property == "name") {
|
||||
static_cast<Function*>(node)->name = c.newValue;
|
||||
++applied;
|
||||
} else if (node->conceptType == "FunctionCall" &&
|
||||
c.property == "functionName") {
|
||||
static_cast<FunctionCall*>(node)->functionName =
|
||||
c.newValue;
|
||||
++applied;
|
||||
} else if (node->conceptType == "Variable" &&
|
||||
c.property == "name") {
|
||||
static_cast<Variable*>(node)->name = c.newValue;
|
||||
++applied;
|
||||
} else if (node->conceptType == "VariableReference" &&
|
||||
c.property == "variableName") {
|
||||
static_cast<VariableReference*>(node)->variableName =
|
||||
c.newValue;
|
||||
++applied;
|
||||
} else if (node->conceptType == "Parameter" &&
|
||||
c.property == "name") {
|
||||
static_cast<Parameter*>(node)->name = c.newValue;
|
||||
++applied;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark affected buffers as modified
|
||||
for (const auto& f : affectedFiles) {
|
||||
auto it = state.bufferStates.find(f);
|
||||
if (it != state.bufferStates.end())
|
||||
it->second->modified = true;
|
||||
}
|
||||
|
||||
json result = {
|
||||
{"applied", applied},
|
||||
{"changes", changesJson},
|
||||
{"changeCount", (int)allChanges.size()},
|
||||
{"fileCount", (int)affectedFiles.size()}
|
||||
};
|
||||
if (!errors.empty()) {
|
||||
json errArr = json::array();
|
||||
for (const auto& e : errors) errArr.push_back(e);
|
||||
result["errors"] = errArr;
|
||||
}
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
// --- getProjectDiagnostics ---
|
||||
if (method == "getProjectDiagnostics") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
|
||||
@@ -838,6 +838,45 @@ private:
|
||||
toolHandlers_["whetstone_index_workspace"] = [this](const json& args) {
|
||||
return callWhetstone("indexWorkspace", args);
|
||||
};
|
||||
|
||||
// whetstone_search_project
|
||||
tools_.push_back({"whetstone_search_project",
|
||||
"Find all references to a symbol across all open files. "
|
||||
"Search by name or nodeId. Returns file, line, col, nodeId, "
|
||||
"kind (definition/call/reference/parameter), and context.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"name", {{"type", "string"},
|
||||
{"description",
|
||||
"Symbol name to search for"}}},
|
||||
{"nodeId", {{"type", "string"},
|
||||
{"description",
|
||||
"Node ID to resolve name from (alternative to name)"}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_search_project"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("searchProject", args);
|
||||
};
|
||||
|
||||
// whetstone_rename_symbol
|
||||
tools_.push_back({"whetstone_rename_symbol",
|
||||
"Rename a symbol across all open files. Updates function "
|
||||
"definitions, calls, variable declarations, references, and "
|
||||
"parameters. Set preview=true to see changes without applying.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"oldName", {{"type", "string"},
|
||||
{"description", "Current symbol name"}}},
|
||||
{"newName", {{"type", "string"},
|
||||
{"description", "New symbol name"}}},
|
||||
{"preview", {{"type", "boolean"},
|
||||
{"description",
|
||||
"Preview changes without applying (default false)"}}}
|
||||
}}, {"required", {"oldName", "newName"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_rename_symbol"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("renameSymbol", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Import.h"
|
||||
#include "ast/Expression.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "CompactAST.h" // getNodeName
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -265,6 +267,144 @@ inline std::vector<ExportedSymbol> collectExportedSymbols(
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Step 261: SymbolReference — a single reference to a symbol
|
||||
// -----------------------------------------------------------------------
|
||||
struct SymbolReference {
|
||||
std::string file; // source file path
|
||||
int line = 0; // 1-based (from span if available)
|
||||
int col = 0; // 1-based
|
||||
std::string nodeId; // AST node ID
|
||||
std::string kind; // "definition", "call", "reference", "parameter"
|
||||
std::string context; // brief context (node type + name)
|
||||
};
|
||||
|
||||
// Recursive helper: collect references to a name in a subtree
|
||||
inline void collectSymbolRefsRecursive(
|
||||
ASTNode* node, const std::string& name,
|
||||
const std::string& filePath,
|
||||
std::vector<SymbolReference>& refs) {
|
||||
if (!node) return;
|
||||
|
||||
SymbolReference ref;
|
||||
ref.file = filePath;
|
||||
ref.nodeId = node->id;
|
||||
ref.line = node->hasSpan() ? node->spanStartLine : 0;
|
||||
ref.col = node->hasSpan() ? node->spanStartCol : 0;
|
||||
|
||||
bool matched = false;
|
||||
if (node->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(node);
|
||||
if (fn->name == name) {
|
||||
ref.kind = "definition";
|
||||
ref.context = "function " + fn->name;
|
||||
matched = true;
|
||||
}
|
||||
} else if (node->conceptType == "FunctionCall") {
|
||||
auto* fc = static_cast<const FunctionCall*>(node);
|
||||
if (fc->functionName == name) {
|
||||
ref.kind = "call";
|
||||
ref.context = "call " + fc->functionName;
|
||||
matched = true;
|
||||
}
|
||||
} else if (node->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(node);
|
||||
if (v->name == name) {
|
||||
ref.kind = "definition";
|
||||
ref.context = "variable " + v->name;
|
||||
matched = true;
|
||||
}
|
||||
} else if (node->conceptType == "VariableReference") {
|
||||
auto* vr = static_cast<const VariableReference*>(node);
|
||||
if (vr->variableName == name) {
|
||||
ref.kind = "reference";
|
||||
ref.context = "ref " + vr->variableName;
|
||||
matched = true;
|
||||
}
|
||||
} else if (node->conceptType == "Parameter") {
|
||||
auto* p = static_cast<const Parameter*>(node);
|
||||
if (p->name == name) {
|
||||
ref.kind = "parameter";
|
||||
ref.context = "param " + p->name;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) refs.push_back(std::move(ref));
|
||||
|
||||
for (auto* child : node->allChildren())
|
||||
collectSymbolRefsRecursive(child, name, filePath, refs);
|
||||
}
|
||||
|
||||
// Collect all references to a symbol name within a single AST
|
||||
inline std::vector<SymbolReference> collectSymbolReferences(
|
||||
Module* ast, const std::string& name,
|
||||
const std::string& filePath) {
|
||||
std::vector<SymbolReference> refs;
|
||||
if (!ast || name.empty()) return refs;
|
||||
collectSymbolRefsRecursive(ast, name, filePath, refs);
|
||||
return refs;
|
||||
}
|
||||
|
||||
// Build rename mutations for all references to a symbol in a single AST
|
||||
struct RenameChange {
|
||||
std::string file;
|
||||
std::string nodeId;
|
||||
std::string property; // which property to set
|
||||
std::string oldValue;
|
||||
std::string newValue;
|
||||
std::string kind; // definition/call/reference/parameter
|
||||
};
|
||||
|
||||
// Recursive helper: collect rename changes in a subtree
|
||||
inline void buildRenameChangesRecursive(
|
||||
ASTNode* node, const std::string& oldName,
|
||||
const std::string& newName, const std::string& filePath,
|
||||
std::vector<RenameChange>& changes) {
|
||||
if (!node) return;
|
||||
|
||||
if (node->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(node);
|
||||
if (fn->name == oldName)
|
||||
changes.push_back({filePath, node->id, "name",
|
||||
oldName, newName, "definition"});
|
||||
} else if (node->conceptType == "FunctionCall") {
|
||||
auto* fc = static_cast<const FunctionCall*>(node);
|
||||
if (fc->functionName == oldName)
|
||||
changes.push_back({filePath, node->id, "functionName",
|
||||
oldName, newName, "call"});
|
||||
} else if (node->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(node);
|
||||
if (v->name == oldName)
|
||||
changes.push_back({filePath, node->id, "name",
|
||||
oldName, newName, "definition"});
|
||||
} else if (node->conceptType == "VariableReference") {
|
||||
auto* vr = static_cast<const VariableReference*>(node);
|
||||
if (vr->variableName == oldName)
|
||||
changes.push_back({filePath, node->id, "variableName",
|
||||
oldName, newName, "reference"});
|
||||
} else if (node->conceptType == "Parameter") {
|
||||
auto* p = static_cast<const Parameter*>(node);
|
||||
if (p->name == oldName)
|
||||
changes.push_back({filePath, node->id, "name",
|
||||
oldName, newName, "parameter"});
|
||||
}
|
||||
|
||||
for (auto* child : node->allChildren())
|
||||
buildRenameChangesRecursive(child, oldName, newName,
|
||||
filePath, changes);
|
||||
}
|
||||
|
||||
inline std::vector<RenameChange> buildRenameChanges(
|
||||
Module* ast, const std::string& oldName,
|
||||
const std::string& newName, const std::string& filePath) {
|
||||
std::vector<RenameChange> changes;
|
||||
if (!ast || oldName.empty() || newName.empty()) return changes;
|
||||
buildRenameChangesRecursive(ast, oldName, newName,
|
||||
filePath, changes);
|
||||
return changes;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ProjectState — aggregates workspace index and open buffer tracking
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user