263 lines
10 KiB
C++
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;
|
|
}
|