diff --git a/PROGRESS.md b/PROGRESS.md index 93dd025..fa03f8b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -174,6 +174,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 76: **IMPLEMENTED** — LayoutManager: 3 preset docking layouts (VSCode/Emacs/JetBrains), panel visibility/ratio queries, dirty flag for rebuild, save/load persistence, preset name round-trip (10/10 tests pass) - [x] Step 77: **IMPLEMENTED** — Custom CodeEditorWidget renderer: per-token coloring, cursor/selection/input handling, monospace grid, blinking cursor, visible whitespace toggle (3/3 tests pass) - [x] Step 78: **IMPLEMENTED** — Line numbers and gutter: fixed-width gutter with right-aligned line numbers, current line highlight, gutter click selects line (2/2 tests pass) +- [x] Step 79: **IMPLEMENTED** — Auto-indent and smart editing: EditorMode integration, enter auto-indent, tab inserts spaces, auto-close brackets/quotes (3/3 tests pass) --- @@ -231,6 +232,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 76:** Compile and pass (10/10) **Step 77:** Compile and pass (3/3) **Step 78:** Compile and pass (2/2) +**Step 79:** Compile and pass (3/3) --- @@ -317,3 +319,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Claude Opus 4.6 | Step 76: LayoutManager with 3 preset docking layouts (VSCode/Emacs/JetBrains). Panel visibility/ratio queries, dirty flag, save/load persistence. 10/10 tests pass. Sprint 4 started. | | 2026-02-09 | Codex | Step 77: Custom code editor renderer (CodeEditorWidget). Per-token coloring, cursor/selection/input, blinking cursor, whitespace toggle. 3/3 tests pass. | | 2026-02-09 | Codex | Step 78: Line numbers and gutter. Gutter width auto-adjusts, current line highlight, gutter click selects line. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 79: Auto-indent and smart editing. EditorMode wired for newline indent, tab spacing, and bracket auto-close. 3/3 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ec3abe7..13638b5 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -331,6 +331,10 @@ add_executable(step78_test tests/step78_test.cpp) target_include_directories(step78_test PRIVATE src) target_link_libraries(step78_test PRIVATE imgui::imgui) +add_executable(step79_test tests/step79_test.cpp) +target_include_directories(step79_test PRIVATE src) +target_link_libraries(step79_test PRIVATE imgui::imgui) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index 1d6f42f..c0e5825 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -8,6 +8,7 @@ #include "imgui.h" #include "SyntaxHighlighter.h" +#include "EditorMode.h" #include #include #include @@ -15,6 +16,7 @@ struct CodeEditorOptions { bool showWhitespace = false; bool readOnly = false; + EditorMode* mode = nullptr; }; struct CodeEditorResult { @@ -121,7 +123,7 @@ public: // Keyboard input if (focused && !options.readOnly) { - handleKeyboard(text, result.changed, lineStarts); + handleKeyboard(text, result.changed, lineStarts, options.mode); } // Render visible lines @@ -323,7 +325,48 @@ private: changed = true; } - void handleKeyboard(std::string& text, bool& changed, const std::vector& lineStarts) { + void insertPair(std::string& text, char open, char close, bool& changed) { + deleteSelection(text, changed); + text.insert(cursor_, 1, open); + text.insert(cursor_ + 1, 1, close); + cursor_ += 1; + changed = true; + } + + static std::string lineTextAt(const std::string& text, const std::vector& lineStarts, int line) { + if (line < 0 || line >= (int)lineStarts.size()) return ""; + int start = lineStarts[line]; + int end = (line + 1 < (int)lineStarts.size()) ? lineStarts[line + 1] - 1 : (int)text.size(); + if (end < start) end = start; + return text.substr(start, end - start); + } + + static std::string leadingIndent(const std::string& line) { + size_t i = 0; + while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i; + return line.substr(0, i); + } + + void insertNewlineWithIndent(std::string& text, + const std::vector& lineStarts, + const EditorMode* mode, + bool& changed) { + std::string indent; + if (mode) { + int line = lineFromPos(cursor_, lineStarts); + std::string lineText = lineTextAt(text, lineStarts, line); + indent = leadingIndent(lineText); + if (mode->shouldIndentAfter(lineText)) { + indent += mode->getIndentString(); + } + } + insertText(text, "\n" + indent, changed); + } + + void handleKeyboard(std::string& text, + bool& changed, + const std::vector& lineStarts, + const EditorMode* mode) { ImGuiIO& io = ImGui::GetIO(); // Text input @@ -332,13 +375,25 @@ private: if (c == 0) continue; if (c == '\r') c = '\n'; if (c == '\n') { - insertText(text, "\n", changed); + insertNewlineWithIndent(text, lineStarts, mode, changed); } else if (c == '\t') { - insertText(text, "\t", changed); + if (mode) insertText(text, mode->getIndentString(), changed); + else insertText(text, "\t", changed); } else if (c >= 32) { - char buf[5] = {0}; - buf[0] = (char)c; - insertText(text, std::string(buf), changed); + if (mode) { + char close = mode->getClosingBracket((char)c); + if (close != 0) { + insertPair(text, (char)c, close, changed); + } else { + char buf[5] = {0}; + buf[0] = (char)c; + insertText(text, std::string(buf), changed); + } + } else { + char buf[5] = {0}; + buf[0] = (char)c; + insertText(text, std::string(buf), changed); + } } } io.InputQueueCharacters.resize(0); diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 842da5d..faf8b8c 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -18,6 +18,7 @@ #include "SyntaxHighlighter.h" #include "KeybindingManager.h" #include "CodeEditorWidget.h" +#include "EditorMode.h" #include "ast/Generator.h" #include @@ -35,6 +36,7 @@ struct EditorState { TextEditor editor; TextASTSync sync; KeybindingManager keys; + EditorMode mode; std::string language = "python"; std::string filePath = "(untitled)"; bool modified = false; @@ -81,6 +83,7 @@ struct EditorState { " return total\n"; editor.setContent(defaultContent, language); + mode.setLanguage(language); sync.setText(defaultContent, language); sync.syncNow(); editBuf = defaultContent; @@ -90,6 +93,7 @@ struct EditorState { void setLanguage(const std::string& lang) { language = lang; + mode.setLanguage(lang); editor.setContent(editBuf, lang); sync.setText(editBuf, lang); sync.syncNow(); @@ -661,6 +665,7 @@ int main(int, char**) { state.updateHighlights(); CodeEditorOptions opts; opts.showWhitespace = state.showWhitespace; + opts.mode = &state.mode; CodeEditorResult res = state.codeWidget.render("##editor", state.editBuf, state.highlights, opts, avail, monoFont); diff --git a/editor/tests/step79_test.cpp b/editor/tests/step79_test.cpp new file mode 100644 index 0000000..cd4f83a --- /dev/null +++ b/editor/tests/step79_test.cpp @@ -0,0 +1,113 @@ +// Step 79 TDD Test: Auto-indent and smart editing +// +// Tests: +// 1. Enter in Python after ':' inserts newline + indent +// 2. Tab inserts spaces per EditorMode +// 3. Auto-close bracket inserts pair and places cursor inside + +#include +#include +#include "imgui.h" +#include "CodeEditorWidget.h" +#include "EditorMode.h" + +static void beginFrame() { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(800, 600); + io.DeltaTime = 1.0f / 60.0f; + ImGui::NewFrame(); +} + +static void endFrame() { + ImGui::Render(); +} + +int main() { + int passed = 0; + int failed = 0; + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontDefault(); + io.Fonts->Build(); + + // --- Test 1: Auto-indent after ':' in Python --- + { + beginFrame(); + ImGui::SetNextWindowFocus(); + ImGui::Begin("TestWindow"); + CodeEditorWidget widget; + EditorMode mode("python"); + std::string text = "def f():"; + widget.setCursor((int)text.size()); + std::vector spans; + CodeEditorOptions opts; + opts.mode = &mode; + + io.AddInputCharacter('\n'); + CodeEditorResult res = widget.render("##editor", text, spans, opts, ImVec2(300, 200), ImGui::GetFont()); + ImGui::End(); + endFrame(); + + assert(res.changed && "Should change on newline"); + assert(text == "def f():\n " && "Python indent should be 4 spaces"); + std::cout << "Test 1 PASS: Auto-indent after ':'" << std::endl; + ++passed; + } + + // --- Test 2: Tab inserts spaces --- + { + beginFrame(); + ImGui::SetNextWindowFocus(); + ImGui::Begin("TestWindow2"); + CodeEditorWidget widget; + EditorMode mode("cpp"); + std::string text; + widget.setCursor(0); + std::vector spans; + CodeEditorOptions opts; + opts.mode = &mode; + + io.AddInputCharacter('\t'); + CodeEditorResult res = widget.render("##editor2", text, spans, opts, ImVec2(300, 200), ImGui::GetFont()); + ImGui::End(); + endFrame(); + + assert(res.changed); + assert(text == " " && "Tab should insert 4 spaces in C++ mode"); + std::cout << "Test 2 PASS: Tab inserts spaces" << std::endl; + ++passed; + } + + // --- Test 3: Auto-close bracket --- + { + beginFrame(); + ImGui::SetNextWindowFocus(); + ImGui::Begin("TestWindow3"); + CodeEditorWidget widget; + EditorMode mode("python"); + std::string text; + widget.setCursor(0); + std::vector spans; + CodeEditorOptions opts; + opts.mode = &mode; + + io.AddInputCharacter('('); + CodeEditorResult res = widget.render("##editor3", text, spans, opts, ImVec2(300, 200), ImGui::GetFont()); + ImGui::End(); + endFrame(); + + assert(res.changed); + assert(text == "()" && "Bracket pair should be inserted"); + assert(res.cursorByte == 1 && "Cursor should be inside the pair"); + std::cout << "Test 3 PASS: Auto-close bracket" << std::endl; + ++passed; + } + + ImGui::DestroyContext(); + + std::cout << "\n=== Step 79 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}