Step 146: add Emacs buffer bridge
This commit is contained in:
@@ -493,3 +493,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Step 143: Elisp function discovery via apropos/describe-function, Emacs function indexing into ExternalModule + LibraryIndex, and elisp primitive updates. 7/7 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 144: Emacs keybinding integration (prefix handling, M-x minibuffer, mode line display, key-binding lookup via daemon). 11/11 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 145: Org-mode rendering with headings/blocks, editable source blocks, inline results, org temp runner, and tree-sitter-org integration. 11/11 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 146: Emacs-Whetstone bridge with buffer pull/push, Emacs frame opening, and sync commands. 5/5 tests pass. |
|
||||
|
||||
@@ -819,6 +819,10 @@ add_executable(step145_test tests/step145_test.cpp)
|
||||
target_include_directories(step145_test PRIVATE src)
|
||||
target_link_libraries(step145_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui tree_sitter_org)
|
||||
|
||||
add_executable(step146_test tests/step146_test.cpp)
|
||||
target_include_directories(step146_test PRIVATE src)
|
||||
target_link_libraries(step146_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -188,6 +188,7 @@ struct EditorState {
|
||||
EmacsFunctionIndex emacsFunctionIndex;
|
||||
bool emacsFunctionIndexDirty = true;
|
||||
EmacsKeybindingState emacsKeys;
|
||||
bool showEmacsBridgePanel = false;
|
||||
bool showOrgPanel = true;
|
||||
OrgDocumentState orgDoc;
|
||||
int orgTempCounter = 0;
|
||||
@@ -1434,6 +1435,48 @@ struct EditorState {
|
||||
updateEmacsModeLine(emacsKeys, emacs, nowSeconds, outputLog);
|
||||
}
|
||||
|
||||
void pullFromEmacs() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before syncing.\n";
|
||||
return;
|
||||
}
|
||||
std::string text = emacs.getBufferText(active()->path);
|
||||
if (!emacs.getLastError().empty() && text == "error") {
|
||||
outputLog += "[emacs] " + emacs.getLastError() + "\n";
|
||||
return;
|
||||
}
|
||||
active()->editBuf = text;
|
||||
onTextChanged();
|
||||
outputLog += "[emacs] Pulled buffer from Emacs.\n";
|
||||
}
|
||||
|
||||
void pushToEmacs() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before syncing.\n";
|
||||
return;
|
||||
}
|
||||
if (!emacs.setBufferText(active()->path, active()->editBuf)) {
|
||||
outputLog += "[emacs] " + emacs.getLastError() + "\n";
|
||||
return;
|
||||
}
|
||||
outputLog += "[emacs] Pushed buffer to Emacs.\n";
|
||||
}
|
||||
|
||||
void openInEmacsFrame() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before opening in Emacs.\n";
|
||||
return;
|
||||
}
|
||||
if (!emacs.openFileInEmacsFrame(active()->path)) {
|
||||
outputLog += "[emacs] " + emacs.getLastError() + "\n";
|
||||
return;
|
||||
}
|
||||
outputLog += "[emacs] Opened in Emacs frame.\n";
|
||||
}
|
||||
|
||||
std::string buildRunCommand(const std::string& path,
|
||||
const std::string& language,
|
||||
bool buildOnly) const {
|
||||
|
||||
@@ -106,6 +106,20 @@ public:
|
||||
static std::string modeLine() {
|
||||
return "(format-mode-line mode-line-format)";
|
||||
}
|
||||
|
||||
// Build an Elisp expression to read a buffer for a given file
|
||||
static std::string bufferStringForFile(const std::string& path) {
|
||||
return "(with-current-buffer (find-file-noselect \"" +
|
||||
escapeString(path) + "\") (buffer-string))";
|
||||
}
|
||||
|
||||
// Build an Elisp expression to replace a buffer's contents and save
|
||||
static std::string replaceBufferForFile(const std::string& path, const std::string& text) {
|
||||
return "(with-current-buffer (find-file-noselect \"" + escapeString(path) + "\") "
|
||||
"(erase-buffer) "
|
||||
"(insert \"" + escapeString(text) + "\") "
|
||||
"(save-buffer))";
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -213,6 +227,27 @@ public:
|
||||
return output;
|
||||
}
|
||||
|
||||
std::string getBufferText(const std::string& path) {
|
||||
return sendCommand(ElispCommandBuilder::bufferStringForFile(path));
|
||||
}
|
||||
|
||||
bool setBufferText(const std::string& path, const std::string& text) {
|
||||
std::string result = sendCommand(ElispCommandBuilder::replaceBufferForFile(path, text));
|
||||
if (!lastError_.empty() && result == "error") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool openFileInEmacsFrame(const std::string& path) {
|
||||
lastError_.clear();
|
||||
std::string cmd = emacsclientPath_ + " -c -n \"" + path + "\"";
|
||||
int code = runCommand(cmd);
|
||||
if (code != 0) {
|
||||
lastError_ = "emacsclient failed to open frame";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-restart daemon if it died
|
||||
bool ensureDaemon() {
|
||||
if (daemonRunning_ && isDaemonAlive()) return true;
|
||||
@@ -300,6 +335,12 @@ public:
|
||||
lastSentCommand_ = elispCommand;
|
||||
|
||||
// Simulate responses for known commands
|
||||
if (elispCommand.find("buffer-string") != std::string::npos) {
|
||||
return "mock-buffer";
|
||||
}
|
||||
if (elispCommand.find("erase-buffer") != std::string::npos) {
|
||||
return "t";
|
||||
}
|
||||
if (elispCommand.find("find-file") != std::string::npos) {
|
||||
return "t"; // success
|
||||
}
|
||||
@@ -356,7 +397,6 @@ public:
|
||||
if (elispCommand.find("format-mode-line") != std::string::npos) {
|
||||
return "Emacs: Fundamental (Whetstone)";
|
||||
}
|
||||
|
||||
// Unknown command — simulate error
|
||||
lastError_ = "void-function";
|
||||
return "error";
|
||||
@@ -372,10 +412,18 @@ public:
|
||||
// Test helper: get the last command sent
|
||||
std::string getLastSentCommand() const { return lastSentCommand_; }
|
||||
std::string getLastInitPath() const { return lastInitPath_; }
|
||||
std::string getLastRunCommand() const { return lastRunCommand_; }
|
||||
|
||||
protected:
|
||||
int runCommand(const std::string& cmd) const override {
|
||||
lastRunCommand_ = cmd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string lastError_;
|
||||
std::string lastSentCommand_;
|
||||
std::string lastInitPath_;
|
||||
mutable std::string lastRunCommand_;
|
||||
};
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ int main(int, char**) {
|
||||
ImGui::MenuItem("Libraries", nullptr, &state.showLibraryBrowserPanel);
|
||||
ImGui::MenuItem("Compose", nullptr, &state.showCompositionPanel);
|
||||
ImGui::MenuItem("Emacs Packages", nullptr, &state.showEmacsPackagesPanel);
|
||||
ImGui::MenuItem("Emacs Bridge", nullptr, &state.showEmacsBridgePanel);
|
||||
ImGui::MenuItem("Settings", nullptr, &state.showSettingsPanel);
|
||||
ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings);
|
||||
if (state.active()) {
|
||||
@@ -674,6 +675,30 @@ int main(int, char**) {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (state.showEmacsBridgePanel) {
|
||||
ImGui::Begin("Emacs Bridge", &state.showEmacsBridgePanel);
|
||||
ImGui::PushFont(uiFont);
|
||||
if (state.active()) {
|
||||
ImGui::Text("Active file: %s", state.active()->path.c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("(no active buffer)");
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Open Emacs Frame")) {
|
||||
state.openInEmacsFrame();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Pull From Emacs")) {
|
||||
state.pullFromEmacs();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Push To Emacs")) {
|
||||
state.pushToEmacs();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (state.layoutPreset == LayoutPreset::Emacs && state.emacsKeys.minibufferActive) {
|
||||
ImGuiWindowFlags mbFlags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
|
||||
38
editor/tests/step146_test.cpp
Normal file
38
editor/tests/step146_test.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Step 146 TDD Test: Emacs-Whetstone bridge
|
||||
#include "EmacsIntegration.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
MockEmacsConnection mock;
|
||||
std::string path = "C:/tmp/example.el";
|
||||
|
||||
std::string text = mock.getBufferText(path);
|
||||
expect(text == "mock-buffer", "buffer text pulled", passed, failed);
|
||||
|
||||
bool setOk = mock.setBufferText(path, "(message \"hi\")");
|
||||
expect(setOk, "buffer text pushed", passed, failed);
|
||||
expect(mock.getLastSentCommand().find("erase-buffer") != std::string::npos,
|
||||
"replace buffer command", passed, failed);
|
||||
|
||||
bool frameOk = mock.openFileInEmacsFrame(path);
|
||||
expect(frameOk, "open emacs frame", passed, failed);
|
||||
expect(mock.getLastRunCommand().find("emacsclient") != std::string::npos,
|
||||
"emacsclient command", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 146 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ functionality through the Whetstone UI.
|
||||
Results rendered inline (like Jupyter notebooks).
|
||||
*New:* tree-sitter-org grammar, `OrgMode.h`
|
||||
|
||||
- [ ] **Step 146: Emacs-Whetstone bridge**
|
||||
- [x] **Step 146: Emacs-Whetstone bridge**
|
||||
Bidirectional sync: Emacs buffers ↔ Whetstone buffers. When the user
|
||||
has the Emacs layout active, they can switch to pure Emacs (daemon
|
||||
connected to a graphical frame) for operations Whetstone doesn't
|
||||
|
||||
Reference in New Issue
Block a user