Step 359: Find and Replace Improvements (12/12 tests)
This commit is contained in:
@@ -2130,4 +2130,8 @@ add_executable(step358_test tests/step358_test.cpp)
|
||||
target_include_directories(step358_test PRIVATE src)
|
||||
target_link_libraries(step358_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step359_test tests/step359_test.cpp)
|
||||
target_include_directories(step359_test PRIVATE src)
|
||||
target_link_libraries(step359_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
162
editor/src/panels/FindReplaceBar.h
Normal file
162
editor/src/panels/FindReplaceBar.h
Normal file
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
// Step 359: Find/replace bar model with in-file and project search.
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct FindMatch {
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
int line = 1;
|
||||
int column = 1;
|
||||
};
|
||||
|
||||
struct ProjectFindResult {
|
||||
std::string filePath;
|
||||
int line = 1;
|
||||
int column = 1;
|
||||
std::string preview;
|
||||
};
|
||||
|
||||
class FindReplaceBar {
|
||||
public:
|
||||
void openFind() {
|
||||
visible_ = true;
|
||||
replaceMode_ = false;
|
||||
}
|
||||
|
||||
void openReplace() {
|
||||
visible_ = true;
|
||||
replaceMode_ = true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
visible_ = false;
|
||||
}
|
||||
|
||||
bool isVisible() const { return visible_; }
|
||||
bool isReplaceMode() const { return replaceMode_; }
|
||||
|
||||
void setQuery(const std::string& query, const std::string& text) {
|
||||
query_ = query;
|
||||
rebuildMatches(text);
|
||||
}
|
||||
|
||||
void setReplaceText(const std::string& repl) { replaceText_ = repl; }
|
||||
|
||||
const std::string& query() const { return query_; }
|
||||
const std::string& replaceText() const { return replaceText_; }
|
||||
|
||||
const std::vector<FindMatch>& matches() const { return matches_; }
|
||||
|
||||
int matchCount() const { return static_cast<int>(matches_.size()); }
|
||||
int currentIndex() const { return currentIndex_; }
|
||||
|
||||
bool hasHighlights() const { return !matches_.empty(); }
|
||||
|
||||
std::string matchCounterText() const {
|
||||
if (matches_.empty()) return "0 of 0";
|
||||
return std::to_string(currentIndex_ + 1) + " of " + std::to_string(matches_.size());
|
||||
}
|
||||
|
||||
void nextMatch() {
|
||||
if (matches_.empty()) return;
|
||||
currentIndex_ = (currentIndex_ + 1) % static_cast<int>(matches_.size());
|
||||
}
|
||||
|
||||
void prevMatch() {
|
||||
if (matches_.empty()) return;
|
||||
currentIndex_ = (currentIndex_ - 1 + static_cast<int>(matches_.size())) %
|
||||
static_cast<int>(matches_.size());
|
||||
}
|
||||
|
||||
bool replaceCurrent(std::string& text) {
|
||||
if (matches_.empty() || currentIndex_ < 0 || currentIndex_ >= (int)matches_.size()) return false;
|
||||
const auto m = matches_[currentIndex_];
|
||||
text.replace(m.start, m.end - m.start, replaceText_);
|
||||
rebuildMatches(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
int replaceAll(std::string& text) {
|
||||
if (query_.empty()) return 0;
|
||||
int replaced = 0;
|
||||
size_t pos = 0;
|
||||
while (true) {
|
||||
size_t found = text.find(query_, pos);
|
||||
if (found == std::string::npos) break;
|
||||
text.replace(found, query_.size(), replaceText_);
|
||||
pos = found + replaceText_.size();
|
||||
replaced++;
|
||||
}
|
||||
rebuildMatches(text);
|
||||
return replaced;
|
||||
}
|
||||
|
||||
std::vector<ProjectFindResult> searchProject(
|
||||
const std::vector<std::pair<std::string, std::string>>& files) const {
|
||||
std::vector<ProjectFindResult> out;
|
||||
if (query_.empty()) return out;
|
||||
for (const auto& [path, content] : files) {
|
||||
int line = 1;
|
||||
int col = 1;
|
||||
for (size_t i = 0; i < content.size(); ++i) {
|
||||
if (i + query_.size() <= content.size() &&
|
||||
content.compare(i, query_.size(), query_) == 0) {
|
||||
ProjectFindResult r;
|
||||
r.filePath = path;
|
||||
r.line = line;
|
||||
r.column = col;
|
||||
size_t previewStart = i > 10 ? i - 10 : 0;
|
||||
size_t previewLen = std::min<size_t>(40, content.size() - previewStart);
|
||||
r.preview = content.substr(previewStart, previewLen);
|
||||
out.push_back(r);
|
||||
}
|
||||
if (content[i] == '\n') {
|
||||
line++;
|
||||
col = 1;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
void rebuildMatches(const std::string& text) {
|
||||
matches_.clear();
|
||||
currentIndex_ = 0;
|
||||
if (query_.empty()) {
|
||||
currentIndex_ = -1;
|
||||
return;
|
||||
}
|
||||
int line = 1;
|
||||
int col = 1;
|
||||
for (size_t i = 0; i + query_.size() <= text.size(); ++i) {
|
||||
if (text.compare(i, query_.size(), query_) == 0) {
|
||||
FindMatch m;
|
||||
m.start = static_cast<int>(i);
|
||||
m.end = static_cast<int>(i + query_.size());
|
||||
m.line = line;
|
||||
m.column = col;
|
||||
matches_.push_back(m);
|
||||
}
|
||||
if (text[i] == '\n') {
|
||||
line++;
|
||||
col = 1;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
if (matches_.empty()) currentIndex_ = -1;
|
||||
}
|
||||
|
||||
bool visible_ = false;
|
||||
bool replaceMode_ = false;
|
||||
std::string query_;
|
||||
std::string replaceText_;
|
||||
std::vector<FindMatch> matches_;
|
||||
int currentIndex_ = -1;
|
||||
};
|
||||
157
editor/tests/step359_test.cpp
Normal file
157
editor/tests/step359_test.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// Step 359: Find and Replace Improvements (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "panels/FindReplaceBar.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
const std::string text = "alpha beta\nbeta gamma\nbeta\n";
|
||||
|
||||
// Test 1: Ctrl+F opens find bar
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
assert(bar.isVisible());
|
||||
assert(!bar.isReplaceMode());
|
||||
std::cout << "Test 1 PASSED: open find bar\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: typing filters matches
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
assert(bar.matchCount() == 3);
|
||||
std::cout << "Test 2 PASSED: typing filters matches\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: match highlighting visible
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
assert(bar.hasHighlights());
|
||||
std::cout << "Test 3 PASSED: highlights visible\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: match count accurate
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
assert(bar.matchCounterText() == "1 of 3");
|
||||
std::cout << "Test 4 PASSED: match count text\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: Enter cycles through matches (next)
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
int start = bar.currentIndex();
|
||||
bar.nextMatch();
|
||||
assert(bar.currentIndex() != start);
|
||||
std::cout << "Test 5 PASSED: next match cycle\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: Ctrl+H shows replace field
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openReplace();
|
||||
assert(bar.isVisible());
|
||||
assert(bar.isReplaceMode());
|
||||
std::cout << "Test 6 PASSED: replace mode open\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: replace works
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
std::string mutableText = text;
|
||||
bar.openReplace();
|
||||
bar.setQuery("gamma", mutableText);
|
||||
bar.setReplaceText("delta");
|
||||
bool ok = bar.replaceCurrent(mutableText);
|
||||
assert(ok);
|
||||
assert(mutableText.find("delta") != std::string::npos);
|
||||
std::cout << "Test 7 PASSED: replace current\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: project-wide search returns results
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
std::vector<std::pair<std::string, std::string>> files = {
|
||||
{"a.txt", text},
|
||||
{"b.txt", "zzz\nbeta\n"}
|
||||
};
|
||||
auto results = bar.searchProject(files);
|
||||
assert(results.size() == 4);
|
||||
std::cout << "Test 8 PASSED: project search results\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: Escape closes find bar
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.close();
|
||||
assert(!bar.isVisible());
|
||||
std::cout << "Test 9 PASSED: close find bar\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: empty search clears highlights
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
assert(bar.hasHighlights());
|
||||
bar.setQuery("", text);
|
||||
assert(!bar.hasHighlights());
|
||||
assert(bar.matchCount() == 0);
|
||||
std::cout << "Test 10 PASSED: empty query clears highlights\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: Shift+Enter cycles previous match
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
bar.openFind();
|
||||
bar.setQuery("beta", text);
|
||||
bar.nextMatch(); // index 1
|
||||
int idx = bar.currentIndex();
|
||||
bar.prevMatch();
|
||||
assert(bar.currentIndex() != idx);
|
||||
std::cout << "Test 11 PASSED: previous match cycle\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: replace-all updates count and clears matches when fully replaced
|
||||
{
|
||||
FindReplaceBar bar;
|
||||
std::string mutableText = text;
|
||||
bar.openReplace();
|
||||
bar.setQuery("beta", mutableText);
|
||||
bar.setReplaceText("B");
|
||||
int replaced = bar.replaceAll(mutableText);
|
||||
assert(replaced == 3);
|
||||
assert(mutableText.find("beta") == std::string::npos);
|
||||
std::cout << "Test 12 PASSED: replace all\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
36
progress.md
36
progress.md
@@ -1971,6 +1971,42 @@ and go-to-definition lookup with current-file preference and cross-file fallback
|
||||
- `step357_test` — PASS (12/12) regression coverage
|
||||
- `step358_test` — PASS (12/12) new step coverage
|
||||
|
||||
### Step 359: Find and Replace Improvements
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implemented a find/replace bar model with in-file match tracking, current-match
|
||||
navigation, replace-current/replace-all flows, and project-wide search results.
|
||||
Model behavior includes open/close modes, highlight state, and match counter text.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/panels/FindReplaceBar.h` — find/replace model:
|
||||
- find/replace visibility + mode toggles
|
||||
- query + match indexing + counter text
|
||||
- next/previous match navigation
|
||||
- replace-current and replace-all operations
|
||||
- project-wide search result generation
|
||||
- `editor/tests/step359_test.cpp` — 12 tests covering:
|
||||
1. open find
|
||||
2. query-to-match filtering
|
||||
3. highlight visibility
|
||||
4. match counter formatting
|
||||
5. next-match cycling
|
||||
6. replace mode open
|
||||
7. replace-current operation
|
||||
8. project search results
|
||||
9. close behavior
|
||||
10. empty-query clears highlights
|
||||
11. previous-match cycling
|
||||
12. replace-all behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — `step359_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step357_test` — PASS (12/12) regression coverage
|
||||
- `step358_test` — PASS (12/12) regression coverage
|
||||
- `step359_test` — PASS (12/12) new step coverage
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user