Step 103: add side-by-side generated view

This commit is contained in:
Bill
2026-02-09 10:52:23 -07:00
parent 60ae9e2d70
commit d627a7a588
4 changed files with 185 additions and 15 deletions

View File

@@ -558,6 +558,10 @@ add_executable(step102_test tests/step102_test.cpp)
target_include_directories(step102_test PRIVATE src)
target_link_libraries(step102_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step103_test tests/step103_test.cpp)
target_include_directories(step103_test PRIVATE src)
target_link_libraries(step103_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -22,6 +22,7 @@ struct CodeEditorOptions {
bool enableFolding = false;
bool showMinimap = false;
bool showAnnotations = false;
bool showCurrentLine = true;
int annotationLayout = 0; // 0=above, 1=margin, 2=beside
const std::vector<int>* errorLines = nullptr;
const std::vector<int>* warningLines = nullptr;
@@ -29,6 +30,10 @@ struct CodeEditorOptions {
const std::vector<struct AnnotationMarker>* annotations = nullptr;
const std::vector<struct SuggestionMarker>* suggestions = nullptr;
const std::vector<struct AnnotationConflictMarker>* conflicts = nullptr;
int highlightLine = -1;
float* syncScrollX = nullptr;
float* syncScrollY = nullptr;
bool scrollMaster = false;
};
struct CodeEditorResult {
@@ -45,6 +50,8 @@ struct CodeEditorResult {
float minimapViewportEnd = 0.0f;
bool suggestionClicked = false;
struct SuggestionMarker clickedSuggestion;
bool lineClicked = false;
int clickedLine = -1;
};
struct DiagnosticRange {
@@ -144,6 +151,12 @@ public:
lineCount * lineHeight + 4.0f);
ImGui::BeginChild(id, size, false, ImGuiWindowFlags_HorizontalScrollbar);
if (options.syncScrollX && !options.scrollMaster) {
ImGui::SetScrollX(*options.syncScrollX);
}
if (options.syncScrollY && !options.scrollMaster) {
ImGui::SetScrollY(*options.syncScrollY);
}
std::string annoPopupId = std::string(id) + "_AnnoPopup";
ImVec2 origin = ImGui::GetCursorScreenPos();
@@ -153,6 +166,12 @@ public:
ImDrawList* drawList = ImGui::GetWindowDrawList();
const float scrollX = ImGui::GetScrollX();
const float scrollY = ImGui::GetScrollY();
if (options.syncScrollX && options.scrollMaster) {
*options.syncScrollX = scrollX;
}
if (options.syncScrollY && options.scrollMaster) {
*options.syncScrollY = scrollY;
}
ImVec2 gutterBase(origin.x, origin.y - scrollY);
ImVec2 textBase(origin.x + gutterWidth - scrollX, origin.y - scrollY);
const float windowWidth = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x;
@@ -181,6 +200,8 @@ public:
int clickCount = ImGui::GetIO().MouseClickedCount[0];
if (mouse.x < origin.x + gutterWidth || clickCount >= 3) {
int line = lineFromMouseY(mouse.y, gutterBase.y, lineHeight, lineCount);
result.lineClicked = true;
result.clickedLine = line;
const FoldRegion* fold = findFoldAtLine(line);
if (fold && mouse.x < origin.x + 12.0f) {
toggleFoldAtLine(line);
@@ -192,6 +213,8 @@ public:
selEnd_ = lineEnd;
} else {
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
result.lineClicked = true;
result.clickedLine = lineFromPos(pos, lineStarts);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
selectWordAt(text, pos);
} else if (ImGui::GetIO().KeyShift) {
@@ -357,11 +380,16 @@ public:
}
// Current line highlight (text area only)
if (ln == currentLine) {
if (options.showCurrentLine && ln == currentLine) {
ImVec2 hlA(textBase.x, y);
ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight);
drawList->AddRectFilled(hlA, hlB, IM_COL32(40, 40, 40, 120));
}
if (options.highlightLine >= 0 && ln == options.highlightLine) {
ImVec2 hlA(textBase.x, y);
ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight);
drawList->AddRectFilled(hlA, hlB, IM_COL32(90, 70, 30, 120));
}
// Selection background
if (hasSelection()) {

View File

@@ -58,16 +58,25 @@ struct BufferState {
TextEditor editor;
TextASTSync sync;
CodeEditorWidget widget;
CodeEditorWidget generatedWidget;
EditorMode mode;
EditorMode generatedMode;
std::string language = "python";
std::string generatedLanguage = "python";
std::string path = "(untitled)";
bool modified = false;
int cursorLine = 1;
int cursorCol = 1;
int lspVersion = 1;
std::string editBuf;
std::string generatedBuf;
std::vector<HighlightSpan> highlights;
std::vector<HighlightSpan> generatedHighlights;
bool highlightsDirty = true;
bool generatedHighlightsDirty = true;
int generatedHighlightLine = -1;
float splitScrollX = 0.0f;
float splitScrollY = 0.0f;
};
struct EditorState {
@@ -205,12 +214,15 @@ struct EditorState {
auto state = std::make_unique<BufferState>();
state->path = path;
state->language = language;
state->generatedLanguage = language;
state->mode.setLanguage(language);
state->generatedMode.setLanguage(language);
state->editor.setContent(content, language);
state->sync.setText(content, language);
state->sync.syncNow();
state->editBuf = content;
state->highlightsDirty = true;
state->generatedHighlightsDirty = true;
state->modified = false;
state->lspVersion = 1;
buffers.openBuffer(path, content, language);
@@ -275,8 +287,14 @@ struct EditorState {
void setLanguage(const std::string& lang) {
if (!active()) return;
std::string prevLang = active()->language;
active()->language = lang;
active()->mode.setLanguage(lang);
if (active()->generatedLanguage == prevLang) {
active()->generatedLanguage = lang;
active()->generatedMode.setLanguage(lang);
active()->generatedHighlightsDirty = true;
}
active()->editor.setContent(active()->editBuf, lang);
active()->sync.setText(active()->editBuf, lang);
active()->sync.syncNow();
@@ -290,6 +308,7 @@ struct EditorState {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
@@ -304,6 +323,7 @@ struct EditorState {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
}
void doRedo() {
@@ -313,6 +333,7 @@ struct EditorState {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
}
void doFind() {
@@ -344,6 +365,7 @@ struct EditorState {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
}
outputLog += "Replaced " + std::to_string(count) + " occurrence(s).\n";
@@ -411,6 +433,7 @@ struct EditorState {
buf->sync.setText(buf->editBuf, buf->language);
buf->sync.syncNow();
buf->highlightsDirty = true;
buf->generatedHighlightsDirty = true;
buf->modified = false;
outputLog += "Reloaded: " + path + "\n";
}
@@ -435,6 +458,36 @@ struct EditorState {
active()->highlightsDirty = false;
}
void updateGenerated() {
if (!active()) return;
Module* ast = active()->sync.getAST();
std::string generated;
if (ast) {
if (active()->generatedLanguage == "python") {
PythonGenerator gen;
generated = gen.generate(ast);
} else if (active()->generatedLanguage == "cpp") {
CppGenerator gen;
generated = gen.generate(ast);
} else if (active()->generatedLanguage == "elisp") {
ElispGenerator gen;
generated = gen.generate(ast);
} else {
PythonGenerator gen;
generated = gen.generate(ast);
}
}
if (generated != active()->generatedBuf) {
active()->generatedBuf = generated;
active()->generatedHighlightsDirty = true;
}
if (active()->generatedHighlightsDirty) {
active()->generatedHighlights =
SyntaxHighlighter::highlight(active()->generatedBuf, active()->generatedLanguage);
active()->generatedHighlightsDirty = false;
}
}
void updateCursorPos(int bytePos) {
if (!active()) return;
active()->cursorLine = 1;
@@ -459,6 +512,14 @@ struct NullLSPTransport : public LSPTransport {
void close() override {}
};
static int countLines(const std::string& text) {
int lines = 1;
for (char c : text) {
if (c == '\n') ++lines;
}
return lines;
}
// ---------------------------------------------------------------------------
// ImGui InputTextMultiline with std::string resize callback
@@ -1228,6 +1289,7 @@ int main(int, char**) {
avail.y -= 4; // small margin
state.updateHighlights();
state.updateGenerated();
std::vector<int> errorLines;
std::vector<int> warningLines;
std::vector<DiagnosticRange> diagRanges;
@@ -1312,10 +1374,68 @@ int main(int, char**) {
opts.annotations = &annoMarkers;
opts.suggestions = &suggestionMarkers;
opts.conflicts = &conflictMarkers;
opts.syncScrollX = &buf->splitScrollX;
opts.syncScrollY = &buf->splitScrollY;
opts.scrollMaster = true;
ImGui::BeginTable("##editorSplit", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp);
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f);
ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
CodeEditorResult res = buf->widget.render("##editor",
buf->editBuf, buf->highlights, opts, avail, monoFont);
buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont);
ImGui::TableSetColumnIndex(1);
ImGui::BeginGroup();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Generated");
ImGui::SameLine();
const char* targetLabels[] = {"Python", "C++", "Elisp"};
const char* targetValues[] = {"python", "cpp", "elisp"};
int targetIndex = 0;
for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) {
if (buf->generatedLanguage == targetValues[i]) {
targetIndex = i;
break;
}
}
ImGui::SetNextItemWidth(120.0f);
if (ImGui::Combo("##targetLang", &targetIndex,
targetLabels, IM_ARRAYSIZE(targetLabels))) {
buf->generatedLanguage = targetValues[targetIndex];
buf->generatedMode.setLanguage(buf->generatedLanguage);
buf->generatedHighlightsDirty = true;
state.updateGenerated();
}
ImVec2 genAvail = ImGui::GetContentRegionAvail();
CodeEditorOptions genOpts;
genOpts.readOnly = true;
genOpts.showWhitespace = state.showWhitespace;
genOpts.mode = &buf->generatedMode;
genOpts.showCurrentLine = false;
genOpts.highlightLine = buf->generatedHighlightLine;
genOpts.syncScrollX = &buf->splitScrollX;
genOpts.syncScrollY = &buf->splitScrollY;
genOpts.scrollMaster = false;
buf->generatedWidget.render("##generated",
buf->generatedBuf, buf->generatedHighlights, genOpts,
ImVec2(0, genAvail.y), monoFont);
ImGui::EndGroup();
ImGui::EndTable();
state.updateCursorPos(res.cursorByte);
if (res.lineClicked) {
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
if (ast) {
int genLines = countLines(buf->generatedBuf);
int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1));
buf->generatedHighlightLine = targetLine;
} else {
buf->generatedHighlightLine = -1;
}
}
if (res.changed) {
state.onTextChanged();
state.completionPending = true;
@@ -1801,19 +1921,9 @@ int main(int, char**) {
ImGui::PushFont(monoFont);
ImGui::BeginChild("##genScroll", ImVec2(0, 0), false);
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
if (ast) {
std::string generated;
if (state.active()->language == "python") {
PythonGenerator gen;
generated = gen.generate(ast);
} else if (state.active()->language == "cpp") {
CppGenerator gen;
generated = gen.generate(ast);
} else if (state.active()->language == "elisp") {
ElispGenerator gen;
generated = gen.generate(ast);
}
ImGui::TextUnformatted(generated.c_str());
state.updateGenerated();
if (ast && state.active()) {
ImGui::TextUnformatted(state.active()->generatedBuf.c_str());
} else {
ImGui::TextDisabled("(no AST)");
}

View File

@@ -0,0 +1,28 @@
// Step 103 TDD Test: Side-by-side generated code view
//
// Tests:
// 1. Generated code for python includes function name
#include <cassert>
#include <iostream>
#include "ast/Generator.h"
#include "ast/Module.h"
#include "ast/Function.h"
int main() {
int passed = 0;
int failed = 0;
Module mod("m1", "mod", "python");
auto* fn = new Function("f1", "foo");
mod.addChild("functions", fn);
PythonGenerator gen;
auto out = gen.generate(&mod);
assert(out.find("def foo") != std::string::npos);
std::cout << "Test 1 PASS: generated code includes function" << std::endl;
++passed;
std::cout << "\n=== Step 103 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}