Step 560: add locals and watches panel data model
This commit is contained in:
@@ -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)
|
||||
|
||||
90
editor/src/LocalsWatchesPanelModel.h
Normal file
90
editor/src/LocalsWatchesPanelModel.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
// Step 560: Locals + Watches Panel Data Model
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<LocalVarEntry>& locals) {
|
||||
locals_ = locals;
|
||||
}
|
||||
|
||||
const std::vector<LocalVarEntry>& 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<long>(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<WatchExpression>& watches() const { return watches_; }
|
||||
|
||||
std::vector<WatchExpression> activeWatches() const {
|
||||
std::vector<WatchExpression> out;
|
||||
for (const auto& w : watches_) {
|
||||
if (w.enabled) out.push_back(w);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<LocalVarEntry> locals_;
|
||||
std::vector<WatchExpression> watches_;
|
||||
std::map<std::string, size_t> 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
138
editor/tests/step560_test.cpp
Normal file
138
editor/tests/step560_test.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
// Step 560: Locals + Watches Panel Data Model (12 tests)
|
||||
|
||||
#include "LocalsWatchesPanelModel.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
34
progress.md
34
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`
|
||||
|
||||
Reference in New Issue
Block a user