Step 103: add side-by-side generated view
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
28
editor/tests/step103_test.cpp
Normal file
28
editor/tests/step103_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user