Step 169: notification system
This commit is contained in:
@@ -516,3 +516,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Added `file_limits_test` to enforce architecture file size limits (with temporary allowlist for known oversize headers). 4/4 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 167: Split EditorState into focused sub-states (Search/Agent/Build/Library/Emacs/UI), updated panels/handlers, and added step167_test. 1/1 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 168: Split oversized editor/AST component headers (CodeEditorWidget, SyntaxHighlighter, Parser, CppGenerator) with new step168 unit + integration tests. 79/79 tests pass (step168_test 75/75, step168_integration_test 4/4). file_limits_test 4/4 passes. |
|
||||
| 2026-02-10 | Codex | Step 169: Notification/toast system with status bar history, output log rewire, and notification tests. 2/2 tests pass (step169_test, step169_integration_test). file_limits_test 4/4 passes. |
|
||||
|
||||
@@ -967,6 +967,14 @@ target_link_libraries(step168_integration_test PRIVATE
|
||||
tree_sitter_rust
|
||||
tree_sitter_go)
|
||||
|
||||
add_executable(step169_test tests/step169_test.cpp)
|
||||
target_include_directories(step169_test PRIVATE src)
|
||||
target_link_libraries(step169_test PRIVATE imgui::imgui)
|
||||
|
||||
add_executable(step169_integration_test tests/step169_integration_test.cpp)
|
||||
target_include_directories(step169_integration_test PRIVATE src)
|
||||
target_link_libraries(step169_integration_test PRIVATE imgui::imgui)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "imgui.h"
|
||||
#include "AgentRegistry.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -30,7 +31,7 @@ static bool agentMatchesFilter(const AgentDescriptor& agent,
|
||||
|
||||
static void renderAgentMarketplace(AgentMarketplaceState& state,
|
||||
AgentRegistry& registry,
|
||||
std::string& outputLog,
|
||||
NotificationSystem& notifications,
|
||||
const std::string& workspaceRoot) {
|
||||
if (state.registryPathBuf[0] == '\0') {
|
||||
std::string path = workspaceRoot.empty() ? "agents.json"
|
||||
@@ -46,30 +47,35 @@ static void renderAgentMarketplace(AgentMarketplaceState& state,
|
||||
ImGui::InputText("Registry URL", state.registryUrlBuf, sizeof(state.registryUrlBuf));
|
||||
if (ImGui::Button("Set URL")) {
|
||||
registry.setRegistryUrl(state.registryUrlBuf);
|
||||
outputLog += "[agents] Registry URL set.\n";
|
||||
notifications.notify(NotificationLevel::Info, "[agents] Registry URL set.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh")) {
|
||||
outputLog += "[agents] STUB: refresh registry from " +
|
||||
std::string(state.registryUrlBuf) + "\n";
|
||||
notifications.notify(NotificationLevel::Info,
|
||||
"[agents] STUB: refresh registry from " +
|
||||
std::string(state.registryUrlBuf));
|
||||
}
|
||||
|
||||
ImGui::InputText("Registry File", state.registryPathBuf, sizeof(state.registryPathBuf));
|
||||
if (ImGui::Button("Load")) {
|
||||
std::string error;
|
||||
if (!registry.loadFromFile(state.registryPathBuf, error)) {
|
||||
outputLog += "[agents] Load failed: " + error + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[agents] Load failed: " + error);
|
||||
} else {
|
||||
outputLog += "[agents] Loaded registry.\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[agents] Loaded registry.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save")) {
|
||||
std::string error;
|
||||
if (!registry.saveToFile(state.registryPathBuf, error)) {
|
||||
outputLog += "[agents] Save failed: " + error + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[agents] Save failed: " + error);
|
||||
} else {
|
||||
outputLog += "[agents] Saved registry.\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[agents] Saved registry.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +120,15 @@ static void renderAgentMarketplace(AgentMarketplaceState& state,
|
||||
if (!selected->installed) {
|
||||
if (ImGui::Button("Install Agent")) {
|
||||
selected->installed = true;
|
||||
outputLog += "[agents] Installed " + selected->name +
|
||||
" (" + selected->endpoint + ")\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[agents] Installed " + selected->name +
|
||||
" (" + selected->endpoint + ")");
|
||||
}
|
||||
} else {
|
||||
if (ImGui::Button("Uninstall Agent")) {
|
||||
selected->installed = false;
|
||||
outputLog += "[agents] Uninstalled " + selected->name + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[agents] Uninstalled " + selected->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "CompositionBuilder.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
@@ -17,7 +18,7 @@ struct CompositionPanelState {
|
||||
static bool renderCompositionPanel(CompositionPanelState& state,
|
||||
const std::vector<PrimitiveSymbol>& primitives,
|
||||
std::string& outCode,
|
||||
std::string& outputLog) {
|
||||
NotificationSystem& notifications) {
|
||||
outCode.clear();
|
||||
ImGui::InputText("Input Type", state.inputType, sizeof(state.inputType));
|
||||
ImGui::InputText("Output Type", state.outputType, sizeof(state.outputType));
|
||||
@@ -48,7 +49,8 @@ static bool renderCompositionPanel(CompositionPanelState& state,
|
||||
if (state.selected >= 0 && state.selected < (int)suggestions.size()) {
|
||||
if (ImGui::Button("Add Step")) {
|
||||
if (state.steps.size() >= 8) {
|
||||
outputLog += "[compose] Max 8 steps.\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[compose] Max 8 steps.");
|
||||
} else {
|
||||
state.steps.push_back(suggestions[state.selected]);
|
||||
}
|
||||
@@ -87,7 +89,8 @@ static bool renderCompositionPanel(CompositionPanelState& state,
|
||||
ImGui::EndChild();
|
||||
if (ImGui::Button("Insert Code")) {
|
||||
outCode = code;
|
||||
outputLog += "[compose] Inserted pipeline.\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[compose] Inserted pipeline.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "imgui.h"
|
||||
#include "DependencyParser.h"
|
||||
#include "PackageRegistry.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
@@ -132,23 +133,25 @@ static std::string formatRequirementLine(const DependencySpec& dep) {
|
||||
|
||||
static bool writeRequirementsFile(const std::string& path,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) {
|
||||
logOut += "[deps] Failed to write " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[deps] Failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
for (const auto& dep : deps) {
|
||||
if (dep.name.empty()) continue;
|
||||
out << formatRequirementLine(dep) << "\n";
|
||||
}
|
||||
logOut += "[deps] Wrote " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[deps] Wrote " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writePackageJson(const std::string& path,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
nlohmann::json j;
|
||||
if (std::filesystem::exists(path)) {
|
||||
std::ifstream in(path);
|
||||
@@ -175,17 +178,19 @@ static bool writePackageJson(const std::string& path,
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) {
|
||||
logOut += "[deps] Failed to write " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[deps] Failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
out << j.dump(2);
|
||||
logOut += "[deps] Wrote " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[deps] Wrote " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeCargoToml(const std::string& path,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
std::vector<std::string> prefix;
|
||||
bool foundDeps = false;
|
||||
if (std::filesystem::exists(path)) {
|
||||
@@ -201,7 +206,8 @@ static bool writeCargoToml(const std::string& path,
|
||||
}
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) {
|
||||
logOut += "[deps] Failed to write " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[deps] Failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
for (const auto& line : prefix) {
|
||||
@@ -214,13 +220,14 @@ static bool writeCargoToml(const std::string& path,
|
||||
std::string version = dep.version.empty() ? "*" : dep.version;
|
||||
out << dep.name << " = \"" << version << "\"\n";
|
||||
}
|
||||
logOut += "[deps] Wrote " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[deps] Wrote " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeGoMod(const std::string& path,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
std::string moduleLine;
|
||||
std::string goLine;
|
||||
if (std::filesystem::exists(path)) {
|
||||
@@ -236,7 +243,8 @@ static bool writeGoMod(const std::string& path,
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) {
|
||||
logOut += "[deps] Failed to write " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[deps] Failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
out << moduleLine << "\n" << goLine << "\n\n";
|
||||
@@ -247,13 +255,14 @@ static bool writeGoMod(const std::string& path,
|
||||
out << " " << dep.name << " " << version << "\n";
|
||||
}
|
||||
out << ")\n";
|
||||
logOut += "[deps] Wrote " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[deps] Wrote " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeVcpkgJson(const std::string& path,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
nlohmann::json j;
|
||||
if (std::filesystem::exists(path)) {
|
||||
std::ifstream in(path);
|
||||
@@ -274,39 +283,43 @@ static bool writeVcpkgJson(const std::string& path,
|
||||
j["dependencies"] = depsArr;
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) {
|
||||
logOut += "[deps] Failed to write " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[deps] Failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
out << j.dump(2);
|
||||
logOut += "[deps] Wrote " + path + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[deps] Wrote " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeDependenciesForSource(const std::string& source,
|
||||
const std::string& workspaceRoot,
|
||||
const std::vector<DependencySpec>& deps,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
if (workspaceRoot.empty()) {
|
||||
logOut += "[deps] No workspace root set.\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[deps] No workspace root set.");
|
||||
return false;
|
||||
}
|
||||
std::string base = sourceBase(source);
|
||||
std::string path = resolveSourcePath(workspaceRoot, base);
|
||||
auto filtered = depsForSource(deps, base);
|
||||
|
||||
if (base == "requirements.txt") return writeRequirementsFile(path, filtered, logOut);
|
||||
if (base == "package.json") return writePackageJson(path, filtered, logOut);
|
||||
if (base == "Cargo.toml") return writeCargoToml(path, filtered, logOut);
|
||||
if (base == "go.mod") return writeGoMod(path, filtered, logOut);
|
||||
if (base == "vcpkg.json") return writeVcpkgJson(path, filtered, logOut);
|
||||
if (base == "requirements.txt") return writeRequirementsFile(path, filtered, notifications);
|
||||
if (base == "package.json") return writePackageJson(path, filtered, notifications);
|
||||
if (base == "Cargo.toml") return writeCargoToml(path, filtered, notifications);
|
||||
if (base == "go.mod") return writeGoMod(path, filtered, notifications);
|
||||
if (base == "vcpkg.json") return writeVcpkgJson(path, filtered, notifications);
|
||||
|
||||
logOut += "[deps] Writeback not implemented for " + base + "\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[deps] Writeback not implemented for " + base);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void refreshDependencies(DependencyPanelState& state,
|
||||
const std::string& workspaceRoot,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
state.deps.clear();
|
||||
state.sources = discoverDependencyFiles(workspaceRoot);
|
||||
state.selected = -1;
|
||||
@@ -325,24 +338,25 @@ static void refreshDependencies(DependencyPanelState& state,
|
||||
} else {
|
||||
state.sourceSelected = -1;
|
||||
}
|
||||
logOut += "[deps] Loaded " + std::to_string(state.deps.size()) + " dependencies.\n";
|
||||
notifications.notify(NotificationLevel::Info,
|
||||
"[deps] Loaded " + std::to_string(state.deps.size()) + " dependencies.");
|
||||
}
|
||||
|
||||
static void renderDependencyPanel(DependencyPanelState& state,
|
||||
const std::string& workspaceRoot,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
if (workspaceRoot.empty()) {
|
||||
ImGui::TextDisabled("Open a workspace to manage dependencies.");
|
||||
return;
|
||||
}
|
||||
if (state.lastWorkspaceRoot != workspaceRoot || state.deps.empty()) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
}
|
||||
|
||||
ImGui::InputText("Search", state.searchBuf, sizeof(state.searchBuf));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh")) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add Package")) {
|
||||
@@ -388,8 +402,9 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
}
|
||||
if (ImGui::Button("Search Registry")) {
|
||||
state.lastLookup = PackageRegistry::query(state.addEcosystem, state.addBuf);
|
||||
logOut += "[deps] Registry: " + state.lastLookup.name + " - " +
|
||||
state.lastLookup.description + "\n";
|
||||
notifications.notify(NotificationLevel::Info,
|
||||
"[deps] Registry: " + state.lastLookup.name + " - " +
|
||||
state.lastLookup.description);
|
||||
}
|
||||
if (!state.lastLookup.name.empty()) {
|
||||
ImGui::TextWrapped("Found: %s", state.lastLookup.name.c_str());
|
||||
@@ -397,7 +412,8 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Add")) {
|
||||
if (state.addBuf[0] == '\0') {
|
||||
logOut += "[deps] Add aborted: empty package name.\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[deps] Add aborted: empty package name.");
|
||||
} else {
|
||||
DependencySpec dep;
|
||||
dep.name = state.addBuf;
|
||||
@@ -408,8 +424,8 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
dep.source = "requirements.txt";
|
||||
}
|
||||
state.deps.push_back(dep);
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, logOut)) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, notifications)) {
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
state.needsIndex = true;
|
||||
}
|
||||
}
|
||||
@@ -450,8 +466,8 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
ImGui::InputText("Version", state.editVersionBuf, sizeof(state.editVersionBuf));
|
||||
if (ImGui::Button("Apply Version")) {
|
||||
dep.version = state.editVersionBuf;
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, logOut)) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, notifications)) {
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
state.needsIndex = true;
|
||||
}
|
||||
}
|
||||
@@ -460,8 +476,8 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
std::string src = dep.source;
|
||||
state.deps.erase(state.deps.begin() + state.selected);
|
||||
state.selected = -1;
|
||||
if (writeDependenciesForSource(src, workspaceRoot, state.deps, logOut)) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
if (writeDependenciesForSource(src, workspaceRoot, state.deps, notifications)) {
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
state.needsIndex = true;
|
||||
}
|
||||
}
|
||||
@@ -471,12 +487,13 @@ static void renderDependencyPanel(DependencyPanelState& state,
|
||||
auto info = PackageRegistry::query(eco, dep.name);
|
||||
if (!info.versions.empty()) {
|
||||
dep.version = info.versions.back();
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, logOut)) {
|
||||
refreshDependencies(state, workspaceRoot, logOut);
|
||||
if (writeDependenciesForSource(dep.source, workspaceRoot, state.deps, notifications)) {
|
||||
refreshDependencies(state, workspaceRoot, notifications);
|
||||
state.needsIndex = true;
|
||||
}
|
||||
} else {
|
||||
logOut += "[deps] No version info for " + dep.name + "\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[deps] No version info for " + dep.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "HelpPanel.h"
|
||||
#include "Telemetry.h"
|
||||
#include "UpdateChecker.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include "state/SearchState.h"
|
||||
#include "state/AgentState.h"
|
||||
#include "state/BuildState.h"
|
||||
@@ -172,8 +173,7 @@ struct EditorState {
|
||||
EmacsState emacsState;
|
||||
UIFlags ui;
|
||||
|
||||
// Bottom panel + global log
|
||||
std::string outputLog;
|
||||
NotificationSystem notifications;
|
||||
HelpPanelState helpPanel;
|
||||
Telemetry telemetry;
|
||||
char outlineFilter[128] = {};
|
||||
@@ -326,6 +326,26 @@ struct EditorState {
|
||||
return text.substr(start, pos - start);
|
||||
}
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message) {
|
||||
notifications.notify(level, message);
|
||||
}
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message, const NotificationTarget& target) {
|
||||
notifications.notify(level, message, target);
|
||||
}
|
||||
|
||||
void navigateToTarget(const NotificationTarget& target) {
|
||||
if (target.path.empty()) return;
|
||||
std::string path = target.path;
|
||||
if (buffers.hasBuffer(path)) switchToBuffer(path);
|
||||
else doOpen(path);
|
||||
if (active()) {
|
||||
int line = std::max(0, target.line);
|
||||
int col = std::max(0, target.col);
|
||||
jumpTo(active(), line, col);
|
||||
}
|
||||
}
|
||||
|
||||
void jumpTo(BufferState* buf, int lineZero, int colZero) {
|
||||
if (!buf) return;
|
||||
int pos = byteOffsetForLineCol(buf->editBuf, lineZero, colZero);
|
||||
@@ -760,7 +780,7 @@ struct EditorState {
|
||||
}
|
||||
|
||||
void init() {
|
||||
outputLog = "Whetstone Editor ready.\n";
|
||||
notify(NotificationLevel::Info, "Whetstone Editor ready.");
|
||||
workspaceRoot = std::filesystem::current_path().string();
|
||||
search.projectSearch.setRoot(workspaceRoot);
|
||||
fileTreeDirty = true;
|
||||
@@ -824,7 +844,7 @@ struct EditorState {
|
||||
std::string log;
|
||||
bool ok = emacsState.emacs.startDaemonWithConfig(initPath, log);
|
||||
if (!log.empty()) {
|
||||
outputLog += "[emacs] " + log + "\n";
|
||||
notify(NotificationLevel::Info, "[emacs] " + log);
|
||||
}
|
||||
if (!ok) {
|
||||
EditorDiagnostic d;
|
||||
@@ -936,7 +956,8 @@ struct EditorState {
|
||||
build.lastBuildCommand = cmd.command;
|
||||
int code = build.terminal.runAndCapture(workspaceRoot, cmd.command, build.lastBuildOutput);
|
||||
build.buildErrors = BuildSystem::parseErrors(build.lastBuildOutput);
|
||||
outputLog += "[build] " + cmd.label + " => exit " + std::to_string(code) + "\n";
|
||||
notify(NotificationLevel::Info,
|
||||
"[build] " + cmd.label + " => exit " + std::to_string(code));
|
||||
return code;
|
||||
}
|
||||
|
||||
@@ -953,13 +974,15 @@ struct EditorState {
|
||||
library.libraryIndex.completionsByLibrary.clear();
|
||||
library.libraryIndexRequests.clear();
|
||||
if (!isStructured() || !activeAST()) {
|
||||
outputLog += "[deps] Library index skipped: no structured AST.\n";
|
||||
notify(NotificationLevel::Warning,
|
||||
"[deps] Library index skipped: no structured AST.");
|
||||
return;
|
||||
}
|
||||
if (!lsp || !lspTransport || !lspTransport->isOpen()) {
|
||||
applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot);
|
||||
rebuildExternalModulesFromIndex();
|
||||
outputLog += "[deps] Library index uses cached symbols (LSP offline).\n";
|
||||
notify(NotificationLevel::Warning,
|
||||
"[deps] Library index uses cached symbols (LSP offline).");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -980,8 +1003,9 @@ struct EditorState {
|
||||
}
|
||||
applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot);
|
||||
rebuildExternalModulesFromIndex();
|
||||
outputLog += "[deps] Library index requests queued: " +
|
||||
std::to_string(library.libraryIndexRequests.size()) + "\n";
|
||||
notify(NotificationLevel::Info,
|
||||
"[deps] Library index requests queued: " +
|
||||
std::to_string(library.libraryIndexRequests.size()));
|
||||
}
|
||||
|
||||
void processLibraryIndexResponses() {
|
||||
@@ -1015,22 +1039,23 @@ struct EditorState {
|
||||
if (updated) {
|
||||
applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot);
|
||||
rebuildExternalModulesFromIndex();
|
||||
outputLog += "[deps] Library index updated.\n";
|
||||
notify(NotificationLevel::Info, "[deps] Library index updated.");
|
||||
}
|
||||
}
|
||||
|
||||
void updateEmacsFunctionIndex() {
|
||||
if (!active() || active()->language != "elisp") return;
|
||||
std::string error;
|
||||
auto packages = queryEmacsPackageList(emacsState.emacs, false, outputLog, error);
|
||||
auto packages = queryEmacsPackageList(emacsState.emacs, false, notifications, error);
|
||||
if (!error.empty()) return;
|
||||
refreshEmacsFunctionIndex(emacsState.emacsFunctionIndex, emacsState.emacs, packages, outputLog);
|
||||
refreshEmacsFunctionIndex(emacsState.emacsFunctionIndex, emacsState.emacs, packages, notifications);
|
||||
addEmacsSymbolsToLibraryIndex(library.libraryIndex, emacsState.emacsFunctionIndex);
|
||||
emacsState.emacsFunctionIndexDirty = false;
|
||||
rebuildExternalModulesFromIndex();
|
||||
outputLog += "[emacs] Indexed functions for " +
|
||||
std::to_string(emacsState.emacsFunctionIndex.functionsByPackage.size()) +
|
||||
" packages.\n";
|
||||
notify(NotificationLevel::Info,
|
||||
"[emacs] Indexed functions for " +
|
||||
std::to_string(emacsState.emacsFunctionIndex.functionsByPackage.size()) +
|
||||
" packages.");
|
||||
}
|
||||
|
||||
void rebuildExternalModulesFromIndex() {
|
||||
@@ -1544,7 +1569,8 @@ struct EditorState {
|
||||
{"Open Project", {"*.whetstone"}, std::string()});
|
||||
if (!path.empty()) {
|
||||
if (!loadProject(path)) {
|
||||
outputLog += "Failed to open project: " + path + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"Failed to open project: " + path);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1555,7 +1581,8 @@ struct EditorState {
|
||||
{"Save Project", {"*.whetstone"}, std::string()});
|
||||
if (!path.empty()) {
|
||||
if (!saveProject(path)) {
|
||||
outputLog += "Failed to save project: " + path + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"Failed to save project: " + path);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1669,54 +1696,58 @@ struct EditorState {
|
||||
|
||||
bool handleEmacsKeyChord(const std::string& chord) {
|
||||
if (ui.layoutPreset != LayoutPreset::Emacs) return false;
|
||||
return emacsHandleKeySequence(emacsState.emacsKeys, emacsState.emacs, chord, outputLog);
|
||||
return emacsHandleKeySequence(emacsState.emacsKeys, emacsState.emacs, chord, notifications);
|
||||
}
|
||||
|
||||
void refreshEmacsModeLine(double nowSeconds) {
|
||||
if (ui.layoutPreset != LayoutPreset::Emacs) return;
|
||||
updateEmacsModeLine(emacsState.emacsKeys, emacsState.emacs, nowSeconds, outputLog);
|
||||
updateEmacsModeLine(emacsState.emacsKeys, emacsState.emacs, nowSeconds, notifications);
|
||||
}
|
||||
|
||||
void pullFromEmacs() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before syncing.\n";
|
||||
notify(NotificationLevel::Warning, "[emacs] Save file before syncing.");
|
||||
return;
|
||||
}
|
||||
std::string text = emacsState.emacs.getBufferText(active()->path);
|
||||
if (!emacsState.emacs.getLastError().empty() && text == "error") {
|
||||
outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacsState.emacs.getLastError());
|
||||
return;
|
||||
}
|
||||
active()->editBuf = text;
|
||||
onTextChanged();
|
||||
outputLog += "[emacs] Pulled buffer from Emacs.\n";
|
||||
notify(NotificationLevel::Success, "[emacs] Pulled buffer from Emacs.");
|
||||
}
|
||||
|
||||
void pushToEmacs() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before syncing.\n";
|
||||
notify(NotificationLevel::Warning, "[emacs] Save file before syncing.");
|
||||
return;
|
||||
}
|
||||
if (!emacsState.emacs.setBufferText(active()->path, active()->editBuf)) {
|
||||
outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacsState.emacs.getLastError());
|
||||
return;
|
||||
}
|
||||
outputLog += "[emacs] Pushed buffer to Emacs.\n";
|
||||
notify(NotificationLevel::Success, "[emacs] Pushed buffer to Emacs.");
|
||||
}
|
||||
|
||||
void openInEmacsFrame() {
|
||||
if (!active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) {
|
||||
outputLog += "[emacs] Save file before opening in Emacs.\n";
|
||||
notify(NotificationLevel::Warning,
|
||||
"[emacs] Save file before opening in Emacs.");
|
||||
return;
|
||||
}
|
||||
if (!emacsState.emacs.openFileInEmacsFrame(active()->path)) {
|
||||
outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacsState.emacs.getLastError());
|
||||
return;
|
||||
}
|
||||
outputLog += "[emacs] Opened in Emacs frame.\n";
|
||||
notify(NotificationLevel::Success, "[emacs] Opened in Emacs frame.");
|
||||
}
|
||||
|
||||
std::string buildRunCommand(const std::string& path,
|
||||
@@ -1840,11 +1871,18 @@ struct EditorState {
|
||||
}
|
||||
active()->cursorLine = line;
|
||||
active()->cursorCol = col;
|
||||
outputLog += "Found \"" + std::string(search.findBuf) + "\" at line " +
|
||||
std::to_string(line) + ", col " + std::to_string(col) + "\n";
|
||||
NotificationTarget target;
|
||||
target.path = active()->path;
|
||||
target.line = std::max(0, line - 1);
|
||||
target.col = std::max(0, col - 1);
|
||||
notify(NotificationLevel::Info,
|
||||
"Found \"" + std::string(search.findBuf) + "\" at line " +
|
||||
std::to_string(line) + ", col " + std::to_string(col),
|
||||
target);
|
||||
} else {
|
||||
search.lastFindPos = 0;
|
||||
outputLog += "\"" + std::string(search.findBuf) + "\" not found.\n";
|
||||
notify(NotificationLevel::Warning,
|
||||
"\"" + std::string(search.findBuf) + "\" not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1863,7 +1901,8 @@ struct EditorState {
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = true;
|
||||
}
|
||||
outputLog += "Replaced " + std::to_string(count) + " occurrence(s).\n";
|
||||
notify(NotificationLevel::Info,
|
||||
"Replaced " + std::to_string(count) + " occurrence(s).");
|
||||
}
|
||||
|
||||
void doSave() {
|
||||
@@ -1874,10 +1913,12 @@ struct EditorState {
|
||||
out << active()->editBuf;
|
||||
out.close();
|
||||
active()->modified = false;
|
||||
outputLog += "Saved: " + active()->path + "\n";
|
||||
notify(NotificationLevel::Success,
|
||||
"Saved: " + active()->path);
|
||||
if (lsp) lsp->didSave(toFileUri(active()->path), active()->editBuf);
|
||||
} else {
|
||||
outputLog += "Error saving: " + active()->path + "\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"Error saving: " + active()->path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,9 +1953,9 @@ struct EditorState {
|
||||
createBuffer(path, content, language, modeOverride);
|
||||
welcome.addRecentFile(path, language, bufferModeToString(modeOverride));
|
||||
saveRecentFiles();
|
||||
outputLog += "Opened: " + path + "\n";
|
||||
notify(NotificationLevel::Success, "Opened: " + path);
|
||||
} else {
|
||||
outputLog += "Error opening: " + path + "\n";
|
||||
notify(NotificationLevel::Error, "Error opening: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1936,7 +1977,7 @@ struct EditorState {
|
||||
buf->highlightsDirty = true;
|
||||
buf->generatedHighlightsDirty = true;
|
||||
buf->modified = false;
|
||||
outputLog += "Reloaded: " + path + "\n";
|
||||
notify(NotificationLevel::Info, "Reloaded: " + path);
|
||||
}
|
||||
|
||||
void handleFileChanges() {
|
||||
@@ -1945,7 +1986,8 @@ struct EditorState {
|
||||
auto it = bufferStates.find(path);
|
||||
if (it == bufferStates.end()) continue;
|
||||
if (it->second->modified) {
|
||||
outputLog += "File changed on disk (dirty): " + path + "\n";
|
||||
notify(NotificationLevel::Warning,
|
||||
"File changed on disk (dirty): " + path);
|
||||
} else {
|
||||
reloadBuffer(path);
|
||||
}
|
||||
@@ -1979,14 +2021,16 @@ struct EditorState {
|
||||
if (!active()) return;
|
||||
Module* ast = activeAST();
|
||||
if (!ast) {
|
||||
outputLog += "Project to " + targetLanguage + ": no AST available.\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"Project to " + targetLanguage + ": no AST available.");
|
||||
return;
|
||||
}
|
||||
|
||||
CrossLanguageProjector projector;
|
||||
auto projected = projector.project(ast, targetLanguage);
|
||||
if (!projected) {
|
||||
outputLog += "Project to " + targetLanguage + ": failed to project AST.\n";
|
||||
notify(NotificationLevel::Error,
|
||||
"Project to " + targetLanguage + ": failed to project AST.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2018,10 +2062,11 @@ struct EditorState {
|
||||
active()->modified = false;
|
||||
}
|
||||
|
||||
outputLog += "Projected to " + targetLanguage + " in " + projName +
|
||||
" (annotations " + std::to_string(srcAnnoCount) + " -> " +
|
||||
std::to_string(projAnnoCount) + ", types preserved: " +
|
||||
(preserved ? "yes" : "no") + ").\n";
|
||||
notify(NotificationLevel::Success,
|
||||
"Projected to " + targetLanguage + " in " + projName +
|
||||
" (annotations " + std::to_string(srcAnnoCount) + " -> " +
|
||||
std::to_string(projAnnoCount) + ", types preserved: " +
|
||||
(preserved ? "yes" : "no") + ").");
|
||||
}
|
||||
|
||||
void refreshActiveTextFromAST() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "EmacsIntegration.h"
|
||||
#include "EmacsPackageBrowser.h"
|
||||
#include "LibraryIndexer.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/ExternalModule.h"
|
||||
#include "ast/TypeSignature.h"
|
||||
@@ -35,11 +36,12 @@ static std::vector<std::string> parseLines(const std::string& text) {
|
||||
|
||||
static std::vector<std::string> queryEmacsFunctions(EmacsConnection& emacs,
|
||||
const std::string& prefix,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
std::string cmd = ElispCommandBuilder::aproposFunctions(prefix);
|
||||
std::string resp = emacs.sendCommand(cmd);
|
||||
if (resp.empty() && !emacs.getLastError().empty()) {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacs.getLastError());
|
||||
return {};
|
||||
}
|
||||
return parseLines(resp);
|
||||
@@ -47,13 +49,14 @@ static std::vector<std::string> queryEmacsFunctions(EmacsConnection& emacs,
|
||||
|
||||
static EmacsFunctionDoc queryEmacsFunctionDoc(EmacsConnection& emacs,
|
||||
const std::string& name,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
EmacsFunctionDoc info;
|
||||
info.name = name;
|
||||
std::string cmd = ElispCommandBuilder::describeFunction(name);
|
||||
std::string resp = emacs.sendCommand(cmd);
|
||||
if (resp.empty() && !emacs.getLastError().empty()) {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacs.getLastError());
|
||||
return info;
|
||||
}
|
||||
auto pos = resp.find('\n');
|
||||
@@ -81,20 +84,20 @@ static std::vector<std::string> buildPackageFunctionPrefixes(const std::string&
|
||||
static void refreshEmacsFunctionIndex(EmacsFunctionIndex& index,
|
||||
EmacsConnection& emacs,
|
||||
const std::vector<EmacsPackageEntry>& packages,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
index.functionsByPackage.clear();
|
||||
for (const auto& pkg : packages) {
|
||||
if (pkg.status != "loaded") continue;
|
||||
std::vector<std::string> functions;
|
||||
for (const auto& prefix : buildPackageFunctionPrefixes(pkg.name)) {
|
||||
functions = queryEmacsFunctions(emacs, "^" + prefix, logOut);
|
||||
functions = queryEmacsFunctions(emacs, "^" + prefix, notifications);
|
||||
if (!functions.empty()) break;
|
||||
}
|
||||
if (functions.empty()) continue;
|
||||
std::vector<EmacsFunctionDoc> docs;
|
||||
docs.reserve(functions.size());
|
||||
for (const auto& fn : functions) {
|
||||
docs.push_back(queryEmacsFunctionDoc(emacs, fn, logOut));
|
||||
docs.push_back(queryEmacsFunctionDoc(emacs, fn, notifications));
|
||||
}
|
||||
index.functionsByPackage[pkg.name] = std::move(docs);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "EmacsIntegration.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -29,7 +30,7 @@ static inline bool emacsIsPrefixKey(const std::string& chord) {
|
||||
static inline bool emacsHandleKeySequence(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
const std::string& chord,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
if (chord.empty()) return false;
|
||||
if (chord == "M-x") {
|
||||
state.minibufferActive = true;
|
||||
@@ -49,22 +50,25 @@ static inline bool emacsHandleKeySequence(EmacsKeybindingState& state,
|
||||
|
||||
std::string cmd = emacs.sendCommand(ElispCommandBuilder::keyBinding(sequence));
|
||||
if (cmd.empty()) {
|
||||
logOut += "[emacs] Unbound: " + sequence + "\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[emacs] Unbound: " + sequence);
|
||||
return true;
|
||||
}
|
||||
state.lastCommand = cmd;
|
||||
std::string result = emacs.sendCommand(ElispCommandBuilder::callInteractive(cmd));
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacs.getLastError());
|
||||
} else {
|
||||
logOut += "[emacs] " + cmd + "\n";
|
||||
notifications.notify(NotificationLevel::Info,
|
||||
"[emacs] " + cmd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool emacsExecuteMinibuffer(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
std::string cmd = emacsTrim(state.minibufferBuf);
|
||||
if (cmd.empty()) {
|
||||
state.minibufferActive = false;
|
||||
@@ -72,9 +76,11 @@ static inline bool emacsExecuteMinibuffer(EmacsKeybindingState& state,
|
||||
}
|
||||
std::string result = emacs.sendCommand(ElispCommandBuilder::executeExtendedCommand(cmd));
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
logOut += "[emacs] " + emacs.getLastError() + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacs.getLastError());
|
||||
} else {
|
||||
logOut += "[emacs] M-x " + cmd + "\n";
|
||||
notifications.notify(NotificationLevel::Info,
|
||||
"[emacs] M-x " + cmd);
|
||||
}
|
||||
state.minibufferActive = false;
|
||||
state.minibufferBuf[0] = '\0';
|
||||
@@ -84,11 +90,12 @@ static inline bool emacsExecuteMinibuffer(EmacsKeybindingState& state,
|
||||
static inline void updateEmacsModeLine(EmacsKeybindingState& state,
|
||||
EmacsConnection& emacs,
|
||||
double nowSeconds,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
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";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + emacs.getLastError());
|
||||
return;
|
||||
}
|
||||
if (!mode.empty()) state.modeLine = mode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "EmacsIntegration.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include "StringUtils.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -118,13 +119,14 @@ static std::vector<EmacsPackageEntry> mergePackageLists(const std::vector<EmacsP
|
||||
|
||||
static std::vector<EmacsPackageEntry> queryEmacsPackageList(EmacsConnection& emacs,
|
||||
bool includeAvailable,
|
||||
std::string& logOut,
|
||||
NotificationSystem& notifications,
|
||||
std::string& outError) {
|
||||
outError.clear();
|
||||
std::string loadedText = emacs.sendCommand(buildPackageListCommand("package-alist"));
|
||||
if (loadedText.empty() && !emacs.getLastError().empty()) {
|
||||
outError = emacs.getLastError();
|
||||
logOut += "[emacs] " + outError + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + outError);
|
||||
}
|
||||
auto loaded = parsePackageListText(loadedText);
|
||||
std::vector<EmacsPackageEntry> available;
|
||||
@@ -132,7 +134,8 @@ static std::vector<EmacsPackageEntry> queryEmacsPackageList(EmacsConnection& ema
|
||||
std::string availText = emacs.sendCommand(buildPackageListCommand("package-archive-contents"));
|
||||
if (availText.empty() && !emacs.getLastError().empty()) {
|
||||
outError = emacs.getLastError();
|
||||
logOut += "[emacs] " + outError + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] " + outError);
|
||||
}
|
||||
available = parsePackageListText(availText);
|
||||
}
|
||||
@@ -141,23 +144,26 @@ static std::vector<EmacsPackageEntry> queryEmacsPackageList(EmacsConnection& ema
|
||||
|
||||
static void refreshEmacsPackages(EmacsPackageBrowserState& state,
|
||||
EmacsConnection& emacs,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
state.lastError.clear();
|
||||
state.packages = queryEmacsPackageList(emacs, state.showAvailable, logOut, state.lastError);
|
||||
state.packages = queryEmacsPackageList(emacs, state.showAvailable, notifications, state.lastError);
|
||||
state.needsRefresh = false;
|
||||
}
|
||||
|
||||
static bool loadEmacsPackage(EmacsConnection& emacs,
|
||||
const std::string& packageName,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
if (packageName.empty()) return false;
|
||||
std::string cmd = "(require '" + packageName + ")";
|
||||
std::string result = emacs.sendCommand(cmd);
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
logOut += "[emacs] Failed to load " + packageName + ": " + emacs.getLastError() + "\n";
|
||||
notifications.notify(NotificationLevel::Error,
|
||||
"[emacs] Failed to load " + packageName + ": " +
|
||||
emacs.getLastError());
|
||||
return false;
|
||||
}
|
||||
logOut += "[emacs] Loaded package: " + packageName + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[emacs] Loaded package: " + packageName);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -170,7 +176,7 @@ static bool emacsPackageMatchesFilter(const EmacsPackageEntry& entry, const std:
|
||||
|
||||
static bool renderEmacsPackageBrowser(EmacsPackageBrowserState& state,
|
||||
EmacsConnection& emacs,
|
||||
std::string& logOut) {
|
||||
NotificationSystem& notifications) {
|
||||
bool updated = false;
|
||||
if (ImGui::Button("Refresh")) {
|
||||
state.needsRefresh = true;
|
||||
@@ -183,7 +189,7 @@ static bool renderEmacsPackageBrowser(EmacsPackageBrowserState& state,
|
||||
ImGui::InputText("Filter", state.filterBuf, sizeof(state.filterBuf));
|
||||
|
||||
if (state.needsRefresh) {
|
||||
refreshEmacsPackages(state, emacs, logOut);
|
||||
refreshEmacsPackages(state, emacs, notifications);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
@@ -236,11 +242,13 @@ static bool renderEmacsPackageBrowser(EmacsPackageBrowserState& state,
|
||||
}
|
||||
if (ImGui::Button("Load")) {
|
||||
if (loadTarget.empty()) {
|
||||
logOut += "[emacs] No package selected.\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[emacs] No package selected.");
|
||||
} else {
|
||||
bool ok = loadEmacsPackage(emacs, loadTarget, logOut);
|
||||
bool ok = loadEmacsPackage(emacs, loadTarget, notifications);
|
||||
if (!ok) {
|
||||
logOut += "[emacs] Package not installed: " + loadTarget + "\n";
|
||||
notifications.notify(NotificationLevel::Warning,
|
||||
"[emacs] Package not installed: " + loadTarget);
|
||||
}
|
||||
state.needsRefresh = true;
|
||||
updated = true;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "imgui.h"
|
||||
#include "LibraryIndexer.h"
|
||||
#include "ast/ExternalModule.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
@@ -58,7 +59,7 @@ static bool renderLibraryBrowser(LibraryBrowserState& state,
|
||||
const LibraryIndexData& index,
|
||||
std::string& outInsert,
|
||||
std::string& outLibrary,
|
||||
std::string& outputLog) {
|
||||
NotificationSystem& notifications) {
|
||||
outInsert.clear();
|
||||
outLibrary.clear();
|
||||
if (!ast) {
|
||||
@@ -112,7 +113,8 @@ static bool renderLibraryBrowser(LibraryBrowserState& state,
|
||||
if (ImGui::Button("Insert")) {
|
||||
outInsert = usage;
|
||||
outLibrary = state.selectedLibrary;
|
||||
outputLog += "[libs] Inserted " + usage + "\n";
|
||||
notifications.notify(NotificationLevel::Success,
|
||||
"[libs] Inserted " + usage);
|
||||
inserted = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
230
editor/src/NotificationSystem.h
Normal file
230
editor/src/NotificationSystem.h
Normal file
@@ -0,0 +1,230 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
enum class NotificationLevel {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
struct NotificationTarget {
|
||||
std::string path;
|
||||
int line = -1; // zero-based
|
||||
int col = -1; // zero-based
|
||||
};
|
||||
|
||||
struct Notification {
|
||||
int id = 0;
|
||||
NotificationLevel level = NotificationLevel::Info;
|
||||
std::string message;
|
||||
double timestamp = 0.0;
|
||||
float durationSeconds = 4.0f;
|
||||
bool dismissed = false;
|
||||
bool read = false;
|
||||
bool hasTarget = false;
|
||||
NotificationTarget target;
|
||||
};
|
||||
|
||||
class NotificationSystem {
|
||||
public:
|
||||
float toastDurationSeconds = 4.0f;
|
||||
bool showHistory = false;
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message) {
|
||||
notifyAt(level, message, ImGui::GetTime(), nullptr);
|
||||
}
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message, const NotificationTarget& target) {
|
||||
notifyAt(level, message, ImGui::GetTime(), &target);
|
||||
}
|
||||
|
||||
void notifyAt(NotificationLevel level, const std::string& message, double nowSeconds) {
|
||||
notifyAt(level, message, nowSeconds, nullptr);
|
||||
}
|
||||
|
||||
void notifyAt(NotificationLevel level,
|
||||
const std::string& message,
|
||||
double nowSeconds,
|
||||
const NotificationTarget* target) {
|
||||
Notification n;
|
||||
n.id = nextId++;
|
||||
n.level = level;
|
||||
n.message = message;
|
||||
n.timestamp = nowSeconds;
|
||||
n.durationSeconds = toastDurationSeconds;
|
||||
if (target) {
|
||||
n.hasTarget = true;
|
||||
n.target = *target;
|
||||
}
|
||||
history.push_back(std::move(n));
|
||||
}
|
||||
|
||||
void dismiss(int id) {
|
||||
for (auto& n : history) {
|
||||
if (n.id == id) {
|
||||
n.dismissed = true;
|
||||
n.read = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void markAllRead() {
|
||||
for (auto& n : history) {
|
||||
n.read = true;
|
||||
}
|
||||
}
|
||||
|
||||
int unreadCount() const {
|
||||
int count = 0;
|
||||
for (const auto& n : history) {
|
||||
if (!n.read) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const std::vector<Notification>& getHistory() const {
|
||||
return history;
|
||||
}
|
||||
|
||||
void renderToasts(const std::function<void(const Notification&)>& onNavigate = {}) {
|
||||
const double now = ImGui::GetTime();
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
const ImVec2 base(viewport->WorkPos.x + viewport->WorkSize.x,
|
||||
viewport->WorkPos.y + viewport->WorkSize.y);
|
||||
const float padding = 12.0f;
|
||||
const float toastWidth = 360.0f;
|
||||
float y = base.y - padding;
|
||||
|
||||
for (int i = (int)history.size() - 1; i >= 0; --i) {
|
||||
auto& n = history[i];
|
||||
if (n.dismissed) continue;
|
||||
if ((now - n.timestamp) > n.durationSeconds) continue;
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoNav |
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(base.x - padding, y), ImGuiCond_Always, ImVec2(1.0f, 1.0f));
|
||||
ImGui::SetNextWindowSize(ImVec2(toastWidth, 0.0f), ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12, 10));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 0.92f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.22f, 0.22f, 0.22f, 0.9f));
|
||||
ImGui::Begin(("##toast_" + std::to_string(n.id)).c_str(), nullptr, flags);
|
||||
|
||||
ImVec4 color = levelColor(n.level);
|
||||
ImGui::TextColored(color, "%s", levelLabel(n.level));
|
||||
ImGui::SameLine(0.0f, 8.0f);
|
||||
ImGui::TextWrapped("%s", n.message.c_str());
|
||||
if (n.hasTarget) {
|
||||
std::string loc = n.target.path;
|
||||
if (n.target.line >= 0) {
|
||||
loc += ":" + std::to_string(n.target.line + 1);
|
||||
if (n.target.col >= 0) loc += ":" + std::to_string(n.target.col + 1);
|
||||
}
|
||||
ImGui::TextDisabled("%s", loc.c_str());
|
||||
}
|
||||
|
||||
bool clicked = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) &&
|
||||
ImGui::IsMouseClicked(0);
|
||||
ImVec2 size = ImGui::GetWindowSize();
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (clicked) {
|
||||
n.read = true;
|
||||
n.dismissed = true;
|
||||
if (n.hasTarget && onNavigate) {
|
||||
onNavigate(n);
|
||||
}
|
||||
}
|
||||
|
||||
y -= size.y + 8.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void renderHistoryWindow(const std::function<void(const Notification&)>& onNavigate = {}) {
|
||||
if (!showHistory) {
|
||||
historyWasOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!historyWasOpen) {
|
||||
markAllRead();
|
||||
historyWasOpen = true;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 360), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Notifications", &showHistory)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (history.empty()) {
|
||||
ImGui::TextDisabled("(no notifications yet)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginChild("##notificationHistory", ImVec2(0, 0), false);
|
||||
for (const auto& n : history) {
|
||||
ImGui::PushID(n.id);
|
||||
ImVec4 color = levelColor(n.level);
|
||||
ImGui::TextColored(color, "%s", levelLabel(n.level));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(n.message.c_str(), false)) {
|
||||
if (n.hasTarget && onNavigate) {
|
||||
onNavigate(n);
|
||||
}
|
||||
}
|
||||
if (n.hasTarget) {
|
||||
std::string loc = n.target.path;
|
||||
if (n.target.line >= 0) {
|
||||
loc += ":" + std::to_string(n.target.line + 1);
|
||||
if (n.target.col >= 0) loc += ":" + std::to_string(n.target.col + 1);
|
||||
}
|
||||
ImGui::TextDisabled("%s", loc.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
private:
|
||||
int nextId = 1;
|
||||
bool historyWasOpen = false;
|
||||
std::vector<Notification> history;
|
||||
|
||||
static const char* levelLabel(NotificationLevel level) {
|
||||
switch (level) {
|
||||
case NotificationLevel::Success: return "Success";
|
||||
case NotificationLevel::Warning: return "Warning";
|
||||
case NotificationLevel::Error: return "Error";
|
||||
case NotificationLevel::Info:
|
||||
default:
|
||||
return "Info";
|
||||
}
|
||||
}
|
||||
|
||||
static ImVec4 levelColor(NotificationLevel level) {
|
||||
switch (level) {
|
||||
case NotificationLevel::Success: return ImVec4(0.25f, 0.78f, 0.35f, 1.0f);
|
||||
case NotificationLevel::Warning: return ImVec4(0.95f, 0.72f, 0.28f, 1.0f);
|
||||
case NotificationLevel::Error: return ImVec4(0.90f, 0.35f, 0.35f, 1.0f);
|
||||
case NotificationLevel::Info:
|
||||
default:
|
||||
return ImVec4(0.40f, 0.70f, 0.95f, 1.0f);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,6 +298,12 @@ int main(int, char**) {
|
||||
renderBottomPanel(state);
|
||||
renderMemoryStrategiesPanel(state);
|
||||
renderStatusBar(state);
|
||||
state.notifications.renderHistoryWindow([&](const Notification& note) {
|
||||
if (note.hasTarget) state.navigateToTarget(note.target);
|
||||
});
|
||||
state.notifications.renderToasts([&](const Notification& note) {
|
||||
if (note.hasTarget) state.navigateToTarget(note.target);
|
||||
});
|
||||
|
||||
// Check for exit request from menu
|
||||
if (state.exitRequested) done = true;
|
||||
|
||||
@@ -13,8 +13,30 @@ static void renderBottomPanel(EditorState& state) {
|
||||
if (ImGui::BeginTabItem("Output")) {
|
||||
ImGui::PushFont(state.monoFont);
|
||||
ImGui::BeginChild("##outputScroll", ImVec2(0, 0), false);
|
||||
ImGui::TextUnformatted(state.outputLog.c_str());
|
||||
// Auto-scroll to bottom
|
||||
for (const auto& note : state.notifications.getHistory()) {
|
||||
const char* label = "Info";
|
||||
ImVec4 color = ImVec4(0.40f, 0.70f, 0.95f, 1.0f);
|
||||
switch (note.level) {
|
||||
case NotificationLevel::Success:
|
||||
label = "Success";
|
||||
color = ImVec4(0.25f, 0.78f, 0.35f, 1.0f);
|
||||
break;
|
||||
case NotificationLevel::Warning:
|
||||
label = "Warning";
|
||||
color = ImVec4(0.95f, 0.72f, 0.28f, 1.0f);
|
||||
break;
|
||||
case NotificationLevel::Error:
|
||||
label = "Error";
|
||||
color = ImVec4(0.90f, 0.35f, 0.35f, 1.0f);
|
||||
break;
|
||||
case NotificationLevel::Info:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ImGui::TextColored(color, "[%s]", label);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextWrapped("%s", note.message.c_str());
|
||||
}
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20)
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
ImGui::EndChild();
|
||||
@@ -86,7 +108,7 @@ static void renderBottomPanel(EditorState& state) {
|
||||
ImGui::PushFont(state.monoFont);
|
||||
renderAgentMarketplace(state.agent.marketplace,
|
||||
state.agent.registry,
|
||||
state.outputLog,
|
||||
state.notifications,
|
||||
state.workspaceRoot);
|
||||
ImGui::PopFont();
|
||||
ImGui::EndTabItem();
|
||||
@@ -261,10 +283,10 @@ static void renderBottomPanel(EditorState& state) {
|
||||
}
|
||||
if (!warning.empty()) {
|
||||
summary += " — warning: " + warning;
|
||||
state.outputLog += warning + "\n";
|
||||
state.notify(NotificationLevel::Warning, warning);
|
||||
}
|
||||
state.optimizeFoldSummary = summary;
|
||||
state.outputLog += summary + "\n";
|
||||
state.notify(NotificationLevel::Info, summary);
|
||||
if (nodes > 0) state.applyOrchestratorToActive();
|
||||
std::string afterText = state.active()->editBuf;
|
||||
state.openDiff(beforeText, afterText, false, 1, {tid});
|
||||
@@ -308,7 +330,7 @@ static void renderBottomPanel(EditorState& state) {
|
||||
summary += "no changes";
|
||||
}
|
||||
state.optimizeDeadCodeSummary = summary;
|
||||
state.outputLog += summary + "\n";
|
||||
state.notify(NotificationLevel::Info, summary);
|
||||
if (nodes > 0) state.applyOrchestratorToActive();
|
||||
std::string afterText = state.active()->editBuf;
|
||||
state.openDiff(beforeText, afterText, false, 2, {tid});
|
||||
@@ -354,10 +376,10 @@ static void renderBottomPanel(EditorState& state) {
|
||||
}
|
||||
if (!warning.empty()) {
|
||||
summary += " — warning: " + warning;
|
||||
state.outputLog += warning + "\n";
|
||||
state.notify(NotificationLevel::Warning, warning);
|
||||
}
|
||||
state.optimizeApplySummary = summary;
|
||||
state.outputLog += summary + "\n";
|
||||
state.notify(NotificationLevel::Info, summary);
|
||||
if (totalNodes > 0) state.applyOrchestratorToActive();
|
||||
std::string afterText = state.active()->editBuf;
|
||||
state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce});
|
||||
@@ -460,10 +482,11 @@ static void renderBottomPanel(EditorState& state) {
|
||||
batch.setRoot(ast);
|
||||
auto res = batch.applySequence(state.diff.batchMutations);
|
||||
if (!res.success) {
|
||||
state.outputLog += res.error + "\n";
|
||||
state.notify(NotificationLevel::Error, res.error);
|
||||
} else {
|
||||
state.outputLog += "Refactor applied (" +
|
||||
std::to_string(res.appliedCount) + " mutations).\n";
|
||||
state.notify(NotificationLevel::Success,
|
||||
"Refactor applied (" +
|
||||
std::to_string(res.appliedCount) + " mutations).");
|
||||
state.applyOrchestratorToActive();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -155,8 +155,9 @@ static void renderRefactorPopup(EditorState& state) {
|
||||
if (!res.success) {
|
||||
state.refactorError = res.error;
|
||||
} else {
|
||||
state.outputLog += "Refactor applied (" +
|
||||
std::to_string(res.appliedCount) + " mutations).\n";
|
||||
state.notify(NotificationLevel::Success,
|
||||
"Refactor applied (" +
|
||||
std::to_string(res.appliedCount) + " mutations).");
|
||||
state.applyOrchestratorToActive();
|
||||
state.showRefactorPopup = false;
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
}
|
||||
state.mutator.setRoot(ast);
|
||||
auto res2 = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res2.error.empty()) state.outputLog += res2.error + "\n";
|
||||
if (!res2.error.empty()) state.notify(NotificationLevel::Error, res2.error);
|
||||
state.applyOrchestratorToActive();
|
||||
}
|
||||
}
|
||||
@@ -555,7 +555,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
target->spanEndLine, target->spanEndCol);
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -566,7 +566,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
target->spanEndLine, target->spanEndCol);
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -577,7 +577,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
target->spanEndLine, target->spanEndCol);
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -588,7 +588,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
target->spanEndLine, target->spanEndCol);
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -599,7 +599,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
target->spanEndLine, target->spanEndCol);
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -615,35 +615,35 @@ static void renderEditorPanel(EditorState& state) {
|
||||
if (ImGui::MenuItem("Tracing")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.setProperty(anno->id, "strategy", "Tracing");
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
} else if (anno->conceptType == "OwnerAnnotation") {
|
||||
if (ImGui::MenuItem("Single")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.setProperty(anno->id, "strategy", "Single");
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
} else if (anno->conceptType == "DeallocateAnnotation") {
|
||||
if (ImGui::MenuItem("Explicit")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.setProperty(anno->id, "strategy", "Explicit");
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
} else if (anno->conceptType == "LifetimeAnnotation") {
|
||||
if (ImGui::MenuItem("RAII")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.setProperty(anno->id, "strategy", "RAII");
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
} else if (anno->conceptType == "AllocateAnnotation") {
|
||||
if (ImGui::MenuItem("Static")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.setProperty(anno->id, "strategy", "Static");
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -659,7 +659,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
if (ImGui::MenuItem(label.c_str())) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.deleteNode(anno->id);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -682,7 +682,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
if (ImGui::MenuItem("Remove child annotation")) {
|
||||
state.mutator.setRoot(ast);
|
||||
auto res = state.mutator.deleteNode(lineConflict.childAnnoId);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Match parent strategy")) {
|
||||
@@ -690,7 +690,7 @@ static void renderEditorPanel(EditorState& state) {
|
||||
auto res = state.mutator.setProperty(lineConflict.childAnnoId,
|
||||
"strategy",
|
||||
lineConflict.parentStrategy);
|
||||
if (!res.error.empty()) state.outputLog += res.error + "\n";
|
||||
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
||||
if (res.error.empty()) mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ static void renderMenuBar(EditorState& state) {
|
||||
auto path = FileDialog::openFile({"Open Project", {"*.whetstone"}, state.lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
if (!state.loadProject(path)) {
|
||||
state.outputLog += "Failed to open project: " + path + "\n";
|
||||
state.notify(NotificationLevel::Error,
|
||||
"Failed to open project: " + path);
|
||||
} else {
|
||||
state.lastDialogPath = path;
|
||||
}
|
||||
@@ -59,7 +60,8 @@ static void renderMenuBar(EditorState& state) {
|
||||
auto path = FileDialog::saveFile({"Save Project", {"*.whetstone"}, state.lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
if (!state.saveProject(path)) {
|
||||
state.outputLog += "Failed to save project: " + path + "\n";
|
||||
state.notify(NotificationLevel::Error,
|
||||
"Failed to save project: " + path);
|
||||
} else {
|
||||
state.lastDialogPath = path;
|
||||
}
|
||||
|
||||
@@ -80,9 +80,11 @@ static void renderSettingsPanel(EditorState& state) {
|
||||
UpdateChecker checker;
|
||||
auto info = checker.check(state.settings.getUpdateUrl());
|
||||
if (info.available) {
|
||||
state.outputLog += "[update] Available: " + info.version + " (" + info.url + ")\n";
|
||||
state.notify(NotificationLevel::Info,
|
||||
"[update] Available: " + info.version + " (" + info.url + ")");
|
||||
} else {
|
||||
state.outputLog += "[update] No update info (offline/stub).\n";
|
||||
state.notify(NotificationLevel::Info,
|
||||
"[update] No update info (offline/stub).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ static void renderDependenciesPanel(EditorState& state) {
|
||||
if (!state.library.showDependencyPanel) return;
|
||||
ImGui::Begin("Dependencies", &state.library.showDependencyPanel);
|
||||
ImGui::PushFont(state.uiFont);
|
||||
renderDependencyPanel(state.library.dependencyPanel, state.workspaceRoot, state.outputLog);
|
||||
renderDependencyPanel(state.library.dependencyPanel, state.workspaceRoot, state.notifications);
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
if (state.library.dependencyPanel.needsIndex) {
|
||||
@@ -100,7 +100,7 @@ static void renderLibrariesPanel(EditorState& state) {
|
||||
state.library.libraryIndex,
|
||||
insertText,
|
||||
insertLibrary,
|
||||
state.outputLog)) {
|
||||
state.notifications)) {
|
||||
state.ensureImportForSymbol(insertLibrary, insertText);
|
||||
state.insertTextAtCursor(insertText);
|
||||
}
|
||||
@@ -123,7 +123,7 @@ static void renderCompositionPanel(EditorState& state) {
|
||||
auto funcs = state.library.primitives.getAvailableFunctions(nodeId);
|
||||
primitives.insert(primitives.end(), funcs.begin(), funcs.end());
|
||||
std::string code;
|
||||
if (renderCompositionPanel(state.library.compositionPanel, primitives, code, state.outputLog)) {
|
||||
if (renderCompositionPanel(state.library.compositionPanel, primitives, code, state.notifications)) {
|
||||
state.insertTextAtCursor(code);
|
||||
}
|
||||
ImGui::PopFont();
|
||||
@@ -134,7 +134,9 @@ static void renderEmacsPackagesPanel(EditorState& state) {
|
||||
if (!state.emacsState.showEmacsPackagesPanel) return;
|
||||
ImGui::Begin("Emacs Packages", &state.emacsState.showEmacsPackagesPanel);
|
||||
ImGui::PushFont(state.uiFont);
|
||||
if (renderEmacsPackageBrowser(state.emacsState.emacsPackages, state.emacsState.emacs, state.outputLog)) {
|
||||
if (renderEmacsPackageBrowser(state.emacsState.emacsPackages,
|
||||
state.emacsState.emacs,
|
||||
state.notifications)) {
|
||||
state.emacsState.emacsFunctionIndexDirty = true;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
@@ -188,7 +190,9 @@ static void renderMinibuffer(EditorState& state) {
|
||||
state.emacsState.emacsKeys.minibufferBuf,
|
||||
sizeof(state.emacsState.emacsKeys.minibufferBuf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
emacsExecuteMinibuffer(state.emacsState.emacsKeys, state.emacsState.emacs, state.outputLog);
|
||||
emacsExecuteMinibuffer(state.emacsState.emacsKeys,
|
||||
state.emacsState.emacs,
|
||||
state.notifications);
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
@@ -237,7 +241,8 @@ static void renderMemoryStrategiesPanel(EditorState& state) {
|
||||
|
||||
if (ImGui::Button("Export JSON")) {
|
||||
auto j = buildAnnotationSummaryJson(entries);
|
||||
state.outputLog += "Annotation summary:\\n" + j.dump(2) + "\\n";
|
||||
state.notify(NotificationLevel::Info,
|
||||
"Annotation summary:\\n" + j.dump(2));
|
||||
}
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
@@ -68,6 +68,16 @@ static void renderStatusBar(EditorState& state) {
|
||||
ImGui::SameLine(0, 30);
|
||||
ImGui::Text("UTF-8");
|
||||
|
||||
ImGui::SameLine(0, 30);
|
||||
int unread = state.notifications.unreadCount();
|
||||
std::string notifLabel = "Notifications";
|
||||
if (unread > 0) {
|
||||
notifLabel += " (" + std::to_string(unread) + ")";
|
||||
}
|
||||
if (ImGui::Button(notifLabel.c_str())) {
|
||||
state.notifications.showHistory = !state.notifications.showHistory;
|
||||
}
|
||||
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
41
editor/tests/step169_integration_test.cpp
Normal file
41
editor/tests/step169_integration_test.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Step 169: Notification system integration wiring checks.
|
||||
|
||||
#include <cassert>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static void assertContains(const std::string& text, const std::string& needle) {
|
||||
assert(text.find(needle) != std::string::npos);
|
||||
}
|
||||
|
||||
int main() {
|
||||
assert(fs::exists("src/NotificationSystem.h"));
|
||||
|
||||
const std::string editorState = readFile("src/EditorState.h");
|
||||
assertContains(editorState, "NotificationSystem");
|
||||
assertContains(editorState, "notifications");
|
||||
|
||||
const std::string statusBar = readFile("src/panels/StatusBarPanel.h");
|
||||
assertContains(statusBar, "Notifications");
|
||||
assertContains(statusBar, "state.notifications");
|
||||
|
||||
const std::string mainCpp = readFile("src/main.cpp");
|
||||
assertContains(mainCpp, "renderHistoryWindow");
|
||||
assertContains(mainCpp, "renderToasts");
|
||||
|
||||
const std::string bottomPanel = readFile("src/panels/BottomPanel.h");
|
||||
assertContains(bottomPanel, "state.notifications");
|
||||
|
||||
printf("step169_integration_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
48
editor/tests/step169_test.cpp
Normal file
48
editor/tests/step169_test.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Step 169: Notification system unit checks.
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "NotificationSystem.h"
|
||||
|
||||
static const Notification* findById(const std::vector<Notification>& list, int id) {
|
||||
for (const auto& n : list) {
|
||||
if (n.id == id) return &n;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int main() {
|
||||
NotificationSystem notifications;
|
||||
notifications.toastDurationSeconds = 5.0f;
|
||||
|
||||
notifications.notifyAt(NotificationLevel::Info, "hello", 1.0);
|
||||
assert(notifications.getHistory().size() == 1);
|
||||
assert(notifications.unreadCount() == 1);
|
||||
|
||||
NotificationTarget target;
|
||||
target.path = "C:\\temp\\example.cpp";
|
||||
target.line = 2;
|
||||
target.col = 5;
|
||||
notifications.notifyAt(NotificationLevel::Error, "boom", 2.0, &target);
|
||||
assert(notifications.getHistory().size() == 2);
|
||||
|
||||
const Notification& last = notifications.getHistory().back();
|
||||
assert(last.hasTarget);
|
||||
assert(last.target.path == target.path);
|
||||
assert(last.target.line == target.line);
|
||||
assert(last.target.col == target.col);
|
||||
|
||||
int id = last.id;
|
||||
notifications.dismiss(id);
|
||||
const Notification* found = findById(notifications.getHistory(), id);
|
||||
assert(found);
|
||||
assert(found->dismissed);
|
||||
assert(found->read);
|
||||
|
||||
notifications.markAllRead();
|
||||
assert(notifications.unreadCount() == 0);
|
||||
|
||||
printf("step169_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -63,7 +63,7 @@ first — all UX work becomes easier when panels are modular.
|
||||
All files under 600 lines after split.
|
||||
*Modifies:* 5+ oversized headers
|
||||
|
||||
- [ ] **Step 169: Notification / toast system**
|
||||
- [x] **Step 169: Notification / toast system**
|
||||
Replace ad-hoc `outputLog +=` with a structured notification system.
|
||||
`NotificationSystem.h`:
|
||||
- Notification types: Success, Warning, Error, Info
|
||||
|
||||
Reference in New Issue
Block a user