Step 79: auto-indent and smart editing
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
113
editor/tests/step79_test.cpp
Normal file
113
editor/tests/step79_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user