Step 144: add Emacs keybinding integration
This commit is contained in:
@@ -790,6 +790,10 @@ add_executable(step143_test tests/step143_test.cpp)
|
||||
target_include_directories(step143_test PRIVATE src)
|
||||
target_link_libraries(step143_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
|
||||
|
||||
add_executable(step144_test tests/step144_test.cpp)
|
||||
target_include_directories(step144_test PRIVATE src)
|
||||
target_link_libraries(step144_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "EmacsIntegration.h"
|
||||
#include "EmacsPackageBrowser.h"
|
||||
#include "EmacsFunctionDiscovery.h"
|
||||
#include "EmacsKeybinding.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
@@ -185,6 +186,7 @@ struct EditorState {
|
||||
EmacsPackageBrowserState emacsPackages;
|
||||
EmacsFunctionIndex emacsFunctionIndex;
|
||||
bool emacsFunctionIndexDirty = true;
|
||||
EmacsKeybindingState emacsKeys;
|
||||
struct LibraryIndexRequest {
|
||||
std::string name;
|
||||
std::string version;
|
||||
@@ -1369,6 +1371,16 @@ struct EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
bool handleEmacsKeyChord(const std::string& chord) {
|
||||
if (layoutPreset != LayoutPreset::Emacs) return false;
|
||||
return emacsHandleKeySequence(emacsKeys, emacs, chord, outputLog);
|
||||
}
|
||||
|
||||
void refreshEmacsModeLine(double nowSeconds) {
|
||||
if (layoutPreset != LayoutPreset::Emacs) return;
|
||||
updateEmacsModeLine(emacsKeys, emacs, nowSeconds, outputLog);
|
||||
}
|
||||
|
||||
std::string buildRunCommand(const std::string& path,
|
||||
const std::string& language,
|
||||
bool buildOnly) const {
|
||||
|
||||
@@ -82,6 +82,30 @@ public:
|
||||
"(doc (or (documentation sym t) \"\"))) "
|
||||
"(concat (if args (prin1-to-string args) \"\") \"\\n\" doc))";
|
||||
}
|
||||
|
||||
// Build an Elisp expression to resolve a key binding to a command name
|
||||
static std::string keyBinding(const std::string& keySequence) {
|
||||
return "(let ((cmd (key-binding (kbd \"" + escapeString(keySequence) + "\")))) "
|
||||
"(if cmd (symbol-name cmd) \"\"))";
|
||||
}
|
||||
|
||||
// Build an Elisp expression to call a command interactively
|
||||
static std::string callInteractive(const std::string& command) {
|
||||
return "(call-interactively '" + escapeString(command) + ")";
|
||||
}
|
||||
|
||||
// Build an Elisp expression to run an extended command by name
|
||||
static std::string executeExtendedCommand(const std::string& command) {
|
||||
std::string sym = escapeString(command);
|
||||
return "(if (commandp '" + sym + ") "
|
||||
"(call-interactively '" + sym + ") "
|
||||
"(concat \"Unknown command: \" \"" + sym + "\"))";
|
||||
}
|
||||
|
||||
// Build an Elisp expression for the current mode line
|
||||
static std::string modeLine() {
|
||||
return "(format-mode-line mode-line-format)";
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -317,6 +341,21 @@ public:
|
||||
}
|
||||
return "()\nEmacs function.";
|
||||
}
|
||||
if (elispCommand.find("key-binding") != std::string::npos) {
|
||||
if (elispCommand.find("C-x C-s") != std::string::npos) {
|
||||
return "save-buffer";
|
||||
}
|
||||
if (elispCommand.find("C-x C-f") != std::string::npos) {
|
||||
return "find-file";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (elispCommand.find("call-interactively") != std::string::npos) {
|
||||
return "t";
|
||||
}
|
||||
if (elispCommand.find("format-mode-line") != std::string::npos) {
|
||||
return "Emacs: Fundamental (Whetstone)";
|
||||
}
|
||||
|
||||
// Unknown command — simulate error
|
||||
lastError_ = "void-function";
|
||||
|
||||
96
editor/src/EmacsKeybinding.h
Normal file
96
editor/src/EmacsKeybinding.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
#include "EmacsIntegration.h"
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
struct EmacsKeybindingState {
|
||||
std::string prefix;
|
||||
std::string lastCommand;
|
||||
std::string lastMessage;
|
||||
bool minibufferActive = false;
|
||||
char minibufferBuf[256] = {};
|
||||
std::string minibufferPrompt = "M-x";
|
||||
std::string modeLine;
|
||||
double lastModeQuery = 0.0;
|
||||
};
|
||||
|
||||
static inline std::string emacsTrim(std::string value) {
|
||||
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
|
||||
value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace));
|
||||
value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end());
|
||||
return value;
|
||||
}
|
||||
|
||||
static inline bool emacsIsPrefixKey(const std::string& chord) {
|
||||
return chord == "C-x" || chord == "C-c";
|
||||
}
|
||||
|
||||
static inline bool emacsHandleKeySequence(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
const std::string& chord,
|
||||
std::string& logOut) {
|
||||
if (chord.empty()) return false;
|
||||
if (chord == "M-x") {
|
||||
state.minibufferActive = true;
|
||||
state.minibufferPrompt = "M-x";
|
||||
state.minibufferBuf[0] = '\0';
|
||||
state.prefix.clear();
|
||||
return true;
|
||||
}
|
||||
if (emacsIsPrefixKey(chord)) {
|
||||
state.prefix = chord;
|
||||
state.lastMessage = chord;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string sequence = state.prefix.empty() ? chord : (state.prefix + " " + chord);
|
||||
state.prefix.clear();
|
||||
|
||||
std::string cmd = emacs.sendCommand(ElispCommandBuilder::keyBinding(sequence));
|
||||
if (cmd.empty()) {
|
||||
logOut += "[emacs] Unbound: " + sequence + "\n";
|
||||
return true;
|
||||
}
|
||||
state.lastCommand = cmd;
|
||||
std::string result = emacs.sendCommand(ElispCommandBuilder::callInteractive(cmd));
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
} else {
|
||||
logOut += "[emacs] " + cmd + "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool emacsExecuteMinibuffer(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
std::string& logOut) {
|
||||
std::string cmd = emacsTrim(state.minibufferBuf);
|
||||
if (cmd.empty()) {
|
||||
state.minibufferActive = false;
|
||||
return false;
|
||||
}
|
||||
std::string result = emacs.sendCommand(ElispCommandBuilder::executeExtendedCommand(cmd));
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
} else {
|
||||
logOut += "[emacs] M-x " + cmd + "\n";
|
||||
}
|
||||
state.minibufferActive = false;
|
||||
state.minibufferBuf[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void updateEmacsModeLine(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
double nowSeconds,
|
||||
std::string& logOut) {
|
||||
if (nowSeconds - state.lastModeQuery < 1.0) return;
|
||||
std::string mode = emacs.sendCommand(ElispCommandBuilder::modeLine());
|
||||
if (!emacs.getLastError().empty() && mode == "error") {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
return;
|
||||
}
|
||||
if (!mode.empty()) state.modeLine = mode;
|
||||
state.lastModeQuery = nowSeconds;
|
||||
}
|
||||
@@ -11,11 +11,52 @@
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_opengl.h>
|
||||
#include <cctype>
|
||||
|
||||
#include "EditorState.h"
|
||||
#include "EditorUtils.h"
|
||||
#include "CompletionUtils.h"
|
||||
|
||||
static std::string emacsChordForEvent(SDL_Keycode sym, SDL_Keymod mods) {
|
||||
std::string base;
|
||||
if (sym >= SDLK_a && sym <= SDLK_z) {
|
||||
char c = (char)('a' + (sym - SDLK_a));
|
||||
if (mods & KMOD_SHIFT) c = (char)std::toupper(c);
|
||||
base.push_back(c);
|
||||
} else if (sym >= SDLK_0 && sym <= SDLK_9) {
|
||||
base.push_back((char)('0' + (sym - SDLK_0)));
|
||||
} else if (sym == SDLK_SPACE) {
|
||||
base = "SPC";
|
||||
} else if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER) {
|
||||
base = "RET";
|
||||
} else if (sym == SDLK_TAB) {
|
||||
base = "TAB";
|
||||
} else if (sym == SDLK_BACKSPACE) {
|
||||
base = "DEL";
|
||||
} else if (sym == SDLK_ESCAPE) {
|
||||
base = "ESC";
|
||||
} else if (sym == SDLK_MINUS) {
|
||||
base = "-";
|
||||
} else if (sym == SDLK_EQUALS) {
|
||||
base = "=";
|
||||
} else if (sym == SDLK_SLASH) {
|
||||
base = "/";
|
||||
} else if (sym == SDLK_SEMICOLON) {
|
||||
base = ";";
|
||||
} else if (sym == SDLK_PERIOD) {
|
||||
base = ".";
|
||||
}
|
||||
|
||||
if (base.empty()) return "";
|
||||
|
||||
std::string prefix;
|
||||
if (mods & KMOD_CTRL) prefix += "C-";
|
||||
if (mods & KMOD_ALT) prefix += "M-";
|
||||
if ((mods & KMOD_SHIFT) && base.size() > 1) prefix += "S-";
|
||||
|
||||
return prefix + base;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -122,6 +163,13 @@ int main(int, char**) {
|
||||
if (event.type == SDL_KEYDOWN && !io.WantTextInput) {
|
||||
int key = 0;
|
||||
auto sym = event.key.keysym.sym;
|
||||
auto sdlMod = event.key.keysym.mod;
|
||||
if (state.layoutPreset == LayoutPreset::Emacs) {
|
||||
std::string chord = emacsChordForEvent(sym, sdlMod);
|
||||
if (!chord.empty() && state.handleEmacsKeyChord(chord)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (sym >= SDLK_a && sym <= SDLK_z) key = 'A' + (sym - SDLK_a);
|
||||
else if (sym >= SDLK_0 && sym <= SDLK_9) key = '0' + (sym - SDLK_0);
|
||||
else if (sym == SDLK_EQUALS) key = '=';
|
||||
@@ -132,7 +180,6 @@ int main(int, char**) {
|
||||
else if (sym == SDLK_BACKQUOTE) key = '`';
|
||||
|
||||
int mods = WMOD_NONE;
|
||||
auto sdlMod = event.key.keysym.mod;
|
||||
if (sdlMod & KMOD_CTRL) mods |= WMOD_CTRL;
|
||||
if (sdlMod & KMOD_SHIFT) mods |= WMOD_SHIFT;
|
||||
if (sdlMod & KMOD_ALT) mods |= WMOD_ALT;
|
||||
@@ -196,6 +243,7 @@ int main(int, char**) {
|
||||
if (state.emacsFunctionIndexDirty) {
|
||||
state.updateEmacsFunctionIndex();
|
||||
}
|
||||
state.refreshEmacsModeLine(ImGui::GetTime());
|
||||
|
||||
// Start frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
@@ -624,6 +672,31 @@ int main(int, char**) {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (state.layoutPreset == LayoutPreset::Emacs && state.emacsKeys.minibufferActive) {
|
||||
ImGuiWindowFlags mbFlags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNavFocus;
|
||||
float mbHeight = ImGui::GetFrameHeight() + 10;
|
||||
ImVec2 mbPos(viewport->WorkPos.x,
|
||||
viewport->WorkPos.y + viewport->WorkSize.y - mbHeight - 24.0f);
|
||||
ImVec2 mbSize(viewport->WorkSize.x, mbHeight);
|
||||
ImGui::SetNextWindowPos(mbPos);
|
||||
ImGui::SetNextWindowSize(mbSize);
|
||||
ImGui::Begin("##Minibuffer", nullptr, mbFlags);
|
||||
ImGui::PushFont(uiFont);
|
||||
ImGui::TextUnformatted(state.emacsKeys.minibufferPrompt.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::InputText("##minibuffer",
|
||||
state.emacsKeys.minibufferBuf,
|
||||
sizeof(state.emacsKeys.minibufferBuf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
emacsExecuteMinibuffer(state.emacsKeys, state.emacs, state.outputLog);
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Find / Replace bar (floating at top of editor)
|
||||
// ---------------------------------------------------------------
|
||||
@@ -2462,6 +2535,10 @@ int main(int, char**) {
|
||||
// Keybinding profile
|
||||
ImGui::Text("Keys: %s", KeybindingManager::profileName(state.keys.getProfile()));
|
||||
ImGui::SameLine(0, 30);
|
||||
if (state.layoutPreset == LayoutPreset::Emacs && !state.emacsKeys.modeLine.empty()) {
|
||||
ImGui::Text("Mode: %s", state.emacsKeys.modeLine.c_str());
|
||||
ImGui::SameLine(0, 30);
|
||||
}
|
||||
|
||||
// Zoom
|
||||
ImGui::Text("Zoom: %d%%", zoomPercent(state.settings.getFontSize(), baseFontSize));
|
||||
|
||||
49
editor/tests/step144_test.cpp
Normal file
49
editor/tests/step144_test.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Step 144 TDD Test: Emacs keybinding deep integration
|
||||
#include "EmacsKeybinding.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;
|
||||
EmacsKeybindingState state;
|
||||
std::string log;
|
||||
|
||||
bool prefixOk = emacsHandleKeySequence(state, mock, "C-x", log);
|
||||
expect(prefixOk, "prefix handled", passed, failed);
|
||||
expect(state.prefix == "C-x", "prefix stored", passed, failed);
|
||||
|
||||
bool seqOk = emacsHandleKeySequence(state, mock, "C-s", log);
|
||||
expect(seqOk, "sequence handled", passed, failed);
|
||||
expect(state.prefix.empty(), "prefix cleared", passed, failed);
|
||||
expect(state.lastCommand == "save-buffer", "command resolved", passed, failed);
|
||||
expect(mock.getLastSentCommand().find("call-interactively") != std::string::npos,
|
||||
"call-interactively sent", passed, failed);
|
||||
|
||||
bool mxOk = emacsHandleKeySequence(state, mock, "M-x", log);
|
||||
expect(mxOk, "M-x handled", passed, failed);
|
||||
expect(state.minibufferActive, "minibuffer active", passed, failed);
|
||||
|
||||
std::snprintf(state.minibufferBuf, sizeof(state.minibufferBuf), "find-file");
|
||||
bool mbOk = emacsExecuteMinibuffer(state, mock, log);
|
||||
expect(mbOk, "minibuffer command executed", passed, failed);
|
||||
expect(!state.minibufferActive, "minibuffer cleared", passed, failed);
|
||||
|
||||
updateEmacsModeLine(state, mock, 2.0, log);
|
||||
expect(!state.modeLine.empty(), "mode line updated", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 144 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user