Step 116: project save/load
This commit is contained in:
@@ -219,6 +219,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
|
||||
- [x] Step 113: **IMPLEMENTED** — Project-wide search panel with regex and glob filters (3/3 tests pass)
|
||||
- [x] Step 114: **IMPLEMENTED** — Go-to-line popup with :line:col parsing (4/4 tests pass)
|
||||
- [x] Step 115: **IMPLEMENTED** — Orchestrator wired for structured mutations; Emacs config path setting (3/3 tests pass)
|
||||
- [x] Step 116: **IMPLEMENTED** — Project save/load (.whetstone) with AST serialization (4/4 tests pass)
|
||||
|
||||
---
|
||||
|
||||
@@ -317,6 +318,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
**Step 113:** Compile and pass (3/3)
|
||||
**Step 114:** Compile and pass (4/4)
|
||||
**Step 115:** Compile and pass (3/3)
|
||||
**Step 116:** Compile and pass (4/4)
|
||||
|
||||
---
|
||||
|
||||
@@ -368,7 +370,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
|
||||
## What's Next
|
||||
|
||||
Sprint 4 in progress. Step 115 (wire orchestrator) done. Next: Step 116 (project save/load).
|
||||
Sprint 4 in progress. Step 116 (project save/load) done. Next: Step 117 (session persistence).
|
||||
|
||||
---
|
||||
|
||||
@@ -445,3 +447,4 @@ Sprint 4 in progress. Step 115 (wire orchestrator) done. Next: Step 116 (project
|
||||
| 2026-02-09 | Codex | Step 113: Project-wide search panel with regex and glob filters. 3/3 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 114: Go-to-line popup with :line:col parsing. 4/4 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 115: Orchestrator wired for structured mutations; Emacs config path setting. 3/3 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 116: Project save/load (.whetstone) with AST serialization. 4/4 tests pass. |
|
||||
|
||||
@@ -622,6 +622,10 @@ add_executable(step115_test tests/step115_test.cpp)
|
||||
target_include_directories(step115_test PRIVATE src)
|
||||
target_link_libraries(step115_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step116_test tests/step116_test.cpp)
|
||||
target_include_directories(step116_test PRIVATE src)
|
||||
target_link_libraries(step116_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
64
editor/src/ProjectManager.h
Normal file
64
editor/src/ProjectManager.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
// Step 116: Project save/load helpers
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Module.h"
|
||||
|
||||
struct ProjectBufferInfo {
|
||||
std::string path;
|
||||
std::string language;
|
||||
std::string mode;
|
||||
};
|
||||
|
||||
struct ProjectFile {
|
||||
std::string workspaceRoot;
|
||||
std::string activePath;
|
||||
std::vector<ProjectBufferInfo> buffers;
|
||||
std::unique_ptr<Module> ast;
|
||||
};
|
||||
|
||||
inline nlohmann::json projectToJson(const ProjectFile& project) {
|
||||
nlohmann::json j;
|
||||
j["workspaceRoot"] = project.workspaceRoot;
|
||||
j["activePath"] = project.activePath;
|
||||
j["buffers"] = nlohmann::json::array();
|
||||
for (const auto& b : project.buffers) {
|
||||
j["buffers"].push_back({
|
||||
{"path", b.path},
|
||||
{"language", b.language},
|
||||
{"mode", b.mode}
|
||||
});
|
||||
}
|
||||
if (project.ast) {
|
||||
j["ast"] = toJson(project.ast.get());
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
inline ProjectFile projectFromJson(const nlohmann::json& j) {
|
||||
ProjectFile project;
|
||||
project.workspaceRoot = j.value("workspaceRoot", "");
|
||||
project.activePath = j.value("activePath", "");
|
||||
if (j.contains("buffers") && j["buffers"].is_array()) {
|
||||
for (const auto& item : j["buffers"]) {
|
||||
ProjectBufferInfo info;
|
||||
info.path = item.value("path", "");
|
||||
info.language = item.value("language", "");
|
||||
info.mode = item.value("mode", "structured");
|
||||
if (!info.path.empty()) project.buffers.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
if (j.contains("ast")) {
|
||||
ASTNode* node = fromJson(j["ast"]);
|
||||
if (node && node->conceptType == "Module") {
|
||||
project.ast.reset(static_cast<Module*>(node));
|
||||
} else {
|
||||
deleteTree(node);
|
||||
}
|
||||
}
|
||||
return project;
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "ProjectSearch.h"
|
||||
#include "GoToLine.h"
|
||||
#include "Orchestrator.h"
|
||||
#include "ProjectManager.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
@@ -469,6 +470,93 @@ struct EditorState {
|
||||
out << j.dump(2);
|
||||
}
|
||||
|
||||
void closeAllBuffers() {
|
||||
auto open = buffers.getOpenBuffers();
|
||||
for (const auto& path : open) {
|
||||
buffers.closeBuffer(path);
|
||||
bufferStates.erase(path);
|
||||
if (path.rfind("(untitled", 0) != 0) watcher.unwatch(path);
|
||||
}
|
||||
activeBuffer = nullptr;
|
||||
}
|
||||
|
||||
bool saveProject(const std::string& path) {
|
||||
ProjectFile project;
|
||||
project.workspaceRoot = workspaceRoot;
|
||||
project.activePath = active() ? active()->path : "";
|
||||
for (const auto& bufPath : buffers.getOpenBuffers()) {
|
||||
auto it = bufferStates.find(bufPath);
|
||||
if (it == bufferStates.end()) continue;
|
||||
ProjectBufferInfo info;
|
||||
info.path = bufPath;
|
||||
info.language = it->second->language;
|
||||
info.mode = bufferModeToString(it->second->bufferMode);
|
||||
project.buffers.push_back(std::move(info));
|
||||
}
|
||||
if (isStructured()) {
|
||||
syncOrchestratorFromActive();
|
||||
if (orchestrator.getAST()) {
|
||||
project.ast = cloneModule(orchestrator.getAST());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
nlohmann::json j = projectToJson(project);
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
if (!out.is_open()) return false;
|
||||
out << j.dump(2);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool loadProject(const std::string& path) {
|
||||
try {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in.is_open()) return false;
|
||||
nlohmann::json j;
|
||||
in >> j;
|
||||
ProjectFile project = projectFromJson(j);
|
||||
closeAllBuffers();
|
||||
|
||||
if (!project.workspaceRoot.empty()) {
|
||||
workspaceRoot = project.workspaceRoot;
|
||||
fileTreeDirty = true;
|
||||
projectSearch.setRoot(workspaceRoot);
|
||||
}
|
||||
|
||||
std::string activePath = project.activePath;
|
||||
for (const auto& buf : project.buffers) {
|
||||
if (buf.path == activePath && project.ast) {
|
||||
std::string lang = buf.language.empty() ? project.ast->targetLanguage : buf.language;
|
||||
std::string generated = generateForLanguage(project.ast.get(), lang);
|
||||
createBuffer(buf.path, generated, lang, bufferModeFromString(buf.mode));
|
||||
if (active()) {
|
||||
active()->sync.setAST(std::move(project.ast));
|
||||
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
|
||||
active()->editBuf = active()->sync.getText();
|
||||
active()->editor.setContent(active()->editBuf, lang);
|
||||
active()->mode.setLanguage(lang);
|
||||
active()->highlightsDirty = true;
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = false;
|
||||
}
|
||||
} else {
|
||||
doOpen(buf.path, bufferModeFromString(buf.mode));
|
||||
}
|
||||
}
|
||||
|
||||
if (!activePath.empty() && buffers.hasBuffer(activePath)) {
|
||||
switchToBuffer(activePath);
|
||||
}
|
||||
orchestratorDirty = true;
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
outputLog = "Whetstone Editor ready.\n";
|
||||
workspaceRoot = std::filesystem::current_path().string();
|
||||
@@ -536,6 +624,28 @@ struct EditorState {
|
||||
registerCommand("file.save", "File: Save",
|
||||
keys.getBinding("file.save").toString(),
|
||||
[this]() { doSave(); });
|
||||
registerCommand("project.open", "Project: Open...",
|
||||
"",
|
||||
[this]() {
|
||||
std::string path = FileDialog::openFile(
|
||||
{"Open Project", {"*.whetstone"}, std::string()});
|
||||
if (!path.empty()) {
|
||||
if (!loadProject(path)) {
|
||||
outputLog += "Failed to open project: " + path + "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
registerCommand("project.save", "Project: Save...",
|
||||
"",
|
||||
[this]() {
|
||||
std::string path = FileDialog::saveFile(
|
||||
{"Save Project", {"*.whetstone"}, std::string()});
|
||||
if (!path.empty()) {
|
||||
if (!saveProject(path)) {
|
||||
outputLog += "Failed to save project: " + path + "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
registerCommand("edit.undo", "Edit: Undo",
|
||||
keys.getBinding("edit.undo").toString(),
|
||||
[this]() { doUndo(); });
|
||||
@@ -1751,6 +1861,16 @@ int main(int, char**) {
|
||||
state.doOpen(path);
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Open Project...")) {
|
||||
auto path = FileDialog::openFile({"Open Project", {"*.whetstone"}, lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
if (!state.loadProject(path)) {
|
||||
state.outputLog += "Failed to open project: " + path + "\n";
|
||||
} else {
|
||||
lastDialogPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Open Folder...")) {
|
||||
auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot});
|
||||
if (!path.empty()) {
|
||||
@@ -1775,6 +1895,16 @@ int main(int, char**) {
|
||||
state.doSave();
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Save Project...")) {
|
||||
auto path = FileDialog::saveFile({"Save Project", {"*.whetstone"}, lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
if (!state.saveProject(path)) {
|
||||
state.outputLog += "Failed to save project: " + path + "\n";
|
||||
} else {
|
||||
lastDialogPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Exit")) done = true;
|
||||
ImGui::EndMenu();
|
||||
|
||||
41
editor/tests/step116_test.cpp
Normal file
41
editor/tests/step116_test.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Step 116 TDD Test: Project save/load serialization
|
||||
#include "ProjectManager.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
ProjectFile project;
|
||||
project.workspaceRoot = "C:/repo";
|
||||
project.activePath = "C:/repo/main.py";
|
||||
project.buffers.push_back({"C:/repo/main.py", "python", "structured"});
|
||||
project.buffers.push_back({"C:/repo/util.py", "python", "text"});
|
||||
project.ast = std::make_unique<Module>("m1", "Main", "python");
|
||||
|
||||
auto j = projectToJson(project);
|
||||
ProjectFile loaded = projectFromJson(j);
|
||||
|
||||
bool okRoot = loaded.workspaceRoot == "C:/repo";
|
||||
bool okActive = loaded.activePath == "C:/repo/main.py";
|
||||
bool okBuffers = loaded.buffers.size() == 2 && loaded.buffers[1].mode == "text";
|
||||
bool okAst = loaded.ast && loaded.ast->name == "Main" && loaded.ast->targetLanguage == "python";
|
||||
|
||||
expect(okRoot, "workspace root", passed, failed);
|
||||
expect(okActive, "active path", passed, failed);
|
||||
expect(okBuffers, "buffer list", passed, failed);
|
||||
expect(okAst, "ast roundtrip", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 116 Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -361,7 +361,7 @@ AST extensions for Sprint 5.
|
||||
their own `~/.emacs.d/`).
|
||||
*Wires:* `Orchestrator.h`
|
||||
|
||||
- [ ] **Step 116: Project save/load (AST serialization)**
|
||||
- [x] **Step 116: Project save/load (AST serialization)**
|
||||
Save/load `.whetstone` project files using `ast/Serialization.h`.
|
||||
JSON format preserving the full AST with annotations. File > Save Project /
|
||||
Open Project. Includes workspace root path and open buffer list.
|
||||
|
||||
Reference in New Issue
Block a user