Step 86: multi-tab editing
This commit is contained in:
@@ -183,6 +183,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
|
||||
### Phase 4b: File Management (Steps 84–89) — In Progress
|
||||
- [x] Step 84: **IMPLEMENTED** — Native file dialogs via tinyfiledialogs, FileDialog wrapper with test provider injection (2/2 tests pass)
|
||||
- [x] Step 85: **IMPLEMENTED** — Filesystem tree with .gitignore filtering, recursive Explorer rendering (1/1 tests pass)
|
||||
- [x] Step 86: **IMPLEMENTED** — Multi-tab editing with BufferManager and per-buffer editor state (3/3 tests pass)
|
||||
|
||||
---
|
||||
|
||||
@@ -247,6 +248,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
**Step 83:** Compile and pass (2/2)
|
||||
**Step 84:** Compile and pass (2/2)
|
||||
**Step 85:** Compile and pass (1/1)
|
||||
**Step 86:** Compile and pass (3/3)
|
||||
|
||||
---
|
||||
|
||||
@@ -340,3 +342,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
|
||||
| 2026-02-09 | Codex | Step 83: Added tree-sitter grammars for JS/TS/Java/Rust/Go and editor modes; syntax highlighting extended to 8 languages. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 84: Native file dialogs via tinyfiledialogs; FileDialog wrapper with injectable provider. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 85: Filesystem tree with .gitignore filtering and Explorer rendering. 1/1 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 86: Multi-tab editing wired via BufferManager with per-buffer state. 3/3 tests pass. |
|
||||
|
||||
@@ -3,6 +3,7 @@ project(WhetstoneEditor LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||
|
||||
# Use vcpkg-installed nlohmann_json instead of FetchContent
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
@@ -489,6 +490,9 @@ target_link_libraries(step84_test PRIVATE tinyfiledialogs)
|
||||
add_executable(step85_test tests/step85_test.cpp)
|
||||
target_include_directories(step85_test PRIVATE src)
|
||||
|
||||
add_executable(step86_test tests/step86_test.cpp)
|
||||
target_include_directories(step86_test PRIVATE src)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -31,24 +31,32 @@
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor application state
|
||||
// ---------------------------------------------------------------------------
|
||||
struct EditorState {
|
||||
struct BufferState {
|
||||
TextEditor editor;
|
||||
TextASTSync sync;
|
||||
KeybindingManager keys;
|
||||
CodeEditorWidget widget;
|
||||
EditorMode mode;
|
||||
std::string language = "python";
|
||||
std::string filePath = "(untitled)";
|
||||
std::string path = "(untitled)";
|
||||
bool modified = false;
|
||||
int cursorLine = 1;
|
||||
int cursorCol = 1;
|
||||
|
||||
// Edit buffer for ImGui InputTextMultiline — kept in sync with TextEditor
|
||||
std::string editBuf;
|
||||
bool bufDirty = true; // true = need to push editBuf → editor
|
||||
std::vector<HighlightSpan> highlights;
|
||||
bool highlightsDirty = true;
|
||||
};
|
||||
|
||||
struct EditorState {
|
||||
KeybindingManager keys;
|
||||
BufferManager buffers;
|
||||
std::map<std::string, std::unique_ptr<BufferState>> bufferStates;
|
||||
BufferState* activeBuffer = nullptr;
|
||||
|
||||
// Find/Replace state
|
||||
bool showFind = false;
|
||||
@@ -60,12 +68,7 @@ struct EditorState {
|
||||
int bottomTab = 0; // 0=Output, 1=AST, 2=Highlighted
|
||||
std::string outputLog;
|
||||
|
||||
// Highlight cache (recomputed on text change)
|
||||
std::vector<HighlightSpan> highlights;
|
||||
bool highlightsDirty = true;
|
||||
|
||||
// Custom editor widget state
|
||||
CodeEditorWidget codeWidget;
|
||||
bool showWhitespace = false;
|
||||
bool showMinimap = false;
|
||||
std::string workspaceRoot;
|
||||
@@ -73,6 +76,45 @@ struct EditorState {
|
||||
FileNode fileTreeRoot;
|
||||
bool fileTreeDirty = true;
|
||||
|
||||
BufferState* active() { return activeBuffer; }
|
||||
|
||||
std::string makeUntitledName() const {
|
||||
if (!buffers.hasBuffer("(untitled)")) return "(untitled)";
|
||||
int i = 1;
|
||||
while (true) {
|
||||
std::string name = "(untitled-" + std::to_string(i) + ")";
|
||||
if (!buffers.hasBuffer(name)) return name;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
void createBuffer(const std::string& path, const std::string& content, const std::string& language) {
|
||||
if (buffers.hasBuffer(path)) {
|
||||
buffers.switchToBuffer(path);
|
||||
activeBuffer = bufferStates[path].get();
|
||||
return;
|
||||
}
|
||||
auto state = std::make_unique<BufferState>();
|
||||
state->path = path;
|
||||
state->language = language;
|
||||
state->mode.setLanguage(language);
|
||||
state->editor.setContent(content, language);
|
||||
state->sync.setText(content, language);
|
||||
state->sync.syncNow();
|
||||
state->editBuf = content;
|
||||
state->highlightsDirty = true;
|
||||
state->modified = false;
|
||||
buffers.openBuffer(path, content, language);
|
||||
activeBuffer = state.get();
|
||||
bufferStates[path] = std::move(state);
|
||||
}
|
||||
|
||||
void switchToBuffer(const std::string& path) {
|
||||
if (!buffers.hasBuffer(path)) return;
|
||||
buffers.switchToBuffer(path);
|
||||
activeBuffer = bufferStates[path].get();
|
||||
}
|
||||
|
||||
void init() {
|
||||
std::string defaultContent =
|
||||
"def add(x, y):\n"
|
||||
@@ -90,63 +132,62 @@ struct EditorState {
|
||||
" total = total + item\n"
|
||||
" return total\n";
|
||||
|
||||
editor.setContent(defaultContent, language);
|
||||
mode.setLanguage(language);
|
||||
sync.setText(defaultContent, language);
|
||||
sync.syncNow();
|
||||
editBuf = defaultContent;
|
||||
highlightsDirty = true;
|
||||
createBuffer(makeUntitledName(), defaultContent, "python");
|
||||
outputLog = "Whetstone Editor ready.\n";
|
||||
workspaceRoot = std::filesystem::current_path().string();
|
||||
fileTreeDirty = true;
|
||||
}
|
||||
|
||||
void setLanguage(const std::string& lang) {
|
||||
language = lang;
|
||||
mode.setLanguage(lang);
|
||||
editor.setContent(editBuf, lang);
|
||||
sync.setText(editBuf, lang);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
if (!active()) return;
|
||||
active()->language = lang;
|
||||
active()->mode.setLanguage(lang);
|
||||
active()->editor.setContent(active()->editBuf, lang);
|
||||
active()->sync.setText(active()->editBuf, lang);
|
||||
active()->sync.syncNow();
|
||||
active()->highlightsDirty = true;
|
||||
}
|
||||
|
||||
// Called after editBuf changes (from ImGui input)
|
||||
void onTextChanged() {
|
||||
editor.setContent(editBuf, language);
|
||||
sync.setText(editBuf, language);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
modified = true;
|
||||
if (!active()) return;
|
||||
active()->editor.setContent(active()->editBuf, active()->language);
|
||||
active()->sync.setText(active()->editBuf, active()->language);
|
||||
active()->sync.syncNow();
|
||||
active()->highlightsDirty = true;
|
||||
active()->modified = true;
|
||||
}
|
||||
|
||||
void doUndo() {
|
||||
editor.undo();
|
||||
editBuf = editor.getContent();
|
||||
sync.setText(editBuf, language);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
if (!active()) return;
|
||||
active()->editor.undo();
|
||||
active()->editBuf = active()->editor.getContent();
|
||||
active()->sync.setText(active()->editBuf, active()->language);
|
||||
active()->sync.syncNow();
|
||||
active()->highlightsDirty = true;
|
||||
}
|
||||
|
||||
void doRedo() {
|
||||
editor.redo();
|
||||
editBuf = editor.getContent();
|
||||
sync.setText(editBuf, language);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
if (!active()) return;
|
||||
active()->editor.redo();
|
||||
active()->editBuf = active()->editor.getContent();
|
||||
active()->sync.setText(active()->editBuf, active()->language);
|
||||
active()->sync.syncNow();
|
||||
active()->highlightsDirty = true;
|
||||
}
|
||||
|
||||
void doFind() {
|
||||
if (!active()) return;
|
||||
if (strlen(findBuf) == 0) return;
|
||||
int pos = editor.find(findBuf, lastFindPos);
|
||||
int pos = active()->editor.find(findBuf, lastFindPos);
|
||||
if (pos >= 0) {
|
||||
lastFindPos = pos + 1;
|
||||
// Calculate line/col from position
|
||||
int line = 1, col = 1;
|
||||
for (int i = 0; i < pos && i < (int)editBuf.size(); ++i) {
|
||||
if (editBuf[i] == '\n') { ++line; col = 1; } else { ++col; }
|
||||
for (int i = 0; i < pos && i < (int)active()->editBuf.size(); ++i) {
|
||||
if (active()->editBuf[i] == '\n') { ++line; col = 1; } else { ++col; }
|
||||
}
|
||||
cursorLine = line;
|
||||
cursorCol = col;
|
||||
active()->cursorLine = line;
|
||||
active()->cursorCol = col;
|
||||
outputLog += "Found \"" + std::string(findBuf) + "\" at line " +
|
||||
std::to_string(line) + ", col " + std::to_string(col) + "\n";
|
||||
} else {
|
||||
@@ -156,28 +197,30 @@ struct EditorState {
|
||||
}
|
||||
|
||||
void doReplaceAll() {
|
||||
if (!active()) return;
|
||||
if (strlen(findBuf) == 0) return;
|
||||
int count = editor.replaceAll(findBuf, replaceBuf);
|
||||
int count = active()->editor.replaceAll(findBuf, replaceBuf);
|
||||
if (count > 0) {
|
||||
editBuf = editor.getContent();
|
||||
sync.setText(editBuf, language);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
modified = true;
|
||||
active()->editBuf = active()->editor.getContent();
|
||||
active()->sync.setText(active()->editBuf, active()->language);
|
||||
active()->sync.syncNow();
|
||||
active()->highlightsDirty = true;
|
||||
active()->modified = true;
|
||||
}
|
||||
outputLog += "Replaced " + std::to_string(count) + " occurrence(s).\n";
|
||||
}
|
||||
|
||||
void doSave() {
|
||||
if (filePath == "(untitled)") return;
|
||||
std::ofstream out(filePath);
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) return;
|
||||
std::ofstream out(active()->path);
|
||||
if (out.is_open()) {
|
||||
out << editBuf;
|
||||
out << active()->editBuf;
|
||||
out.close();
|
||||
modified = false;
|
||||
outputLog += "Saved: " + filePath + "\n";
|
||||
active()->modified = false;
|
||||
outputLog += "Saved: " + active()->path + "\n";
|
||||
} else {
|
||||
outputLog += "Error saving: " + filePath + "\n";
|
||||
outputLog += "Error saving: " + active()->path + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +229,8 @@ struct EditorState {
|
||||
if (in.is_open()) {
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
editBuf = ss.str();
|
||||
filePath = path;
|
||||
// Detect language from extension
|
||||
std::string content = ss.str();
|
||||
std::string language = "python";
|
||||
if (path.size() > 3 && path.substr(path.size() - 3) == ".py")
|
||||
language = "python";
|
||||
else if (path.size() > 4 && path.substr(path.size() - 4) == ".cpp")
|
||||
@@ -207,39 +249,38 @@ struct EditorState {
|
||||
language = "rust";
|
||||
else if (path.size() > 3 && path.substr(path.size() - 3) == ".go")
|
||||
language = "go";
|
||||
editor.setContent(editBuf, language);
|
||||
sync.setText(editBuf, language);
|
||||
sync.syncNow();
|
||||
highlightsDirty = true;
|
||||
modified = false;
|
||||
createBuffer(path, content, language);
|
||||
outputLog += "Opened: " + path + "\n";
|
||||
} else {
|
||||
outputLog += "Error opening: " + path + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void updateHighlights() {
|
||||
if (!active()) return;
|
||||
if (!active()->highlightsDirty) return;
|
||||
active()->highlights = SyntaxHighlighter::highlight(active()->editBuf, active()->language);
|
||||
active()->highlightsDirty = false;
|
||||
}
|
||||
|
||||
void updateCursorPos(int bytePos) {
|
||||
if (!active()) return;
|
||||
active()->cursorLine = 1;
|
||||
active()->cursorCol = 1;
|
||||
for (int i = 0; i < bytePos && i < (int)active()->editBuf.size(); ++i) {
|
||||
if (active()->editBuf[i] == '\n') { ++active()->cursorLine; active()->cursorCol = 1; }
|
||||
else { ++active()->cursorCol; }
|
||||
}
|
||||
}
|
||||
|
||||
void refreshFileTree() {
|
||||
if (!fileTreeDirty) return;
|
||||
fileTreeRoot = fileTree.build(workspaceRoot);
|
||||
fileTreeDirty = false;
|
||||
}
|
||||
|
||||
void updateHighlights() {
|
||||
if (!highlightsDirty) return;
|
||||
highlights = SyntaxHighlighter::highlight(editBuf, language);
|
||||
highlightsDirty = false;
|
||||
}
|
||||
|
||||
void updateCursorPos(int bytePos) {
|
||||
cursorLine = 1;
|
||||
cursorCol = 1;
|
||||
for (int i = 0; i < bytePos && i < (int)editBuf.size(); ++i) {
|
||||
if (editBuf[i] == '\n') { ++cursorLine; cursorCol = 1; }
|
||||
else { ++cursorCol; }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ImGui InputTextMultiline with std::string resize callback
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -539,10 +580,8 @@ int main(int, char**) {
|
||||
else if (action == "search.find") state.showFind = !state.showFind;
|
||||
else if (action == "file.save") state.doSave();
|
||||
else if (action == "file.new") {
|
||||
state.editBuf.clear();
|
||||
state.onTextChanged();
|
||||
state.filePath = "(untitled)";
|
||||
state.modified = false;
|
||||
std::string lang = state.active() ? state.active()->language : "python";
|
||||
state.createBuffer(state.makeUntitledName(), "", lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,10 +617,8 @@ int main(int, char**) {
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
if (ImGui::MenuItem("New", state.keys.getBinding("file.new").toString().c_str()))
|
||||
{
|
||||
state.editBuf.clear();
|
||||
state.onTextChanged();
|
||||
state.filePath = "(untitled)";
|
||||
state.modified = false;
|
||||
std::string lang = state.active() ? state.active()->language : "python";
|
||||
state.createBuffer(state.makeUntitledName(), "", lang);
|
||||
}
|
||||
if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str()))
|
||||
{
|
||||
@@ -601,10 +638,12 @@ int main(int, char**) {
|
||||
}
|
||||
if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str()))
|
||||
{
|
||||
if (state.filePath == "(untitled)") {
|
||||
if (!state.active()) {
|
||||
// no-op
|
||||
} else if (state.active()->path.rfind("(untitled", 0) == 0) {
|
||||
auto path = FileDialog::saveFile({"Save File", {"*.*"}, lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
state.filePath = path;
|
||||
state.active()->path = path;
|
||||
lastDialogPath = path;
|
||||
state.doSave();
|
||||
}
|
||||
@@ -618,10 +657,10 @@ int main(int, char**) {
|
||||
}
|
||||
if (ImGui::BeginMenu("Edit")) {
|
||||
if (ImGui::MenuItem("Undo", state.keys.getBinding("edit.undo").toString().c_str(),
|
||||
false, state.editor.canUndo()))
|
||||
false, state.active() ? state.active()->editor.canUndo() : false))
|
||||
state.doUndo();
|
||||
if (ImGui::MenuItem("Redo", state.keys.getBinding("edit.redo").toString().c_str(),
|
||||
false, state.editor.canRedo()))
|
||||
false, state.active() ? state.active()->editor.canRedo() : false))
|
||||
state.doRedo();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Find/Replace", state.keys.getBinding("search.find").toString().c_str()))
|
||||
@@ -634,21 +673,21 @@ int main(int, char**) {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Language")) {
|
||||
if (ImGui::MenuItem("Python", nullptr, state.language == "python"))
|
||||
if (ImGui::MenuItem("Python", nullptr, state.active() && state.active()->language == "python"))
|
||||
state.setLanguage("python");
|
||||
if (ImGui::MenuItem("C++", nullptr, state.language == "cpp"))
|
||||
if (ImGui::MenuItem("C++", nullptr, state.active() && state.active()->language == "cpp"))
|
||||
state.setLanguage("cpp");
|
||||
if (ImGui::MenuItem("Elisp", nullptr, state.language == "elisp"))
|
||||
if (ImGui::MenuItem("Elisp", nullptr, state.active() && state.active()->language == "elisp"))
|
||||
state.setLanguage("elisp");
|
||||
if (ImGui::MenuItem("JavaScript", nullptr, state.language == "javascript"))
|
||||
if (ImGui::MenuItem("JavaScript", nullptr, state.active() && state.active()->language == "javascript"))
|
||||
state.setLanguage("javascript");
|
||||
if (ImGui::MenuItem("TypeScript", nullptr, state.language == "typescript"))
|
||||
if (ImGui::MenuItem("TypeScript", nullptr, state.active() && state.active()->language == "typescript"))
|
||||
state.setLanguage("typescript");
|
||||
if (ImGui::MenuItem("Java", nullptr, state.language == "java"))
|
||||
if (ImGui::MenuItem("Java", nullptr, state.active() && state.active()->language == "java"))
|
||||
state.setLanguage("java");
|
||||
if (ImGui::MenuItem("Rust", nullptr, state.language == "rust"))
|
||||
if (ImGui::MenuItem("Rust", nullptr, state.active() && state.active()->language == "rust"))
|
||||
state.setLanguage("rust");
|
||||
if (ImGui::MenuItem("Go", nullptr, state.language == "go"))
|
||||
if (ImGui::MenuItem("Go", nullptr, state.active() && state.active()->language == "go"))
|
||||
state.setLanguage("go");
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
@@ -672,11 +711,15 @@ int main(int, char**) {
|
||||
ImGui::PushFont(uiFont);
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "OPEN EDITORS");
|
||||
ImGui::Separator();
|
||||
// Show current file as a selectable
|
||||
{
|
||||
std::string label = state.filePath;
|
||||
if (state.modified) label += " *";
|
||||
ImGui::Selectable(label.c_str(), true);
|
||||
// Show open buffers
|
||||
for (const auto& path : state.buffers.getOpenBuffers()) {
|
||||
auto* buf = state.bufferStates[path].get();
|
||||
std::string label = path;
|
||||
if (buf && buf->modified) label += " *";
|
||||
bool selected = state.active() && state.active()->path == path;
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
state.switchToBuffer(path);
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "FILES");
|
||||
@@ -720,28 +763,46 @@ int main(int, char**) {
|
||||
|
||||
// Tab bar for the file
|
||||
if (ImGui::BeginTabBar("EditorTabs")) {
|
||||
std::string tabLabel = state.filePath;
|
||||
if (state.modified) tabLabel += " *";
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str())) {
|
||||
// Editable text area fills available space
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
avail.y -= 4; // small margin
|
||||
auto openBuffers = state.buffers.getOpenBuffers();
|
||||
for (const auto& path : openBuffers) {
|
||||
auto* buf = state.bufferStates[path].get();
|
||||
if (!buf) continue;
|
||||
std::string tabLabel = path;
|
||||
if (buf->modified) tabLabel += " *";
|
||||
bool open = true;
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) {
|
||||
if (!state.active() || state.active()->path != path) {
|
||||
state.switchToBuffer(path);
|
||||
}
|
||||
// Editable text area fills available space
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
avail.y -= 4; // small margin
|
||||
|
||||
state.updateHighlights();
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.mode = &state.mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
state.updateHighlights();
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.mode = &buf->mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
|
||||
CodeEditorResult res = state.codeWidget.render("##editor",
|
||||
state.editBuf, state.highlights, opts, avail, monoFont);
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
CodeEditorResult res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, avail, monoFont);
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (!open) {
|
||||
state.buffers.closeBuffer(path);
|
||||
state.bufferStates.erase(path);
|
||||
if (state.buffers.bufferCount() > 0) {
|
||||
state.switchToBuffer(state.buffers.getOpenBuffers().front());
|
||||
} else {
|
||||
state.activeBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
@@ -772,7 +833,7 @@ int main(int, char**) {
|
||||
if (ImGui::BeginTabItem("AST")) {
|
||||
ImGui::PushFont(monoFont);
|
||||
ImGui::BeginChild("##astScroll", ImVec2(0, 0), false);
|
||||
Module* ast = state.sync.getAST();
|
||||
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
|
||||
if (ast) {
|
||||
// Show basic AST info
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f),
|
||||
@@ -811,7 +872,8 @@ int main(int, char**) {
|
||||
ImGui::PushFont(monoFont);
|
||||
ImGui::BeginChild("##hlScroll", ImVec2(0, 0), false);
|
||||
state.updateHighlights();
|
||||
RenderHighlightedText(state.editBuf, state.highlights);
|
||||
if (state.active())
|
||||
RenderHighlightedText(state.active()->editBuf, state.active()->highlights);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopFont();
|
||||
ImGui::EndTabItem();
|
||||
@@ -821,16 +883,16 @@ int main(int, char**) {
|
||||
if (ImGui::BeginTabItem("Generated")) {
|
||||
ImGui::PushFont(monoFont);
|
||||
ImGui::BeginChild("##genScroll", ImVec2(0, 0), false);
|
||||
Module* ast = state.sync.getAST();
|
||||
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
|
||||
if (ast) {
|
||||
std::string generated;
|
||||
if (state.language == "python") {
|
||||
if (state.active()->language == "python") {
|
||||
PythonGenerator gen;
|
||||
generated = gen.generate(ast);
|
||||
} else if (state.language == "cpp") {
|
||||
} else if (state.active()->language == "cpp") {
|
||||
CppGenerator gen;
|
||||
generated = gen.generate(ast);
|
||||
} else if (state.language == "elisp") {
|
||||
} else if (state.active()->language == "elisp") {
|
||||
ElispGenerator gen;
|
||||
generated = gen.generate(ast);
|
||||
}
|
||||
@@ -866,11 +928,17 @@ int main(int, char**) {
|
||||
ImGui::PushFont(uiFont);
|
||||
|
||||
// Left side: line/col
|
||||
ImGui::Text("Ln %d, Col %d", state.cursorLine, state.cursorCol);
|
||||
if (state.active())
|
||||
ImGui::Text("Ln %d, Col %d", state.active()->cursorLine, state.active()->cursorCol);
|
||||
else
|
||||
ImGui::Text("Ln -, Col -");
|
||||
ImGui::SameLine(0, 30);
|
||||
|
||||
// Language
|
||||
ImGui::Text("%s", state.language.c_str());
|
||||
if (state.active())
|
||||
ImGui::Text("%s", state.active()->language.c_str());
|
||||
else
|
||||
ImGui::Text("-");
|
||||
ImGui::SameLine(0, 30);
|
||||
|
||||
// Keybinding profile
|
||||
@@ -878,7 +946,7 @@ int main(int, char**) {
|
||||
ImGui::SameLine(0, 30);
|
||||
|
||||
// Modified indicator
|
||||
if (state.modified)
|
||||
if (state.active() && state.active()->modified)
|
||||
ImGui::Text("Modified");
|
||||
else
|
||||
ImGui::Text("Saved");
|
||||
|
||||
36
editor/tests/step86_test.cpp
Normal file
36
editor/tests/step86_test.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// Step 86 TDD Test: Multi-tab editing (BufferManager)
|
||||
//
|
||||
// Tests:
|
||||
// 1. Opening buffers sets active
|
||||
// 2. Switching active buffer works
|
||||
// 3. Closing active buffer selects another
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include "BufferManager.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
BufferManager bm;
|
||||
bm.openBuffer("a.py", "print(1)", "python");
|
||||
assert(bm.getActiveBufferPath() == "a.py");
|
||||
bm.openBuffer("b.py", "print(2)", "python");
|
||||
assert(bm.getActiveBufferPath() == "b.py");
|
||||
std::cout << "Test 1 PASS: Opening buffers sets active" << std::endl;
|
||||
++passed;
|
||||
|
||||
bm.switchToBuffer("a.py");
|
||||
assert(bm.getActiveBufferPath() == "a.py");
|
||||
std::cout << "Test 2 PASS: Switching active buffer" << std::endl;
|
||||
++passed;
|
||||
|
||||
bm.closeBuffer("a.py");
|
||||
assert(bm.getActiveBufferPath() == "b.py");
|
||||
std::cout << "Test 3 PASS: Closing active selects another" << std::endl;
|
||||
++passed;
|
||||
|
||||
std::cout << "\n=== Step 86 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user