From d801b1493c928e34da92c8d9f56fe9d415005478 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 10:36:40 -0700 Subject: [PATCH] Step 560: add locals and watches panel data model --- editor/CMakeLists.txt | 9 ++ editor/src/LocalsWatchesPanelModel.h | 90 +++++++++++++++++ editor/tests/step560_test.cpp | 138 +++++++++++++++++++++++++++ progress.md | 34 +++++++ 4 files changed, 271 insertions(+) create mode 100644 editor/src/LocalsWatchesPanelModel.h create mode 100644 editor/tests/step560_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 639f942..e1132e8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3919,4 +3919,13 @@ target_link_libraries(step559_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step560_test tests/step560_test.cpp) +target_include_directories(step560_test PRIVATE src) +target_link_libraries(step560_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/LocalsWatchesPanelModel.h b/editor/src/LocalsWatchesPanelModel.h new file mode 100644 index 0000000..6bc59ed --- /dev/null +++ b/editor/src/LocalsWatchesPanelModel.h @@ -0,0 +1,90 @@ +#pragma once +// Step 560: Locals + Watches Panel Data Model + +#include +#include +#include + +struct LocalVarEntry { + std::string name; + std::string type; + std::string value; +}; + +struct WatchExpression { + std::string id; + std::string expression; + bool enabled = true; + std::string lastValue; + bool lastEvalOk = false; +}; + +class LocalsWatchesPanelModel { +public: + void setLocals(const std::vector& locals) { + locals_ = locals; + } + + const std::vector& locals() const { return locals_; } + + bool addWatch(const std::string& id, const std::string& expression) { + if (id.empty() || expression.empty()) return false; + if (watchIndex_.count(id) != 0) return false; + watches_.push_back({id, expression, true, "", false}); + watchIndex_[id] = watches_.size() - 1; + return true; + } + + bool removeWatch(const std::string& id) { + auto it = watchIndex_.find(id); + if (it == watchIndex_.end()) return false; + watches_.erase(watches_.begin() + static_cast(it->second)); + rebuildIndex(); + return true; + } + + bool enableWatch(const std::string& id, bool enabled) { + auto* w = findWatch(id); + if (!w) return false; + w->enabled = enabled; + return true; + } + + bool updateWatchValue(const std::string& id, + const std::string& value, + bool evalOk) { + auto* w = findWatch(id); + if (!w) return false; + w->lastValue = value; + w->lastEvalOk = evalOk; + return true; + } + + const std::vector& watches() const { return watches_; } + + std::vector activeWatches() const { + std::vector out; + for (const auto& w : watches_) { + if (w.enabled) out.push_back(w); + } + return out; + } + +private: + std::vector locals_; + std::vector watches_; + std::map watchIndex_; + + WatchExpression* findWatch(const std::string& id) { + auto it = watchIndex_.find(id); + if (it == watchIndex_.end()) return nullptr; + return &watches_[it->second]; + } + + void rebuildIndex() { + watchIndex_.clear(); + for (size_t i = 0; i < watches_.size(); ++i) { + watchIndex_[watches_[i].id] = i; + } + } +}; diff --git a/editor/tests/step560_test.cpp b/editor/tests/step560_test.cpp new file mode 100644 index 0000000..7be0101 --- /dev/null +++ b/editor/tests/step560_test.cpp @@ -0,0 +1,138 @@ +// Step 560: Locals + Watches Panel Data Model (12 tests) + +#include "LocalsWatchesPanelModel.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_set_locals_persists_entries() { + TEST(set_locals_persists_entries); + LocalsWatchesPanelModel m; + m.setLocals({{"cursor", "int", "12"}, {"state", "std::string", "ok"}}); + CHECK(m.locals().size() == 2, "two locals expected"); + PASS(); +} + +void test_locals_preserve_type_information() { + TEST(locals_preserve_type_information); + LocalsWatchesPanelModel m; + m.setLocals({{"cursor", "int", "12"}}); + CHECK(m.locals()[0].type == "int", "type info mismatch"); + PASS(); +} + +void test_add_watch_succeeds_for_unique_id() { + TEST(add_watch_succeeds_for_unique_id); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add watch should succeed"); + CHECK(m.watches().size() == 1, "one watch expected"); + PASS(); +} + +void test_add_watch_rejects_duplicate_id() { + TEST(add_watch_rejects_duplicate_id); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "first add should succeed"); + CHECK(!m.addWatch("w1", "state"), "duplicate id should fail"); + PASS(); +} + +void test_add_watch_rejects_empty_fields() { + TEST(add_watch_rejects_empty_fields); + LocalsWatchesPanelModel m; + CHECK(!m.addWatch("", "cursor"), "empty id should fail"); + CHECK(!m.addWatch("w1", ""), "empty expression should fail"); + PASS(); +} + +void test_remove_watch_succeeds() { + TEST(remove_watch_succeeds); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add should succeed"); + CHECK(m.removeWatch("w1"), "remove should succeed"); + CHECK(m.watches().empty(), "watches should be empty"); + PASS(); +} + +void test_remove_watch_fails_for_unknown_id() { + TEST(remove_watch_fails_for_unknown_id); + LocalsWatchesPanelModel m; + CHECK(!m.removeWatch("missing"), "remove unknown should fail"); + PASS(); +} + +void test_enable_disable_watch() { + TEST(enable_disable_watch); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add should succeed"); + CHECK(m.enableWatch("w1", false), "disable should succeed"); + CHECK(!m.watches()[0].enabled, "watch should be disabled"); + CHECK(m.enableWatch("w1", true), "enable should succeed"); + CHECK(m.watches()[0].enabled, "watch should be enabled"); + PASS(); +} + +void test_update_watch_value_tracks_eval_status() { + TEST(update_watch_value_tracks_eval_status); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add should succeed"); + CHECK(m.updateWatchValue("w1", "12", true), "update should succeed"); + CHECK(m.watches()[0].lastValue == "12", "value mismatch"); + CHECK(m.watches()[0].lastEvalOk, "eval status mismatch"); + PASS(); +} + +void test_update_watch_value_fails_for_unknown_watch() { + TEST(update_watch_value_fails_for_unknown_watch); + LocalsWatchesPanelModel m; + CHECK(!m.updateWatchValue("missing", "x", true), "unknown watch update should fail"); + PASS(); +} + +void test_active_watches_filters_disabled_entries() { + TEST(active_watches_filters_disabled_entries); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add w1"); + CHECK(m.addWatch("w2", "state"), "add w2"); + CHECK(m.enableWatch("w2", false), "disable w2"); + auto active = m.activeWatches(); + CHECK(active.size() == 1, "only one active watch expected"); + CHECK(active[0].id == "w1", "active watch mismatch"); + PASS(); +} + +void test_remove_rebuilds_index_for_remaining_watches() { + TEST(remove_rebuilds_index_for_remaining_watches); + LocalsWatchesPanelModel m; + CHECK(m.addWatch("w1", "cursor"), "add w1"); + CHECK(m.addWatch("w2", "state"), "add w2"); + CHECK(m.removeWatch("w1"), "remove w1"); + CHECK(m.updateWatchValue("w2", "ok", true), "w2 should remain addressable"); + CHECK(m.watches()[0].id == "w2", "remaining watch should be w2"); + PASS(); +} + +int main() { + std::cout << "Step 560: Locals + Watches Panel Data Model\n"; + + test_set_locals_persists_entries(); // 1 + test_locals_preserve_type_information(); // 2 + test_add_watch_succeeds_for_unique_id(); // 3 + test_add_watch_rejects_duplicate_id(); // 4 + test_add_watch_rejects_empty_fields(); // 5 + test_remove_watch_succeeds(); // 6 + test_remove_watch_fails_for_unknown_id(); // 7 + test_enable_disable_watch(); // 8 + test_update_watch_value_tracks_eval_status(); // 9 + test_update_watch_value_fails_for_unknown_watch(); // 10 + test_active_watches_filters_disabled_entries(); // 11 + test_remove_rebuilds_index_for_remaining_watches(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index b363ec3..2465dc4 100644 --- a/progress.md +++ b/progress.md @@ -9976,3 +9976,37 @@ mapping for runtime inspection surfaces. - `editor/src/CallStackFrameInspector.h` within header-size limit (`58` <= `600`) - `editor/tests/step559_test.cpp` within test-file size guidance (`146` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 560: Locals + Watches Panel Data Model +**Status:** PASS (12/12 tests) + +Implements runtime locals and watch-expression state modeling with type-aware +locals display fields and watch lifecycle management. + +**Files added:** +- `editor/src/LocalsWatchesPanelModel.h` - locals/watches data model: + - type-aware local variable entries + - watch add/remove/enable-disable lifecycle + - watch evaluation value/status tracking + - active-watch filtering and index-rebuild safety +- `editor/tests/step560_test.cpp` - 12 tests covering: + - locals persistence/type behavior + - watch lifecycle behavior and input validation + - watch enable/disable behavior + - evaluation update behavior + - active-watch filtering behavior + - index rebuild behavior after watch removal + +**Files modified:** +- `editor/CMakeLists.txt` - `step560_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step560_test step559_test` - PASS +- `./editor/build-native/step560_test` - PASS (12/12) +- `./editor/build-native/step559_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/LocalsWatchesPanelModel.h` within header-size limit (`90` <= `600`) +- `editor/tests/step560_test.cpp` within test-file size guidance (`138` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`