Step 85: filesystem tree explorer

This commit is contained in:
Bill
2026-02-09 09:29:01 -07:00
parent c195128034
commit 6da8b36347
5 changed files with 198 additions and 6 deletions

87
editor/src/FileTree.h Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
// Step 85: Filesystem tree with basic .gitignore support
#include <string>
#include <vector>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <algorithm>
struct FileNode {
std::string name;
std::string path;
bool isDir = false;
std::vector<FileNode> children;
};
class FileTree {
public:
FileNode build(const std::string& rootPath) {
root_ = std::filesystem::path(rootPath);
ignorePatterns_ = loadGitIgnore(root_ / ".gitignore");
return buildNode(root_);
}
private:
std::filesystem::path root_;
std::vector<std::string> ignorePatterns_;
FileNode buildNode(const std::filesystem::path& p) {
FileNode node;
node.name = p.filename().string();
node.path = p.string();
node.isDir = std::filesystem::is_directory(p);
if (node.isDir) {
for (auto& entry : std::filesystem::directory_iterator(p)) {
const auto childPath = entry.path();
if (shouldIgnore(childPath)) continue;
node.children.push_back(buildNode(childPath));
}
std::sort(node.children.begin(), node.children.end(),
[](const FileNode& a, const FileNode& b) {
if (a.isDir != b.isDir) return a.isDir > b.isDir;
return a.name < b.name;
});
}
return node;
}
std::vector<std::string> loadGitIgnore(const std::filesystem::path& p) {
std::vector<std::string> patterns;
std::ifstream f(p.string());
if (!f.is_open()) return patterns;
std::string line;
while (std::getline(f, line)) {
if (line.empty()) continue;
if (line[0] == '#') continue;
patterns.push_back(line);
}
return patterns;
}
bool shouldIgnore(const std::filesystem::path& p) const {
std::string rel = std::filesystem::relative(p, root_).generic_string();
std::string name = p.filename().string();
bool isDir = std::filesystem::is_directory(p);
for (const auto& pat : ignorePatterns_) {
if (pat.empty()) continue;
if (pat.back() == '/' && isDir) {
std::string dirName = pat.substr(0, pat.size() - 1);
if (name == dirName) return true;
if (rel.rfind(dirName + "/", 0) == 0) return true;
} else if (pat.front() == '*') {
std::string suffix = pat.substr(1);
if (suffix.size() <= name.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0)
return true;
} else if (pat.front() == '/') {
std::string exact = pat.substr(1);
if (rel == exact) return true;
} else {
if (name == pat) return true;
}
}
return false;
}
};

View File

@@ -20,6 +20,7 @@
#include "CodeEditorWidget.h"
#include "EditorMode.h"
#include "FileDialog.h"
#include "FileTree.h"
#include "ast/Generator.h"
#include <cstdio>
@@ -29,6 +30,7 @@
#include <fstream>
#include <sstream>
#include <cstring>
#include <filesystem>
// ---------------------------------------------------------------------------
// Editor application state
@@ -66,6 +68,10 @@ struct EditorState {
CodeEditorWidget codeWidget;
bool showWhitespace = false;
bool showMinimap = false;
std::string workspaceRoot;
FileTree fileTree;
FileNode fileTreeRoot;
bool fileTreeDirty = true;
void init() {
std::string defaultContent =
@@ -91,6 +97,8 @@ struct EditorState {
editBuf = defaultContent;
highlightsDirty = true;
outputLog = "Whetstone Editor ready.\n";
workspaceRoot = std::filesystem::current_path().string();
fileTreeDirty = true;
}
void setLanguage(const std::string& lang) {
@@ -210,6 +218,12 @@ struct EditorState {
}
}
void refreshFileTree() {
if (!fileTreeDirty) return;
fileTreeRoot = fileTree.build(workspaceRoot);
fileTreeDirty = false;
}
void updateHighlights() {
if (!highlightsDirty) return;
highlights = SyntaxHighlighter::highlight(editBuf, language);
@@ -407,6 +421,26 @@ static void RenderHighlightedText(const std::string& text,
}
}
// ---------------------------------------------------------------------------
// File tree rendering
// ---------------------------------------------------------------------------
static void RenderFileTree(const FileNode& node, EditorState& state) {
if (!node.isDir) {
if (ImGui::Selectable(node.name.c_str())) {
state.doOpen(node.path);
}
return;
}
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (ImGui::TreeNodeEx(node.name.c_str(), flags)) {
for (const auto& child : node.children) {
RenderFileTree(child, state);
}
ImGui::TreePop();
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
@@ -557,6 +591,14 @@ int main(int, char**) {
state.doOpen(path);
}
}
if (ImGui::MenuItem("Open Folder...")) {
auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot});
if (!path.empty()) {
state.workspaceRoot = path;
state.fileTreeDirty = true;
lastDialogPath = path;
}
}
if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str()))
{
if (state.filePath == "(untitled)") {
@@ -639,12 +681,11 @@ int main(int, char**) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "FILES");
ImGui::Separator();
// Placeholder files
const char* files[] = {"Calculator.py", "ConditionalExample.py", "main.cpp", "test.el"};
for (auto f : files) {
if (ImGui::Selectable(f)) {
state.doOpen(f);
}
state.refreshFileTree();
if (state.fileTreeRoot.path.empty()) {
ImGui::TextDisabled("(no workspace)");
} else {
RenderFileTree(state.fileTreeRoot, state);
}
ImGui::PopFont();
ImGui::End();