diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c0a248f..9259f8a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index a31672c..4d26b42 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -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* errorLines = nullptr; const std::vector* warningLines = nullptr; @@ -29,6 +30,10 @@ struct CodeEditorOptions { const std::vector* annotations = nullptr; const std::vector* suggestions = nullptr; const std::vector* 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()) { diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 10e86f3..4b035a4 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -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 highlights; + std::vector 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(); 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 errorLines; std::vector warningLines; std::vector 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)"); } diff --git a/editor/tests/step103_test.cpp b/editor/tests/step103_test.cpp new file mode 100644 index 0000000..205acff --- /dev/null +++ b/editor/tests/step103_test.cpp @@ -0,0 +1,28 @@ +// Step 103 TDD Test: Side-by-side generated code view +// +// Tests: +// 1. Generated code for python includes function name + +#include +#include +#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; +}