Files
whetstone_DSL/editor/src/ProjectState.h
2026-02-11 20:27:47 +00:00

419 lines
15 KiB
C++

#pragma once
// Step 258: Project Model and Workspace Indexing
//
// Tracks workspace files, detects languages, and provides a project-level
// index of file paths (not content) for fast lookup. Works with
// HeadlessEditorState to manage multiple open buffers.
#include <string>
#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 "ast/Expression.h"
#include "ast/Parameter.h"
#include "CompactAST.h" // getNodeName
namespace fs = std::filesystem;
// -----------------------------------------------------------------------
// Language detection from file extension
// -----------------------------------------------------------------------
inline std::string detectLanguage(const std::string& filePath) {
std::string ext = fs::path(filePath).extension().string();
if (ext == ".py") return "python";
if (ext == ".cpp" || ext == ".cc" ||
ext == ".cxx" || ext == ".h" ||
ext == ".hpp" || ext == ".hxx") return "cpp";
if (ext == ".rs") return "rust";
if (ext == ".go") return "go";
if (ext == ".java") return "java";
if (ext == ".js" || ext == ".mjs") return "javascript";
if (ext == ".ts" || ext == ".tsx") return "typescript";
if (ext == ".el" || ext == ".elisp") return "elisp";
if (ext == ".org") return "org";
return "";
}
// -----------------------------------------------------------------------
// WorkspaceIndex — file path index (not content) for fast lookup
// -----------------------------------------------------------------------
struct IndexedFile {
std::string path; // absolute path
std::string relativePath; // relative to workspace root
std::string language; // detected from extension
bool isDir = false;
};
class WorkspaceIndex {
public:
void scan(const std::string& workspaceRoot) {
files_.clear();
root_ = workspaceRoot;
if (root_.empty() || !fs::exists(root_)) return;
FileTree tree;
FileNode rootNode = tree.build(root_);
collectFiles(rootNode);
}
const std::vector<IndexedFile>& files() const { return files_; }
// Find files matching a glob-like pattern (extension filter)
std::vector<IndexedFile> findByExtension(
const std::string& ext) const {
std::vector<IndexedFile> result;
for (const auto& f : files_) {
if (!f.isDir &&
fs::path(f.path).extension().string() == ext)
result.push_back(f);
}
return result;
}
// Find files by language
std::vector<IndexedFile> findByLanguage(
const std::string& lang) const {
std::vector<IndexedFile> result;
for (const auto& f : files_) {
if (!f.isDir && f.language == lang)
result.push_back(f);
}
return result;
}
// Lookup by relative path
const IndexedFile* findByRelativePath(
const std::string& relPath) const {
auto it = byRelPath_.find(relPath);
if (it != byRelPath_.end()) return &files_[it->second];
return nullptr;
}
int fileCount() const {
int count = 0;
for (const auto& f : files_)
if (!f.isDir) ++count;
return count;
}
int dirCount() const {
int count = 0;
for (const auto& f : files_)
if (f.isDir) ++count;
return count;
}
const std::string& root() const { return root_; }
private:
std::string root_;
std::vector<IndexedFile> files_;
std::map<std::string, size_t> byRelPath_;
void collectFiles(const FileNode& node) {
if (node.path == root_) {
for (const auto& child : node.children)
collectFiles(child);
return;
}
IndexedFile entry;
entry.path = node.path;
entry.isDir = node.isDir;
std::error_code ec;
entry.relativePath = fs::relative(
fs::path(node.path), fs::path(root_), ec).string();
if (!node.isDir)
entry.language = detectLanguage(node.path);
size_t idx = files_.size();
files_.push_back(entry);
byRelPath_[entry.relativePath] = idx;
if (node.isDir) {
for (const auto& child : node.children)
collectFiles(child);
}
}
};
// -----------------------------------------------------------------------
// 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;
}
// -----------------------------------------------------------------------
// 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
// -----------------------------------------------------------------------
struct ProjectState {
WorkspaceIndex index;
ImportGraph importGraph;
void scanWorkspace(const std::string& workspaceRoot) {
index.scan(workspaceRoot);
}
};