Step 79: auto-indent and smart editing

This commit is contained in:
Bill
2026-02-09 09:07:53 -07:00
parent b18e41c6d0
commit c66d80c0e8
5 changed files with 187 additions and 7 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -8,6 +8,7 @@
#include "imgui.h"
#include "SyntaxHighlighter.h"
#include "EditorMode.h"
#include <string>
#include <vector>
#include <algorithm>
@@ -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<int>& 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<int>& 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<int>& 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<int>& 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);

View File

@@ -18,6 +18,7 @@
#include "SyntaxHighlighter.h"
#include "KeybindingManager.h"
#include "CodeEditorWidget.h"
#include "EditorMode.h"
#include "ast/Generator.h"
#include <cstdio>
@@ -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);

View File

@@ -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 <cassert>
#include <iostream>
#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<HighlightSpan> 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<HighlightSpan> 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<HighlightSpan> 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;
}