Files
whetstone_DSL/editor/src/EmacsPackageBrowser.h
2026-02-09 21:26:10 -07:00

263 lines
10 KiB
C++

#pragma once
#include "imgui.h"
#include "EmacsIntegration.h"
#include "NotificationSystem.h"
#include "StringUtils.h"
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <cctype>
struct EmacsPackageEntry {
std::string name;
std::string version;
std::string description;
std::string status; // loaded | available | not-installed
};
struct EmacsPackageBrowserState {
std::vector<EmacsPackageEntry> packages;
char filterBuf[128] = {};
char loadBuf[128] = {};
int selected = -1;
bool showAvailable = true;
bool needsRefresh = true;
std::string lastError;
};
// trimCopy is provided by EditorUtils.h
static inline std::string toLowerCopyPkg(const std::string& s) {
std::string out = s;
std::transform(out.begin(), out.end(), out.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
return out;
}
static std::string buildPackageListCommand(const std::string& listSymbol) {
std::string cmd =
"(progn "
"(require 'package) "
"(mapconcat "
" (lambda (p) "
" (let* ((desc (cond "
" ((and (consp (cdr p)) (listp (cdr p))) (car (cdr p))) "
" ((consp (cdr p)) (cdr p)) "
" (t nil))) "
" (name (symbol-name (car p))) "
" (ver (if (and desc (fboundp 'package-desc-version)) "
" (package-version-join (package-desc-version desc)) \"\")) "
" (summary (if (and desc (fboundp 'package-desc-summary)) "
" (package-desc-summary desc) \"\"))) "
" (concat name \"|\" ver \"|\" summary))) "
+ listSymbol +
" \"\\n\"))";
return cmd;
}
static std::vector<EmacsPackageEntry> parsePackageListText(const std::string& text) {
std::vector<EmacsPackageEntry> out;
if (text.empty()) return out;
size_t start = 0;
while (start < text.size()) {
size_t end = text.find('\n', start);
if (end == std::string::npos) end = text.size();
std::string line = trimCopy(text.substr(start, end - start));
if (!line.empty()) {
size_t a = line.find('|');
size_t b = a == std::string::npos ? std::string::npos : line.find('|', a + 1);
EmacsPackageEntry entry;
if (a == std::string::npos) {
entry.name = line;
} else {
entry.name = trimCopy(line.substr(0, a));
if (b == std::string::npos) {
entry.version = trimCopy(line.substr(a + 1));
} else {
entry.version = trimCopy(line.substr(a + 1, b - a - 1));
entry.description = trimCopy(line.substr(b + 1));
}
}
if (!entry.name.empty()) {
out.push_back(std::move(entry));
}
}
start = end + 1;
}
return out;
}
static std::vector<EmacsPackageEntry> mergePackageLists(const std::vector<EmacsPackageEntry>& loaded,
const std::vector<EmacsPackageEntry>& available,
bool includeAvailable) {
std::unordered_map<std::string, EmacsPackageEntry> merged;
for (auto entry : loaded) {
entry.status = "loaded";
merged[entry.name] = std::move(entry);
}
if (includeAvailable) {
for (auto entry : available) {
auto it = merged.find(entry.name);
if (it == merged.end()) {
entry.status = "available";
merged[entry.name] = std::move(entry);
} else if (it->second.description.empty() && !entry.description.empty()) {
it->second.description = entry.description;
}
}
}
std::vector<EmacsPackageEntry> out;
out.reserve(merged.size());
for (auto& kv : merged) out.push_back(std::move(kv.second));
std::sort(out.begin(), out.end(),
[](const EmacsPackageEntry& a, const EmacsPackageEntry& b) {
return a.name < b.name;
});
return out;
}
static std::vector<EmacsPackageEntry> queryEmacsPackageList(EmacsConnection& emacs,
bool includeAvailable,
NotificationSystem& notifications,
std::string& outError) {
outError.clear();
std::string loadedText = emacs.sendCommand(buildPackageListCommand("package-alist"));
if (loadedText.empty() && !emacs.getLastError().empty()) {
outError = emacs.getLastError();
notifications.notify(NotificationLevel::Error,
"[emacs] " + outError);
}
auto loaded = parsePackageListText(loadedText);
std::vector<EmacsPackageEntry> available;
if (includeAvailable) {
std::string availText = emacs.sendCommand(buildPackageListCommand("package-archive-contents"));
if (availText.empty() && !emacs.getLastError().empty()) {
outError = emacs.getLastError();
notifications.notify(NotificationLevel::Error,
"[emacs] " + outError);
}
available = parsePackageListText(availText);
}
return mergePackageLists(loaded, available, includeAvailable);
}
static void refreshEmacsPackages(EmacsPackageBrowserState& state,
EmacsConnection& emacs,
NotificationSystem& notifications) {
state.lastError.clear();
state.packages = queryEmacsPackageList(emacs, state.showAvailable, notifications, state.lastError);
state.needsRefresh = false;
}
static bool loadEmacsPackage(EmacsConnection& emacs,
const std::string& packageName,
NotificationSystem& notifications) {
if (packageName.empty()) return false;
std::string cmd = "(require '" + packageName + ")";
std::string result = emacs.sendCommand(cmd);
if (!emacs.getLastError().empty() && result == "error") {
notifications.notify(NotificationLevel::Error,
"[emacs] Failed to load " + packageName + ": " +
emacs.getLastError());
return false;
}
notifications.notify(NotificationLevel::Success,
"[emacs] Loaded package: " + packageName);
return true;
}
static bool emacsPackageMatchesFilter(const EmacsPackageEntry& entry, const std::string& filter) {
if (filter.empty()) return true;
std::string hay = toLowerCopyPkg(entry.name + " " + entry.description);
std::string needle = toLowerCopyPkg(filter);
return hay.find(needle) != std::string::npos;
}
static bool renderEmacsPackageBrowser(EmacsPackageBrowserState& state,
EmacsConnection& emacs,
NotificationSystem& notifications) {
bool updated = false;
if (ImGui::Button("Refresh")) {
state.needsRefresh = true;
}
ImGui::SameLine();
if (ImGui::Checkbox("Show Available", &state.showAvailable)) {
state.needsRefresh = true;
}
ImGui::SameLine();
ImGui::InputText("Filter", state.filterBuf, sizeof(state.filterBuf));
if (state.needsRefresh) {
refreshEmacsPackages(state, emacs, notifications);
updated = true;
}
ImGui::Separator();
if (state.packages.empty()) {
ImGui::TextDisabled("(no packages loaded)");
} else {
if (ImGui::BeginTable("##emacsPkgs", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Package", ImGuiTableColumnFlags_WidthStretch, 0.25f);
ImGui::TableSetupColumn("Version", ImGuiTableColumnFlags_WidthStretch, 0.15f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.15f);
ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch, 0.45f);
ImGui::TableHeadersRow();
std::string filter = state.filterBuf;
int row = 0;
for (int i = 0; i < (int)state.packages.size(); ++i) {
const auto& entry = state.packages[i];
if (!emacsPackageMatchesFilter(entry, filter)) continue;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
bool selected = (state.selected == i);
if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
state.selected = i;
}
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(entry.version.empty() ? "-" : entry.version.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(entry.status.empty() ? "-" : entry.status.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextWrapped("%s", entry.description.empty() ? "-" : entry.description.c_str());
++row;
}
if (row == 0) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("(no matches)");
}
ImGui::EndTable();
}
}
ImGui::Separator();
ImGui::InputText("Load Package", state.loadBuf, sizeof(state.loadBuf));
ImGui::SameLine();
std::string loadTarget = state.loadBuf;
if (loadTarget.empty() && state.selected >= 0 &&
state.selected < (int)state.packages.size()) {
loadTarget = state.packages[state.selected].name;
}
if (ImGui::Button("Load")) {
if (loadTarget.empty()) {
notifications.notify(NotificationLevel::Warning,
"[emacs] No package selected.");
} else {
bool ok = loadEmacsPackage(emacs, loadTarget, notifications);
if (!ok) {
notifications.notify(NotificationLevel::Warning,
"[emacs] Package not installed: " + loadTarget);
}
state.needsRefresh = true;
updated = true;
}
}
if (!loadTarget.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", loadTarget.c_str());
}
return updated;
}