Phase 3c: Classical editing mode (Steps 50-53)

Implement TextASTSync and TextEditor — the core text editing layer
that bridges raw text and the structured AST. TextASTSync provides
bidirectional text-AST sync with debounce via TreeSitterParser and
generators. TextEditor wraps a text buffer with edit operations,
undo/redo that tracks AST state, find/replace, and selection.

New files: TextASTSync.h, TextEditor.h, step50_test.cpp
Updated: step51/53 tests (replaced stubs with real includes),
CMakeLists.txt (tree-sitter linking), PROGRESS.md
Fixed: step53 find-position off-by-one, canRedo assert logic

19/19 tests pass (step50: 8, step51: 5, step53: 6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-08 15:45:59 -07:00
parent 41166812b0
commit 742c33d813
7 changed files with 380 additions and 86 deletions

View File

@@ -0,0 +1,139 @@
// Step 50 TDD Test: Text editor component
//
// Validates the TextEditor and TextASTSync components exist and
// basic operations work: create, set content, get content, basic
// insert/delete, language selection.
#include <iostream>
#include <string>
#include <cassert>
#include "TextASTSync.h"
#include "TextEditor.h"
static bool contains(const std::string& haystack, const std::string& needle) {
return haystack.find(needle) != std::string::npos;
}
int main() {
int passed = 0;
int failed = 0;
// --- Test 1: TextASTSync default state ---
{
TextASTSync sync;
assert(sync.getAST() == nullptr && "Default AST should be null");
assert(!sync.isParsePending() && "No parse should be pending initially");
std::cout << "Test 1 PASS: TextASTSync default state" << std::endl;
++passed;
}
// --- Test 2: TextEditor set/get content ---
{
TextEditor editor;
editor.setContent("hello world", "python");
assert(editor.getContent() == "hello world" && "Content should match");
std::cout << "Test 2 PASS: TextEditor set/get content" << std::endl;
++passed;
}
// --- Test 3: TextEditor insert ---
{
TextEditor editor;
editor.setContent("hello world", "python");
editor.insertText(5, " beautiful");
assert(editor.getContent() == "hello beautiful world" && "Insert should work");
std::cout << "Test 3 PASS: TextEditor insert" << std::endl;
++passed;
}
// --- Test 4: TextEditor delete ---
{
TextEditor editor;
editor.setContent("hello world", "python");
editor.deleteText(5, 6); // delete " world"
assert(editor.getContent() == "hello" && "Delete should work");
std::cout << "Test 4 PASS: TextEditor delete" << std::endl;
++passed;
}
// --- Test 5: TextASTSync Python parse produces valid AST ---
{
TextASTSync sync;
sync.setText("def test_func(x, y):\n return x + y\n", "python");
sync.syncNow();
Module* ast = sync.getAST();
assert(ast != nullptr && "AST should exist after sync");
assert(ast->targetLanguage == "python" && "Language should be python");
auto functions = ast->getChildren("functions");
assert(functions.size() == 1 && "Should have one function");
auto* fn = static_cast<Function*>(functions[0]);
assert(fn->name == "test_func" && "Function name should match");
auto params = fn->getChildren("parameters");
assert(params.size() == 2 && "Should have two parameters");
std::cout << "Test 5 PASS: Python parse produces valid AST" << std::endl;
++passed;
}
// --- Test 6: TextEditor produces AST from content ---
{
TextEditor editor;
editor.setContent("def my_func():\n pass\n", "python");
Module* ast = editor.getAST();
assert(ast != nullptr && "TextEditor should produce AST");
auto functions = ast->getChildren("functions");
assert(!functions.empty() && "Should have parsed function");
auto* fn = static_cast<Function*>(functions[0]);
assert(fn->name == "my_func" && "Function name should match");
std::cout << "Test 6 PASS: TextEditor produces AST from content" << std::endl;
++passed;
}
// --- Test 7: TextEditor undo/redo availability ---
{
TextEditor editor;
editor.setContent("test", "python");
assert(!editor.canUndo() && "Should not be able to undo initially");
assert(!editor.canRedo() && "Should not be able to redo initially");
editor.insertText(4, "!");
assert(editor.canUndo() && "Should be able to undo after edit");
editor.undo();
assert(editor.canRedo() && "Should be able to redo after undo");
assert(editor.getContent() == "test" && "Undo should restore text");
std::cout << "Test 7 PASS: Undo/redo availability tracking" << std::endl;
++passed;
}
// --- Test 8: TextASTSync C++ parse ---
{
TextASTSync sync;
sync.setText("void hello() { return; }", "cpp");
sync.syncNow();
Module* ast = sync.getAST();
assert(ast != nullptr && "C++ AST should exist");
assert(ast->targetLanguage == "cpp" && "Language should be cpp");
std::cout << "Test 8 PASS: C++ parse via TextASTSync" << std::endl;
++passed;
}
// --- Summary ---
std::cout << "\n=== Step 50 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}

View File

@@ -1,52 +1,28 @@
// Step 51 TDD Test: Text AST synchronization
// Step 51 TDD Test: Text -> AST synchronization
//
// Tests bidirectional sync between text editing and AST:
// 1. Text change re-parse via tree-sitter AST updates
// 2. AST change (structured edit) regenerate text
// 3. Round-trip: text AST text produces semantically equivalent output
// 1. Text change -> re-parse via tree-sitter -> AST updates
// 2. AST change (structured edit) -> regenerate text
// 3. Round-trip: text -> AST -> text produces semantically equivalent output
// 4. Debouncing: rapid text changes don't cause excessive re-parsing
//
// Will fail until the text-AST sync layer is implemented.
// Depends on: TextASTSync, TreeSitterParser, PythonGenerator, CppGenerator
#include <iostream>
#include <string>
#include <cassert>
#include <memory>
#include "ast/Parser.h"
#include "ast/Generator.h"
#include "TextASTSync.h"
static bool contains(const std::string& haystack, const std::string& needle) {
return haystack.find(needle) != std::string::npos;
}
// Forward declaration for the sync class that doesn't exist yet
// This will need to be implemented as part of Step 51
class TextASTSync {
public:
// Set the source text; triggers re-parse (possibly debounced)
void setText(const std::string& text, const std::string& language);
// Get the current AST root
Module* getAST() const;
// Mutate the AST; triggers text regeneration
void setAST(std::unique_ptr<Module> module);
// Get the current text representation
std::string getText() const;
// Force immediate sync (skip debounce)
void syncNow();
// Check if a re-parse is pending (debounced but not yet executed)
bool isParsePending() const;
};
int main() {
int passed = 0;
int failed = 0;
// --- Test 1: Set text AST updates ---
// --- Test 1: Set text -> AST updates ---
{
TextASTSync sync;
sync.setText("def greet(name):\n return name\n", "python");
@@ -61,11 +37,11 @@ int main() {
auto* fn = static_cast<Function*>(functions[0]);
assert(fn->name == "greet" && "Function name should be 'greet'");
std::cout << "Test 1 PASS: setText AST has correct function" << std::endl;
std::cout << "Test 1 PASS: setText -> AST has correct function" << std::endl;
++passed;
}
// --- Test 2: AST change text updates ---
// --- Test 2: AST change -> text updates ---
{
TextASTSync sync;
sync.setText("def f(x):\n return x\n", "python");
@@ -83,11 +59,11 @@ int main() {
assert(contains(newText, "compute") && "Text should reflect renamed function");
assert(!contains(newText, "def f(") && "Old function name should be gone");
std::cout << "Test 2 PASS: AST change text updates" << std::endl;
std::cout << "Test 2 PASS: AST change -> text updates" << std::endl;
++passed;
}
// --- Test 3: Round-trip text AST text ---
// --- Test 3: Round-trip text -> AST -> text ---
{
std::string original = "def add(a, b):\n return a + b\n";
TextASTSync sync;
@@ -108,11 +84,11 @@ int main() {
++passed;
}
// --- Test 4: Debounce rapid changes don't cause immediate re-parse ---
// --- Test 4: Debounce --- rapid changes don't cause immediate re-parse ---
{
TextASTSync sync;
sync.setText("def a():\n pass\n", "python");
// Don't call syncNow simulate rapid edits
// Don't call syncNow --- simulate rapid edits
sync.setText("def ab():\n pass\n", "python");
sync.setText("def abc():\n pass\n", "python");
@@ -133,7 +109,7 @@ int main() {
++passed;
}
// --- Test 5: C++ text AST sync ---
// --- Test 5: C++ text -> AST sync ---
{
TextASTSync sync;
sync.setText("int f(int x) { return x + 1; }", "cpp");
@@ -146,7 +122,7 @@ int main() {
auto functions = ast->getChildren("functions");
assert(!functions.empty() && "Should parse C++ function");
std::cout << "Test 5 PASS: C++ text AST sync works" << std::endl;
std::cout << "Test 5 PASS: C++ text -> AST sync works" << std::endl;
++passed;
}

View File

@@ -4,54 +4,21 @@
// 1. Undo in text mode undoes both text and AST changes
// 2. Redo restores both text and AST
// 3. Find text in the text buffer
// 4. Replace text AST updates accordingly
// 4. Replace text -> AST updates accordingly
// 5. Copy/paste operations work in text mode
// 6. Text undo/redo integrates with orchestrator's operation journal
//
// Will fail until classical editing operations are implemented.
// Depends on: TextEditor, TreeSitterParser, PythonGenerator
#include <iostream>
#include <string>
#include <cassert>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "TextEditor.h"
static bool contains(const std::string& haystack, const std::string& needle) {
return haystack.find(needle) != std::string::npos;
}
// Forward declaration — TextEditor class that wraps text ops + AST sync
class TextEditor {
public:
// Set initial content
void setContent(const std::string& text, const std::string& language);
// Get current text
std::string getContent() const;
// Get current AST
Module* getAST() const;
// Edit operations
void insertText(int position, const std::string& text);
void deleteText(int position, int length);
void replaceText(int position, int length, const std::string& replacement);
// Undo/redo (must also undo/redo AST changes)
void undo();
void redo();
bool canUndo() const;
bool canRedo() const;
// Find/replace
int find(const std::string& query, int startPos = 0) const;
int replaceAll(const std::string& query, const std::string& replacement);
// Clipboard
std::string getSelection(int start, int length) const;
};
int main() {
int passed = 0;
int failed = 0;
@@ -112,7 +79,7 @@ int main() {
editor.replaceText(4, 5, "world");
editor.undo();
assert(!editor.canRedo() == false && "Should be able to redo after undo");
assert(editor.canRedo() && "Should be able to redo after undo");
editor.redo();
std::string text = editor.getContent();
@@ -134,7 +101,9 @@ int main() {
int pos = editor.find("return");
assert(pos >= 0 && "Should find 'return' in text");
assert(pos == 18 && "Position of 'return' should be at column after indent");
// "def hello():\n return 42\n"
// 0123456789012 34567... -> 'r' of "return" is at index 17
assert(pos == 17 && "Position of 'return' should be at index 17");
int notFound = editor.find("nonexistent");
assert(notFound == -1 && "Should return -1 for not found");