diff --git a/PROGRESS.md b/PROGRESS.md index 15537e8..e59e740 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -514,3 +514,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 164: Update checker stub + installer manifests/config, settings UI for update URL. 3/3 tests pass. | | 2026-02-10 | Codex | Step 165: Sprint 5 integration tests across primitives, projection, pipeline, composition, and Emacs indexing. 8/8 tests pass. | | 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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 3884865..78f29b4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -946,6 +946,11 @@ target_link_libraries(step166_test PRIVATE tree_sitter_rust tree_sitter_go) +add_executable(step167_test tests/step167_test.cpp) +target_include_directories(step167_test PRIVATE src) +target_link_libraries(step167_test PRIVATE + nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 1dc4320..a0ee81d 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -49,6 +49,12 @@ #include "HelpPanel.h" #include "Telemetry.h" #include "UpdateChecker.h" +#include "state/SearchState.h" +#include "state/AgentState.h" +#include "state/BuildState.h" +#include "state/LibraryState.h" +#include "state/EmacsState.h" +#include "state/UIFlags.h" #include "DependencyPanel.h" #include "LibraryIndexer.h" #include "LibraryBrowserPanel.h" @@ -147,91 +153,36 @@ struct BufferState { }; struct EditorState { + // Rendering context (set by main before first frame) + ImFont* monoFont = nullptr; + ImFont* uiFont = nullptr; + float baseFontSize = 15.0f; + std::string lastDialogPath; + bool exitRequested = false; + KeybindingManager keys; BufferManager buffers; std::map> bufferStates; BufferState* activeBuffer = nullptr; - EmacsConnection emacs; - // Find/Replace state - bool showFind = false; - char findBuf[256] = {}; - char replaceBuf[256] = {}; - int lastFindPos = 0; - bool showProjectSearch = false; - char searchQuery[256] = {}; - char searchInclude[256] = {}; - char searchExclude[256] = {}; - bool searchUseRegex = true; - std::vector searchResults; - bool showGoToLine = false; - char goToLineBuf[64] = {}; - bool goToLineError = false; + SearchState search; + AgentState agent; + BuildState build; + LibraryState library; + EmacsState emacsState; + UIFlags ui; - // Bottom panel - int bottomTab = 0; // 0=Output, 1=AST, 2=Highlighted + // Bottom panel + global log std::string outputLog; - bool showTerminalPanel = false; - TerminalPanel terminal; - bool runInProgress = false; - bool hasRunResult = false; - int lastRunExitCode = 0; - std::string lastRunCommand; - std::unique_ptr agentServer; - MockWebSocketTransport* agentTransport = nullptr; - int agentPort = 8765; - std::vector agentLog; - std::map agentRoles; - WorkflowRecorder workflowRecorder; - AgentRegistry agentRegistry; - AgentMarketplaceState agentMarketplace; HelpPanelState helpPanel; Telemetry telemetry; - BuildSystem::Type buildType = BuildSystem::Type::None; - std::vector buildErrors; - std::string lastBuildOutput; - std::string lastBuildCommand; - bool showDependencyPanel = true; - DependencyPanelState dependencyPanel; - bool showLibraryBrowserPanel = true; - LibraryBrowserState libraryBrowser; - bool showCompositionPanel = false; - CompositionPanelState compositionPanel; - bool showEmacsPackagesPanel = false; - EmacsPackageBrowserState emacsPackages; - EmacsFunctionIndex emacsFunctionIndex; - bool emacsFunctionIndexDirty = true; - EmacsKeybindingState emacsKeys; - bool showEmacsBridgePanel = false; - bool showOrgPanel = true; - OrgDocumentState orgDoc; - int orgTempCounter = 0; - struct LibraryIndexRequest { - std::string name; - std::string version; - std::string source; - int symbolsRequestId = -1; - int completionRequestId = -1; - }; - std::vector libraryIndexRequests; - LibraryIndexData libraryIndex; - PrimitivesRegistry primitives; - - // Custom editor widget state - bool showWhitespace = false; - bool showMinimap = false; - bool showAnnotations = false; - bool showOutline = true; - bool showLineNumbers = true; char outlineFilter[128] = {}; - LayoutPreset layoutPreset = LayoutPreset::VSCode; WelcomeScreen welcome; std::string workspaceRoot; FileTree fileTree; FileNode fileTreeRoot; bool fileTreeDirty = true; FileWatcher watcher; - ProjectSearch projectSearch; std::shared_ptr lspTransport; std::shared_ptr lsp; bool symbolsPending = false; @@ -256,8 +207,6 @@ struct EditorState { std::vector whetstoneDiagnostics; std::vector emacsDiagnostics; SettingsManager settings; - bool showLspSettings = false; - bool showSettingsPanel = false; double lastAutoSave = 0.0; ASTMutationAPI mutator; std::vector suggestions; @@ -476,11 +425,11 @@ struct EditorState { activeBuffer = state.get(); bufferStates[path] = std::move(state); active()->orchestratorDirty = true; - primitives.setRoot(activeAST()); - primitives.setLanguage(active()->language); + library.primitives.setRoot(activeAST()); + library.primitives.setLanguage(active()->language); recordUndoSnapshot(); if (language == "elisp") { - emacsFunctionIndexDirty = true; + emacsState.emacsFunctionIndexDirty = true; } if (path.rfind("(untitled", 0) != 0) watcher.watch(path); if (lsp && path.rfind("(untitled", 0) != 0) { @@ -569,13 +518,13 @@ struct EditorState { buffers.switchToBuffer(path); activeBuffer = bufferStates[path].get(); if (active()) active()->orchestratorDirty = true; - primitives.setRoot(activeAST()); - primitives.setLanguage(active()->language); + library.primitives.setRoot(activeAST()); + library.primitives.setLanguage(active()->language); symbolsPending = true; symbolsLastChange = ImGui::GetTime(); if (lsp) lsp->clearDocumentSymbols(); if (active() && active()->language == "elisp") { - emacsFunctionIndexDirty = true; + emacsState.emacsFunctionIndexDirty = true; } } @@ -671,7 +620,7 @@ struct EditorState { if (!project.workspaceRoot.empty()) { workspaceRoot = project.workspaceRoot; fileTreeDirty = true; - projectSearch.setRoot(workspaceRoot); + search.projectSearch.setRoot(workspaceRoot); } std::string activePath = project.activePath; @@ -718,7 +667,7 @@ struct EditorState { SessionData session; session.workspaceRoot = workspaceRoot; session.activePath = active() ? active()->path : ""; - session.layoutPreset = LayoutManager::presetName(layoutPreset); + session.layoutPreset = LayoutManager::presetName(ui.layoutPreset); session.imguiIni = imguiIni; for (const auto& bufPath : buffers.getOpenBuffers()) { auto it = bufferStates.find(bufPath); @@ -761,9 +710,9 @@ struct EditorState { if (!session.workspaceRoot.empty()) { workspaceRoot = session.workspaceRoot; fileTreeDirty = true; - projectSearch.setRoot(workspaceRoot); + search.projectSearch.setRoot(workspaceRoot); } - layoutPreset = LayoutManager::presetFromName(session.layoutPreset); + ui.layoutPreset = LayoutManager::presetFromName(session.layoutPreset); for (const auto& buf : session.buffers) { doOpen(buf.path, bufferModeFromString(buf.mode)); auto it = bufferStates.find(buf.path); @@ -788,9 +737,9 @@ struct EditorState { } void applySettingsToState() { - showMinimap = settings.getShowMinimap(); - showLineNumbers = settings.getShowLineNumbers(); - layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset()); + ui.showMinimap = settings.getShowMinimap(); + ui.showLineNumbers = settings.getShowLineNumbers(); + ui.layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset()); keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile())); registerCommands(); applyTabSizeToBuffers(settings.getTabSize()); @@ -802,9 +751,9 @@ struct EditorState { } void saveSettingsToDisk() { - settings.setShowMinimap(showMinimap); - settings.setShowLineNumbers(showLineNumbers); - settings.setLayoutPreset(LayoutManager::presetName(layoutPreset)); + settings.setShowMinimap(ui.showMinimap); + settings.setShowLineNumbers(ui.showLineNumbers); + settings.setLayoutPreset(LayoutManager::presetName(ui.layoutPreset)); settings.setKeybindingProfile(KeybindingManager::profileName(keys.getProfile())); std::filesystem::create_directories(settingsFilePath().parent_path()); settings.saveToFile(settingsFilePath().string()); @@ -813,7 +762,7 @@ struct EditorState { void init() { outputLog = "Whetstone Editor ready.\n"; workspaceRoot = std::filesystem::current_path().string(); - projectSearch.setRoot(workspaceRoot); + search.projectSearch.setRoot(workspaceRoot); fileTreeDirty = true; loadRecentFiles(); loadSettingsFromDisk(); @@ -843,7 +792,8 @@ struct EditorState { : std::filesystem::path(workspaceRoot); std::filesystem::path dir = root / ".whetstone_org"; std::filesystem::create_directories(dir); - std::string name = "org_block_" + std::to_string(++orgTempCounter) + orgTempExtension(language); + std::string name = "org_block_" + std::to_string(++emacsState.orgTempCounter) + + orgTempExtension(language); std::filesystem::path filePath = dir / name; std::ofstream out(filePath.string(), std::ios::binary); out << code; @@ -852,18 +802,18 @@ struct EditorState { std::string runOrgBlock(const std::string& language, const std::string& code) { if (language == "elisp") { - std::string result = emacs.sendCommand(ElispCommandBuilder::eval(code)); - if (!emacs.getLastError().empty() && result == "error") { - return emacs.getLastError(); + std::string result = emacsState.emacs.sendCommand(ElispCommandBuilder::eval(code)); + if (!emacsState.emacs.getLastError().empty() && result == "error") { + return emacsState.emacs.getLastError(); } return result.empty() ? "OK" : result; } std::string path = writeOrgTempFile(language, code); std::string cmd = buildRunCommand(path, language, false); if (cmd.empty()) return "No runner for language: " + language; - showTerminalPanel = true; + build.showTerminalPanel = true; std::string output; - int codeExit = terminal.runAndCapture(workspaceRoot, cmd, output); + int codeExit = build.terminal.runAndCapture(workspaceRoot, cmd, output); output += "[exit code: " + std::to_string(codeExit) + "]"; return output; } @@ -872,7 +822,7 @@ struct EditorState { emacsDiagnostics.clear(); std::string initPath = resolveEmacsInitPath(); std::string log; - bool ok = emacs.startDaemonWithConfig(initPath, log); + bool ok = emacsState.emacs.startDaemonWithConfig(initPath, log); if (!log.empty()) { outputLog += "[emacs] " + log + "\n"; } @@ -880,7 +830,7 @@ struct EditorState { EditorDiagnostic d; d.uri = "emacs://init"; d.severity = 1; - d.message = "[Emacs] " + emacs.getLastError(); + d.message = "[Emacs] " + emacsState.emacs.getLastError(); d.source = "EmacsDaemon"; emacsDiagnostics.push_back(d); return; @@ -914,57 +864,57 @@ struct EditorState { void initAgentServer() { auto transport = std::make_unique(); - agentTransport = transport.get(); - agentServer = std::make_unique(std::move(transport)); - agentServer->setRequestHandler([this](const json& request, - const std::string& sessionId) { + agent.transport = transport.get(); + agent.server = std::make_unique(std::move(transport)); + agent.server->setRequestHandler([this](const json& request, + const std::string& sessionId) { return processAgentRequest(request, sessionId); }); - agentServer->setSessionEventCallback([this](const AgentSession& s, - const std::string& event) { + agent.server->setSessionEventCallback([this](const AgentSession& s, + const std::string& event) { logAgentEvent("Agent " + s.sessionId + " " + event); if (event == "connected") { - agentRoles[s.sessionId] = AgentRole::Linter; + agent.roles[s.sessionId] = AgentRole::Linter; } else if (event == "disconnected") { - agentRoles.erase(s.sessionId); + agent.roles.erase(s.sessionId); } }); - agentServer->setRequestLogCallback([this](const std::string& sid, - const json& req, - const json& res) { + agent.server->setRequestLogCallback([this](const std::string& sid, + const json& req, + const json& res) { std::string method = req.value("method", ""); bool ok = !res.contains("error"); logAgentEvent("RPC " + sid + " " + method + (ok ? " ok" : " error")); - workflowRecorder.record(sid, req, res); + agent.workflowRecorder.record(sid, req, res); }); - if (agentServer->start(agentPort)) { - logAgentEvent("Agent server started on port " + std::to_string(agentPort)); + if (agent.server->start(agent.port)) { + logAgentEvent("Agent server started on port " + std::to_string(agent.port)); } else { - logAgentEvent("Agent server failed to start on port " + std::to_string(agentPort)); + logAgentEvent("Agent server failed to start on port " + std::to_string(agent.port)); } } void shutdownAgentServer() { - if (agentServer) agentServer->stop(); + if (agent.server) agent.server->stop(); } void logAgentEvent(const std::string& msg) { - agentLog.push_back(msg); + agent.log.push_back(msg); } AgentRole getAgentRole(const std::string& sessionId) const { - auto it = agentRoles.find(sessionId); - return it != agentRoles.end() ? it->second : AgentRole::Linter; + auto it = agent.roles.find(sessionId); + return it != agent.roles.end() ? it->second : AgentRole::Linter; } void setAgentRole(const std::string& sessionId, AgentRole role) { - agentRoles[sessionId] = role; + agent.roles[sessionId] = role; } std::string agentActorLabel(const std::string& sessionId) const { std::string label = "agent:" + sessionId; - if (agentServer) { - if (const auto* sess = agentServer->getSession(sessionId)) { + if (agent.server) { + if (const auto* sess = agent.server->getSession(sessionId)) { if (!sess->agentName.empty()) { label = "agent:" + sess->agentName; } @@ -974,18 +924,18 @@ struct EditorState { } void refreshBuildSystem() { - buildType = BuildSystem::detect(workspaceRoot); + build.buildType = BuildSystem::detect(workspaceRoot); } int runBuildCommand(const BuildCommand& cmd) { if (workspaceRoot.empty()) { - terminal.append("[build] No workspace root set.\n"); + build.terminal.append("[build] No workspace root set.\n"); return -1; } - showTerminalPanel = true; - lastBuildCommand = cmd.command; - int code = terminal.runAndCapture(workspaceRoot, cmd.command, lastBuildOutput); - buildErrors = BuildSystem::parseErrors(lastBuildOutput); + build.showTerminalPanel = true; + 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"; return code; } @@ -999,23 +949,23 @@ struct EditorState { } void requestLibraryIndex() { - libraryIndex.symbolsByLibrary.clear(); - libraryIndex.completionsByLibrary.clear(); - libraryIndexRequests.clear(); + library.libraryIndex.symbolsByLibrary.clear(); + library.libraryIndex.completionsByLibrary.clear(); + library.libraryIndexRequests.clear(); if (!isStructured() || !activeAST()) { outputLog += "[deps] Library index skipped: no structured AST.\n"; return; } if (!lsp || !lspTransport || !lspTransport->isOpen()) { - applyStubFallback(libraryIndex, dependencyPanel.deps, workspaceRoot); + applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot); rebuildExternalModulesFromIndex(); outputLog += "[deps] Library index uses cached symbols (LSP offline).\n"; return; } - for (const auto& dep : dependencyPanel.deps) { + for (const auto& dep : library.dependencyPanel.deps) { if (dep.name.empty()) continue; - LibraryIndexRequest req; + LibraryState::LibraryIndexRequest req; req.name = dep.name; req.version = dep.version; req.source = dep.source; @@ -1026,22 +976,22 @@ struct EditorState { req.completionRequestId = lsp->requestLibraryCompletion( toFileUri(active()->path), line, col, dep.name + "."); } - libraryIndexRequests.push_back(req); + library.libraryIndexRequests.push_back(req); } - applyStubFallback(libraryIndex, dependencyPanel.deps, workspaceRoot); + applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot); rebuildExternalModulesFromIndex(); outputLog += "[deps] Library index requests queued: " + - std::to_string(libraryIndexRequests.size()) + "\n"; + std::to_string(library.libraryIndexRequests.size()) + "\n"; } void processLibraryIndexResponses() { if (!lsp) return; bool updated = false; - for (auto& req : libraryIndexRequests) { + for (auto& req : library.libraryIndexRequests) { if (req.symbolsRequestId >= 0) { std::vector symbols; if (lsp->takeWorkspaceSymbols(req.symbolsRequestId, symbols)) { - libraryIndex.symbolsByLibrary[req.name] = std::move(symbols); + library.libraryIndex.symbolsByLibrary[req.name] = std::move(symbols); req.symbolsRequestId = -1; updated = true; } @@ -1055,7 +1005,7 @@ struct EditorState { if (!item.label.empty()) labels.push_back(item.label); } if (!labels.empty()) { - libraryIndex.completionsByLibrary[req.name] = std::move(labels); + library.libraryIndex.completionsByLibrary[req.name] = std::move(labels); } req.completionRequestId = -1; updated = true; @@ -1063,7 +1013,7 @@ struct EditorState { } } if (updated) { - applyStubFallback(libraryIndex, dependencyPanel.deps, workspaceRoot); + applyStubFallback(library.libraryIndex, library.dependencyPanel.deps, workspaceRoot); rebuildExternalModulesFromIndex(); outputLog += "[deps] Library index updated.\n"; } @@ -1072,24 +1022,24 @@ struct EditorState { void updateEmacsFunctionIndex() { if (!active() || active()->language != "elisp") return; std::string error; - auto packages = queryEmacsPackageList(emacs, false, outputLog, error); + auto packages = queryEmacsPackageList(emacsState.emacs, false, outputLog, error); if (!error.empty()) return; - refreshEmacsFunctionIndex(emacsFunctionIndex, emacs, packages, outputLog); - addEmacsSymbolsToLibraryIndex(libraryIndex, emacsFunctionIndex); - emacsFunctionIndexDirty = false; + refreshEmacsFunctionIndex(emacsState.emacsFunctionIndex, emacsState.emacs, packages, outputLog); + addEmacsSymbolsToLibraryIndex(library.libraryIndex, emacsState.emacsFunctionIndex); + emacsState.emacsFunctionIndexDirty = false; rebuildExternalModulesFromIndex(); outputLog += "[emacs] Indexed functions for " + - std::to_string(emacsFunctionIndex.functionsByPackage.size()) + + std::to_string(emacsState.emacsFunctionIndex.functionsByPackage.size()) + " packages.\n"; } void rebuildExternalModulesFromIndex() { Module* ast = activeAST(); if (!ast) return; - rebuildExternalModules(ast, dependencyPanel.deps, libraryIndex); + rebuildExternalModules(ast, library.dependencyPanel.deps, library.libraryIndex); if (active() && active()->language == "elisp") { int signatureId = 0; - appendEmacsExternalModules(ast, emacsFunctionIndex, signatureId); + appendEmacsExternalModules(ast, emacsState.emacsFunctionIndex, signatureId); } if (active()) active()->orchestratorDirty = true; } @@ -1181,11 +1131,11 @@ struct EditorState { std::string spec = params.value("spec", ""); bool preferImports = params.value("preferImports", true); - primitives.setRoot(ast); - primitives.setLanguage(active()->language); + library.primitives.setRoot(ast); + library.primitives.setLanguage(active()->language); AgentCodeGen gen; - AgentCodeGenResult genRes = gen.generate(spec, primitives, active()->language, preferImports); + AgentCodeGenResult genRes = gen.generate(spec, library.primitives, active()->language, preferImports); if (!genRes.node) { response["error"] = {{"code", -32020}, {"message", "Code generation failed"}}; return response; @@ -1217,7 +1167,7 @@ struct EditorState { if (method == "startWorkflowRecording") { auto params = request.contains("params") ? request["params"] : json::object(); std::string name = params.value("name", "workflow"); - workflowRecorder.startRecording(name, sessionId); + agent.workflowRecorder.startRecording(name, sessionId); response["result"] = { {"recording", true}, {"name", name} @@ -1226,12 +1176,12 @@ struct EditorState { } if (method == "stopWorkflowRecording") { - response["result"] = workflowRecorder.stopRecording(); + response["result"] = agent.workflowRecorder.stopRecording(); return response; } if (method == "getWorkflowRecording") { - response["result"] = workflowRecorder.exportWorkflow(); + response["result"] = agent.workflowRecorder.exportWorkflow(); return response; } @@ -1248,11 +1198,11 @@ struct EditorState { } auto requests = temp.buildReplayRequests(); json results = json::array(); - workflowRecorder.setReplaying(true); + agent.workflowRecorder.setReplaying(true); for (auto& req : requests) { results.push_back(processAgentRequest(req, sessionId)); } - workflowRecorder.setReplaying(false); + agent.workflowRecorder.setReplaying(false); response["result"] = { {"count", results.size()}, {"responses", results} @@ -1617,30 +1567,30 @@ struct EditorState { [this]() { doRedo(); }); registerCommand("search.find", "Search: Find/Replace", keys.getBinding("search.find").toString(), - [this]() { showFind = !showFind; }); + [this]() { search.showFind = !search.showFind; }); registerCommand("search.findInFiles", "Search: Find in Files", keys.getBinding("search.findInFiles").toString(), - [this]() { showProjectSearch = !showProjectSearch; }); + [this]() { search.showProjectSearch = !search.showProjectSearch; }); registerCommand("nav.goToLine", "Navigate: Go to Line", keys.getBinding("nav.goToLine").toString(), [this]() { - showGoToLine = true; - goToLineBuf[0] = '\0'; - goToLineError = false; + search.showGoToLine = true; + search.goToLineBuf[0] = '\0'; + search.goToLineError = false; }); registerCommand("view.whitespace", "View: Toggle Whitespace", "", - [this]() { showWhitespace = !showWhitespace; }); + [this]() { ui.showWhitespace = !ui.showWhitespace; }); registerCommand("view.minimap", "View: Toggle Minimap", "", - [this]() { showMinimap = !showMinimap; }); + [this]() { ui.showMinimap = !ui.showMinimap; }); registerCommand("view.annotations", "View: Toggle Annotations", "", - [this]() { showAnnotations = !showAnnotations; }); + [this]() { ui.showAnnotations = !ui.showAnnotations; }); registerCommand("view.outline", "View: Toggle Outline", "", - [this]() { showOutline = !showOutline; }); + [this]() { ui.showOutline = !ui.showOutline; }); registerCommand("view.toggleTerminal", "View: Toggle Terminal", keys.getBinding("view.toggleTerminal").toString(), - [this]() { showTerminalPanel = !showTerminalPanel; }); + [this]() { build.showTerminalPanel = !build.showTerminalPanel; }); registerCommand("view.lsp", "View: LSP Servers...", "", - [this]() { showLspSettings = !showLspSettings; }); + [this]() { ui.showLspSettings = !ui.showLspSettings; }); registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "", [this]() { if (!active()) return; @@ -1694,7 +1644,7 @@ struct EditorState { std::string prevLang = active()->language; active()->language = lang; active()->mode.setLanguage(lang); - primitives.setLanguage(lang); + library.primitives.setLanguage(lang); if (active()->generatedLanguage == prevLang) { active()->generatedLanguage = lang; active()->generatedMode.setLanguage(lang); @@ -1709,7 +1659,7 @@ struct EditorState { active()->orchestratorDirty = true; active()->highlightsDirty = true; if (lang == "elisp") { - emacsFunctionIndexDirty = true; + emacsState.emacsFunctionIndexDirty = true; } if (lang == "org") { active()->bufferMode = BufferManager::BufferMode::Text; @@ -1718,13 +1668,13 @@ struct EditorState { } bool handleEmacsKeyChord(const std::string& chord) { - if (layoutPreset != LayoutPreset::Emacs) return false; - return emacsHandleKeySequence(emacsKeys, emacs, chord, outputLog); + if (ui.layoutPreset != LayoutPreset::Emacs) return false; + return emacsHandleKeySequence(emacsState.emacsKeys, emacsState.emacs, chord, outputLog); } void refreshEmacsModeLine(double nowSeconds) { - if (layoutPreset != LayoutPreset::Emacs) return; - updateEmacsModeLine(emacsKeys, emacs, nowSeconds, outputLog); + if (ui.layoutPreset != LayoutPreset::Emacs) return; + updateEmacsModeLine(emacsState.emacsKeys, emacsState.emacs, nowSeconds, outputLog); } void pullFromEmacs() { @@ -1733,9 +1683,9 @@ struct EditorState { outputLog += "[emacs] Save file before syncing.\n"; return; } - std::string text = emacs.getBufferText(active()->path); - if (!emacs.getLastError().empty() && text == "error") { - outputLog += "[emacs] " + emacs.getLastError() + "\n"; + std::string text = emacsState.emacs.getBufferText(active()->path); + if (!emacsState.emacs.getLastError().empty() && text == "error") { + outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n"; return; } active()->editBuf = text; @@ -1749,8 +1699,8 @@ struct EditorState { outputLog += "[emacs] Save file before syncing.\n"; return; } - if (!emacs.setBufferText(active()->path, active()->editBuf)) { - outputLog += "[emacs] " + emacs.getLastError() + "\n"; + if (!emacsState.emacs.setBufferText(active()->path, active()->editBuf)) { + outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n"; return; } outputLog += "[emacs] Pushed buffer to Emacs.\n"; @@ -1762,8 +1712,8 @@ struct EditorState { outputLog += "[emacs] Save file before opening in Emacs.\n"; return; } - if (!emacs.openFileInEmacsFrame(active()->path)) { - outputLog += "[emacs] " + emacs.getLastError() + "\n"; + if (!emacsState.emacs.openFileInEmacsFrame(active()->path)) { + outputLog += "[emacs] " + emacsState.emacs.getLastError() + "\n"; return; } outputLog += "[emacs] Opened in Emacs frame.\n"; @@ -1805,8 +1755,8 @@ struct EditorState { bool runActiveFile(bool buildOnly) { if (!active()) return false; if (active()->path.rfind("(untitled", 0) == 0) { - terminal.append("[terminal] Save the file before running.\n"); - showTerminalPanel = true; + build.terminal.append("[terminal] Save the file before running.\n"); + build.showTerminalPanel = true; return false; } if (active()->modified) { @@ -1814,22 +1764,22 @@ struct EditorState { } std::string cmd = buildRunCommand(active()->path, active()->language, buildOnly); if (cmd.empty()) { - terminal.append("[terminal] No runner for language: " + active()->language + "\n"); - showTerminalPanel = true; + build.terminal.append("[terminal] No runner for language: " + active()->language + "\n"); + build.showTerminalPanel = true; return false; } std::string cwd = workspaceRoot.empty() ? std::filesystem::path(active()->path).parent_path().string() : workspaceRoot; - showTerminalPanel = true; - runInProgress = true; - hasRunResult = false; - lastRunCommand = cmd; - int code = terminal.runAndAppend(cwd, cmd); - terminal.append("[exit code: " + std::to_string(code) + "]\n"); - runInProgress = false; - hasRunResult = true; - lastRunExitCode = code; + build.showTerminalPanel = true; + build.runInProgress = true; + build.hasRunResult = false; + build.lastRunCommand = cmd; + int code = build.terminal.runAndAppend(cwd, cmd); + build.terminal.append("[exit code: " + std::to_string(code) + "]\n"); + build.runInProgress = false; + build.hasRunResult = true; + build.lastRunExitCode = code; return code == 0; } @@ -1842,8 +1792,8 @@ struct EditorState { active()->sync.syncNow(); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); } - primitives.setRoot(activeAST()); - primitives.setLanguage(active()->language); + library.primitives.setRoot(activeAST()); + library.primitives.setLanguage(active()->language); active()->orchestratorDirty = true; active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; @@ -1880,28 +1830,28 @@ struct EditorState { void doFind() { if (!active()) return; - if (strlen(findBuf) == 0) return; - int pos = active()->editor.find(findBuf, lastFindPos); + if (strlen(search.findBuf) == 0) return; + int pos = active()->editor.find(search.findBuf, search.lastFindPos); if (pos >= 0) { - lastFindPos = pos + 1; + search.lastFindPos = pos + 1; int line = 1, col = 1; for (int i = 0; i < pos && i < (int)active()->editBuf.size(); ++i) { if (active()->editBuf[i] == '\n') { ++line; col = 1; } else { ++col; } } active()->cursorLine = line; active()->cursorCol = col; - outputLog += "Found \"" + std::string(findBuf) + "\" at line " + + outputLog += "Found \"" + std::string(search.findBuf) + "\" at line " + std::to_string(line) + ", col " + std::to_string(col) + "\n"; } else { - lastFindPos = 0; - outputLog += "\"" + std::string(findBuf) + "\" not found.\n"; + search.lastFindPos = 0; + outputLog += "\"" + std::string(search.findBuf) + "\" not found.\n"; } } void doReplaceAll() { if (!active()) return; - if (strlen(findBuf) == 0) return; - int count = active()->editor.replaceAll(findBuf, replaceBuf); + if (strlen(search.findBuf) == 0) return; + int count = active()->editor.replaceAll(search.findBuf, search.replaceBuf); if (count > 0) { active()->editBuf = active()->editor.getContent(); if (active()->bufferMode == BufferManager::BufferMode::Structured) { diff --git a/editor/src/EditorUtils.h b/editor/src/EditorUtils.h index cefa15c..13ee6b5 100644 --- a/editor/src/EditorUtils.h +++ b/editor/src/EditorUtils.h @@ -1,6 +1,7 @@ #pragma once #include "EditorState.h" +#include "StringUtils.h" #include "imgui.h" #include "SyntaxHighlighter.h" @@ -410,6 +411,8 @@ static void collectOutlineFromAST(const Module* mod, std::vector& o } } +// trimCopy is in StringUtils.h + static std::string toLowerCopy(const std::string& input) { std::string out = input; std::transform(out.begin(), out.end(), out.begin(), @@ -699,7 +702,7 @@ static void RenderWelcome(WelcomeScreen& welcome, EditorState& state, std::strin if (!path.empty()) { state.workspaceRoot = path; state.fileTreeDirty = true; - state.projectSearch.setRoot(state.workspaceRoot); + state.search.projectSearch.setRoot(state.workspaceRoot); lastDialogPath = path; } } diff --git a/editor/src/EmacsPackageBrowser.h b/editor/src/EmacsPackageBrowser.h index 0d8773e..0867dc3 100644 --- a/editor/src/EmacsPackageBrowser.h +++ b/editor/src/EmacsPackageBrowser.h @@ -1,6 +1,7 @@ #pragma once #include "imgui.h" #include "EmacsIntegration.h" +#include "StringUtils.h" #include #include #include @@ -24,13 +25,7 @@ struct EmacsPackageBrowserState { std::string lastError; }; -static inline std::string trimCopy(const std::string& s) { - size_t start = 0; - while (start < s.size() && std::isspace((unsigned char)s[start])) ++start; - size_t end = s.size(); - while (end > start && std::isspace((unsigned char)s[end - 1])) --end; - return s.substr(start, end - start); -} +// trimCopy is provided by EditorUtils.h static inline std::string toLowerCopyPkg(const std::string& s) { std::string out = s; diff --git a/editor/src/GoToLine.h b/editor/src/GoToLine.h index ced6572..5b88390 100644 --- a/editor/src/GoToLine.h +++ b/editor/src/GoToLine.h @@ -3,14 +3,7 @@ #include #include - -inline std::string trimCopy(const std::string& s) { - size_t start = 0; - while (start < s.size() && std::isspace((unsigned char)s[start])) ++start; - size_t end = s.size(); - while (end > start && std::isspace((unsigned char)s[end - 1])) --end; - return s.substr(start, end - start); -} +#include "StringUtils.h" inline bool parseLineColInput(const std::string& input, int& outLine, int& outCol) { std::string s = trimCopy(input); diff --git a/editor/src/StringUtils.h b/editor/src/StringUtils.h new file mode 100644 index 0000000..4b72343 --- /dev/null +++ b/editor/src/StringUtils.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +static inline std::string trimCopy(const std::string& s) { + size_t start = 0; + while (start < s.size() && std::isspace((unsigned char)s[start])) ++start; + size_t end = s.size(); + while (end > start && std::isspace((unsigned char)s[end - 1])) --end; + return s.substr(start, end - start); +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index e1ac26b..dc0ab62 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -1,11 +1,7 @@ // Whetstone Editor — ImGui-based structured code editor // -// VSCode/JetBrains-inspired layout with docking: -// - File tree (left) -// - Editable text area (center) backed by TextEditor + TextASTSync -// - Syntax-highlighted preview / AST view (bottom) -// - Toolbar + status bar -// - Configurable keybinding profiles (VSCode default) +// Panel rendering is delegated to headers in panels/. +// main.cpp handles: initialization, event loop, panel dispatch, cleanup. #include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" @@ -18,7 +14,18 @@ #include "CompletionUtils.h" #include "ThemeEngine.h" -static std::string emacsChordForEvent(SDL_Keycode sym, SDL_Keymod mods) { +// --- Panel headers --- +#include "panels/MenuBarPanel.h" +#include "panels/ExplorerPanel.h" +#include "panels/SidePanels.h" +#include "panels/SearchPanels.h" +#include "panels/SettingsPanel.h" +#include "panels/DialogPanels.h" +#include "panels/EditorPanel.h" +#include "panels/BottomPanel.h" +#include "panels/StatusBarPanel.h" + +static std::string emacsChordForEvent(SDL_Keycode sym, Uint16 mods) { std::string base; if (sym >= SDLK_a && sym <= SDLK_z) { char c = (char)('a' + (sym - SDLK_a)); @@ -93,14 +100,12 @@ int main(int, char**) { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - // Load a monospace font (Consolas on Windows, fallback to default) + // Load fonts const float baseFontSize = 15.0f; - ImFont* monoFont = nullptr; - monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", baseFontSize); + ImFont* monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", baseFontSize); if (!monoFont) { monoFont = io.Fonts->AddFontDefault(); } - // Also keep default font for UI elements ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", baseFontSize); if (!uiFont) { uiFont = io.Fonts->AddFontDefault(); @@ -111,6 +116,9 @@ int main(int, char**) { // Editor state EditorState state; + state.monoFont = monoFont; + state.uiFont = uiFont; + state.baseFontSize = baseFontSize; state.init(); ThemeEngine& themes = ThemeEngine::instance(); themes.loadThemesFromDirectory("themes"); @@ -133,13 +141,11 @@ int main(int, char**) { state.lspTransport = std::make_shared(); state.lsp = std::make_shared(state.lspTransport); - // File dialog defaults - std::string lastDialogPath; - bool done = false; - while (!done) { - SDL_Event event; - while (SDL_PollEvent(&event)) { + while (!done) { + // --- Event handling --- + SDL_Event event; + while (SDL_PollEvent(&event)) { ImGui_ImplSDL2_ProcessEvent(&event); if (event.type == SDL_QUIT) done = true; @@ -150,24 +156,24 @@ int main(int, char**) { if (event.type == SDL_DROPFILE) { char* droppedFile = event.drop.file; if (droppedFile) { - DragDropHandler::handleDrop(droppedFile, - [&](const std::string& p) { state.doOpen(p); }, - [&](const std::string& p) { - state.workspaceRoot = p; - state.fileTreeDirty = true; - state.projectSearch.setRoot(state.workspaceRoot); - state.refreshBuildSystem(); - }); + DragDropHandler::handleDrop(droppedFile, + [&](const std::string& p) { state.doOpen(p); }, + [&](const std::string& p) { + state.workspaceRoot = p; + state.fileTreeDirty = true; + state.search.projectSearch.setRoot(state.workspaceRoot); + state.refreshBuildSystem(); + }); SDL_free(droppedFile); } } - // Handle keyboard shortcuts via KeybindingManager + // Keyboard shortcuts 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) { + if (state.ui.layoutPreset == LayoutPreset::Emacs) { std::string chord = emacsChordForEvent(sym, sdlMod); if (!chord.empty() && state.handleEmacsKeyChord(chord)) { continue; @@ -209,15 +215,15 @@ int main(int, char**) { std::string action = state.keys.getAction(combo); if (action == "edit.undo") state.doUndo(); else if (action == "edit.redo") state.doRedo(); - else if (action == "search.find") state.showFind = !state.showFind; - else if (action == "search.findInFiles") state.showProjectSearch = !state.showProjectSearch; + else if (action == "search.find") state.search.showFind = !state.search.showFind; + else if (action == "search.findInFiles") state.search.showProjectSearch = !state.search.showProjectSearch; else if (action == "nav.goToLine") { - state.showGoToLine = true; - state.goToLineBuf[0] = '\0'; - state.goToLineError = false; + state.search.showGoToLine = true; + state.search.goToLineBuf[0] = '\0'; + state.search.goToLineError = false; } else if (action == "view.toggleTerminal") { - state.showTerminalPanel = !state.showTerminalPanel; + state.build.showTerminalPanel = !state.build.showTerminalPanel; } else if (action == "file.save") state.doSave(); else if (action == "file.new") { @@ -232,7 +238,7 @@ int main(int, char**) { } } - // File watcher polling + // --- Per-frame updates --- state.handleFileChanges(); if (state.settings.getAutoSaveSeconds() > 0 && state.active() && state.active()->modified) { double now = ImGui::GetTime(); @@ -243,12 +249,12 @@ int main(int, char**) { } state.pollLspMessages(); state.processLibraryIndexResponses(); - if (state.emacsFunctionIndexDirty) { + if (state.emacsState.emacsFunctionIndexDirty) { state.updateEmacsFunctionIndex(); } state.refreshEmacsModeLine(ImGui::GetTime()); - // Start frame + // --- Start frame --- ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); @@ -271,2444 +277,32 @@ int main(int, char**) { ImGui::DockSpace(dockId, ImVec2(0, 0), ImGuiDockNodeFlags_None); ImGui::End(); - // --------------------------------------------------------------- - // Menu bar (as a window docked to the top) - // --------------------------------------------------------------- - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New", state.keys.getBinding("file.new").toString().c_str())) - { - std::string lang = state.active() ? state.active()->language : "python"; - state.createBuffer(state.makeUntitledName(), "", lang); - } - if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str())) - { - auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go","*.org"}, lastDialogPath}); - if (!path.empty()) { - lastDialogPath = path; - state.doOpen(path); - } - } - if (ImGui::MenuItem("Open Project...")) { - auto path = FileDialog::openFile({"Open Project", {"*.whetstone"}, lastDialogPath}); - if (!path.empty()) { - if (!state.loadProject(path)) { - state.outputLog += "Failed to open project: " + path + "\n"; - } else { - lastDialogPath = path; - } - } - } - if (ImGui::MenuItem("Open Folder...")) { - auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot}); - if (!path.empty()) { - state.workspaceRoot = path; - state.fileTreeDirty = true; - state.projectSearch.setRoot(state.workspaceRoot); - lastDialogPath = path; - state.refreshBuildSystem(); - } - } - if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str())) - { - if (!state.active()) { - // no-op - } else if (state.active()->path.rfind("(untitled", 0) == 0) { - auto path = FileDialog::saveFile({"Save File", {"*.*"}, lastDialogPath}); - if (!path.empty()) { - state.active()->path = path; - lastDialogPath = path; - state.doSave(); - } - } else { - state.doSave(); - } - } - if (ImGui::MenuItem("Save Project...")) { - auto path = FileDialog::saveFile({"Save Project", {"*.whetstone"}, lastDialogPath}); - if (!path.empty()) { - if (!state.saveProject(path)) { - state.outputLog += "Failed to save project: " + path + "\n"; - } else { - lastDialogPath = path; - } - } - } - ImGui::Separator(); - if (ImGui::MenuItem("Exit")) done = true; - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Undo", state.keys.getBinding("edit.undo").toString().c_str(), - false, state.active() ? state.active()->orchestrator.getUndoDepth() > 0 : false)) - state.doUndo(); - if (ImGui::MenuItem("Redo", state.keys.getBinding("edit.redo").toString().c_str(), - false, state.active() ? state.active()->orchestrator.getRedoDepth() > 0 : false)) - state.doRedo(); - ImGui::Separator(); - if (ImGui::MenuItem("Find/Replace", state.keys.getBinding("search.find").toString().c_str())) - state.showFind = !state.showFind; - if (ImGui::MenuItem("Find in Files", state.keys.getBinding("search.findInFiles").toString().c_str())) - state.showProjectSearch = !state.showProjectSearch; - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("View")) { - ImGui::MenuItem("Show Whitespace", nullptr, &state.showWhitespace); - ImGui::MenuItem("Show Minimap", nullptr, &state.showMinimap); - ImGui::MenuItem("Show Annotations", nullptr, &state.showAnnotations); - ImGui::MenuItem("Show Outline", nullptr, &state.showOutline); - ImGui::MenuItem("Terminal", state.keys.getBinding("view.toggleTerminal").toString().c_str(), - &state.showTerminalPanel); - ImGui::MenuItem("Dependencies", nullptr, &state.showDependencyPanel); - ImGui::MenuItem("Libraries", nullptr, &state.showLibraryBrowserPanel); - ImGui::MenuItem("Compose", nullptr, &state.showCompositionPanel); - ImGui::MenuItem("Emacs Packages", nullptr, &state.showEmacsPackagesPanel); - ImGui::MenuItem("Emacs Bridge", nullptr, &state.showEmacsBridgePanel); - ImGui::MenuItem("Settings", nullptr, &state.showSettingsPanel); - ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings); - if (state.active()) { - bool textMode = state.active()->bufferMode == BufferManager::BufferMode::Text; - if (ImGui::MenuItem("Text-Editor Mode", nullptr, textMode)) { - state.active()->bufferMode = textMode ? - BufferManager::BufferMode::Structured : - BufferManager::BufferMode::Text; - state.buffers.setBufferMode(state.active()->path, state.active()->bufferMode); - if (state.active()->bufferMode == BufferManager::BufferMode::Text) { - state.suggestions.clear(); - state.whetstoneDiagnostics.clear(); - state.analysisPending = false; - } else { - state.onTextChanged(); - } - if (state.active()->path.rfind("(untitled", 0) != 0) { - state.welcome.addRecentFile(state.active()->path, - state.active()->language, - bufferModeToString(state.active()->bufferMode)); - state.saveRecentFiles(); - } - } - } - if (ImGui::BeginMenu("Layout")) { - if (ImGui::MenuItem("VSCode", nullptr, state.layoutPreset == LayoutPreset::VSCode)) - state.layoutPreset = LayoutPreset::VSCode; - if (ImGui::MenuItem("Emacs", nullptr, state.layoutPreset == LayoutPreset::Emacs)) - state.layoutPreset = LayoutPreset::Emacs; - if (ImGui::MenuItem("JetBrains", nullptr, state.layoutPreset == LayoutPreset::JetBrains)) - state.layoutPreset = LayoutPreset::JetBrains; - ImGui::EndMenu(); - } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Navigate")) { - if (ImGui::MenuItem("Go to Line...", state.keys.getBinding("nav.goToLine").toString().c_str())) { - state.showGoToLine = true; - state.goToLineBuf[0] = '\0'; - state.goToLineError = false; - } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Build")) { - if (ImGui::MenuItem("Run", state.keys.getBinding("build.run").toString().c_str())) { - state.runActiveFile(false); - } - if (ImGui::MenuItem("Build", state.keys.getBinding("build.build").toString().c_str())) { - state.runActiveFile(true); - } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Language")) { - if (ImGui::MenuItem("Python", nullptr, state.active() && state.active()->language == "python")) - state.setLanguage("python"); - if (ImGui::MenuItem("C++", nullptr, state.active() && state.active()->language == "cpp")) - state.setLanguage("cpp"); - if (ImGui::MenuItem("Elisp", nullptr, state.active() && state.active()->language == "elisp")) - state.setLanguage("elisp"); - if (ImGui::MenuItem("JavaScript", nullptr, state.active() && state.active()->language == "javascript")) - state.setLanguage("javascript"); - if (ImGui::MenuItem("TypeScript", nullptr, state.active() && state.active()->language == "typescript")) - state.setLanguage("typescript"); - if (ImGui::MenuItem("Java", nullptr, state.active() && state.active()->language == "java")) - state.setLanguage("java"); - if (ImGui::MenuItem("Rust", nullptr, state.active() && state.active()->language == "rust")) - state.setLanguage("rust"); - if (ImGui::MenuItem("Go", nullptr, state.active() && state.active()->language == "go")) - state.setLanguage("go"); - if (ImGui::MenuItem("Org", nullptr, state.active() && state.active()->language == "org")) - state.setLanguage("org"); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Refactor")) { - bool canRefactor = state.isStructured() && state.active() && !state.active()->readOnly; - if (!canRefactor) ImGui::BeginDisabled(); - if (ImGui::MenuItem("Rename Variable")) { - state.refactorAction = 1; - state.showRefactorPopup = true; - state.refactorNameA[0] = '\0'; - state.refactorNameB[0] = '\0'; - ImGui::OpenPopup("RefactorPopup"); - } - if (ImGui::MenuItem("Extract Function")) { - state.refactorAction = 2; - state.showRefactorPopup = true; - state.refactorNameA[0] = '\0'; - state.refactorNameB[0] = '\0'; - ImGui::OpenPopup("RefactorPopup"); - } - if (ImGui::MenuItem("Inline Variable")) { - state.refactorAction = 3; - state.showRefactorPopup = true; - state.refactorNameA[0] = '\0'; - state.refactorNameB[0] = '\0'; - ImGui::OpenPopup("RefactorPopup"); - } - if (!canRefactor) ImGui::EndDisabled(); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Keybindings")) { - for (auto p : KeybindingManager::availableProfiles()) { - if (ImGui::MenuItem(KeybindingManager::profileName(p), nullptr, - state.keys.getProfile() == p)) { - state.keys.setProfile(p); - state.registerCommands(); - } - } - ImGui::EndMenu(); - } - - const bool canProject = state.activeAST() != nullptr; - ImGui::SameLine(); - ImGui::Dummy(ImVec2(12.0f, 0.0f)); - ImGui::SameLine(); - if (!canProject) ImGui::BeginDisabled(); - if (ImGui::Button("Project to...")) { - ImGui::OpenPopup("ProjectToPopup"); - } - if (ImGui::BeginPopup("ProjectToPopup")) { - if (ImGui::MenuItem("Python")) state.projectToLanguage("python"); - if (ImGui::MenuItem("C++")) state.projectToLanguage("cpp"); - if (ImGui::MenuItem("Elisp")) state.projectToLanguage("elisp"); - ImGui::EndPopup(); - } - if (!canProject) ImGui::EndDisabled(); - - ImGui::SameLine(); - ImGui::Dummy(ImVec2(12.0f, 0.0f)); - ImGui::SameLine(); - bool canRunFile = state.active() && !state.active()->readOnly; - if (!canRunFile) ImGui::BeginDisabled(); - if (ImGui::Button("Run >")) { - state.runActiveFile(false); - } - ImGui::SameLine(); - if (ImGui::Button("Build")) { - state.runActiveFile(true); - } - if (!canRunFile) ImGui::EndDisabled(); - ImGui::EndMainMenuBar(); - } - - // Native dialogs replace manual path popup (Step 84) - - // --------------------------------------------------------------- - // File Explorer (left panel) - // --------------------------------------------------------------- - ImGui::Begin("Explorer"); - ImGui::PushFont(uiFont); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "OPEN EDITORS"); - ImGui::Separator(); - // Show open buffers - for (const auto& path : state.buffers.getOpenBuffers()) { - auto* buf = state.bufferStates[path].get(); - std::string label = path; - if (buf && buf->modified) label += " *"; - bool selected = state.active() && state.active()->path == path; - if (ImGui::Selectable(label.c_str(), selected)) { - state.switchToBuffer(path); - } - } - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "FILES"); - ImGui::Separator(); - state.refreshFileTree(); - if (state.fileTreeRoot.path.empty()) { - ImGui::TextDisabled("(no workspace)"); - } else { - RenderFileTree(state.fileTreeRoot, state); - } - ImGui::PopFont(); - ImGui::End(); - - // --------------------------------------------------------------- - // Symbol Outline - // --------------------------------------------------------------- - if (state.showOutline) { - ImGui::Begin("Outline", &state.showOutline); - ImGui::PushFont(uiFont); - ImGui::SetNextItemWidth(-1.0f); - ImGui::InputText("Filter", state.outlineFilter, sizeof(state.outlineFilter)); - ImGui::Separator(); - - std::string filter = toLowerCopy(state.outlineFilter); - if (!state.active()) { - ImGui::TextDisabled("(no file)"); - } else { - std::vector lspSymbols = - state.lsp ? state.lsp->getDocumentSymbols() : std::vector{}; - - if (!lspSymbols.empty()) { - int outlineId = 0; - std::function renderLsp; - renderLsp = [&](const LSPClient::DocumentSymbol& sym) { - if (!lspSymbolMatchesFilter(sym, filter)) return; - ImGui::PushID(outlineId++); - std::string label = std::string(lspSymbolKindLabel(sym.kind)) + ": " + sym.name; - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth; - if (sym.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; - bool open = ImGui::TreeNodeEx(label.c_str(), flags); - if (ImGui::IsItemClicked()) { - state.jumpTo(state.active(), sym.selectionRange.start.line, - sym.selectionRange.start.character); - } - if (!sym.children.empty() && open) { - for (const auto& child : sym.children) { - renderLsp(child); - } - ImGui::TreePop(); - } - ImGui::PopID(); - }; - for (const auto& sym : lspSymbols) renderLsp(sym); - } else if (state.isStructured()) { - std::vector outline; - collectOutlineFromAST(state.activeAST(), outline); - if (outline.empty()) { - ImGui::TextDisabled("(no symbols)"); - } else { - int outlineId = 0; - std::function renderItem; - renderItem = [&](const OutlineItem& item) { - if (!outlineMatchesFilter(item, filter)) return; - ImGui::PushID(outlineId++); - std::string label = item.kind + ": " + item.name; - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth; - if (item.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; - bool open = ImGui::TreeNodeEx(label.c_str(), flags); - if (ImGui::IsItemClicked() && item.line >= 0) { - state.jumpTo(state.active(), item.line, item.col); - } - if (!item.children.empty() && open) { - for (const auto& child : item.children) { - renderItem(child); - } - ImGui::TreePop(); - } - ImGui::PopID(); - }; - for (const auto& item : outline) renderItem(item); - } - } else { - ImGui::TextDisabled("(no symbols)"); - } - } - ImGui::PopFont(); - ImGui::End(); - } - - // --------------------------------------------------------------- - // Dependencies Panel - // --------------------------------------------------------------- - if (state.showDependencyPanel) { - ImGui::Begin("Dependencies", &state.showDependencyPanel); - ImGui::PushFont(uiFont); - renderDependencyPanel(state.dependencyPanel, state.workspaceRoot, state.outputLog); - ImGui::PopFont(); - ImGui::End(); - if (state.dependencyPanel.needsIndex) { - state.requestLibraryIndex(); - state.dependencyPanel.needsIndex = false; - } - } - - if (state.showLibraryBrowserPanel) { - ImGui::Begin("Libraries", &state.showLibraryBrowserPanel); - ImGui::PushFont(uiFont); - std::string insertText; - std::string insertLibrary; - if (renderLibraryBrowser(state.libraryBrowser, - state.activeAST(), - state.libraryIndex, - insertText, - insertLibrary, - state.outputLog)) { - state.ensureImportForSymbol(insertLibrary, insertText); - state.insertTextAtCursor(insertText); - } - ImGui::PopFont(); - ImGui::End(); - } - - if (state.showCompositionPanel) { - ImGui::Begin("Compose", &state.showCompositionPanel); - ImGui::PushFont(uiFont); - std::string nodeId; - if (state.activeAST()) { - ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), - std::max(0, state.active()->cursorLine - 1), - std::max(0, state.active()->cursorCol - 1)); - if (scopeNode) nodeId = scopeNode->id; - } - std::vector primitives; - auto funcs = state.primitives.getAvailableFunctions(nodeId); - primitives.insert(primitives.end(), funcs.begin(), funcs.end()); - std::string code; - if (renderCompositionPanel(state.compositionPanel, primitives, code, state.outputLog)) { - state.insertTextAtCursor(code); - } - ImGui::PopFont(); - ImGui::End(); - } - - if (state.showEmacsPackagesPanel) { - ImGui::Begin("Emacs Packages", &state.showEmacsPackagesPanel); - ImGui::PushFont(uiFont); - if (renderEmacsPackageBrowser(state.emacsPackages, state.emacs, state.outputLog)) { - state.emacsFunctionIndexDirty = true; - } - ImGui::PopFont(); - ImGui::End(); - } - - if (state.showEmacsBridgePanel) { - ImGui::Begin("Emacs Bridge", &state.showEmacsBridgePanel); - ImGui::PushFont(uiFont); - if (state.active()) { - ImGui::Text("Active file: %s", state.active()->path.c_str()); - } else { - ImGui::TextDisabled("(no active buffer)"); - } - ImGui::Separator(); - if (ImGui::Button("Open Emacs Frame")) { - state.openInEmacsFrame(); - } - ImGui::SameLine(); - if (ImGui::Button("Pull From Emacs")) { - state.pullFromEmacs(); - } - ImGui::SameLine(); - if (ImGui::Button("Push To Emacs")) { - state.pushToEmacs(); - } - ImGui::PopFont(); - ImGui::End(); - } - - if (state.layoutPreset == LayoutPreset::Emacs && state.emacsKeys.minibufferActive) { - ImGuiWindowFlags mbFlags = ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | - 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) - // --------------------------------------------------------------- - if (state.showFind) { - ImGui::Begin("Find & Replace", &state.showFind, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::PushFont(uiFont); - ImGui::SetNextItemWidth(300); - if (ImGui::InputText("Find", state.findBuf, sizeof(state.findBuf), - ImGuiInputTextFlags_EnterReturnsTrue)) { - state.doFind(); - } - ImGui::SameLine(); - if (ImGui::Button("Find Next")) state.doFind(); - - ImGui::SetNextItemWidth(300); - ImGui::InputText("Replace", state.replaceBuf, sizeof(state.replaceBuf)); - ImGui::SameLine(); - if (ImGui::Button("Replace All")) state.doReplaceAll(); - ImGui::PopFont(); - ImGui::End(); - } - - // --------------------------------------------------------------- - // Project Search (Ctrl+Shift+F) - // --------------------------------------------------------------- - if (state.showProjectSearch) { - ImGui::Begin("Search", &state.showProjectSearch); - ImGui::PushFont(uiFont); - bool doSearch = false; - ImGui::SetNextItemWidth(420); - if (ImGui::InputText("Query", state.searchQuery, sizeof(state.searchQuery), - ImGuiInputTextFlags_EnterReturnsTrue)) { - doSearch = true; - } - ImGui::SetNextItemWidth(420); - ImGui::InputText("Include (glob)", state.searchInclude, sizeof(state.searchInclude)); - ImGui::SetNextItemWidth(420); - ImGui::InputText("Exclude (glob)", state.searchExclude, sizeof(state.searchExclude)); - ImGui::Checkbox("Regex", &state.searchUseRegex); - ImGui::SameLine(); - if (ImGui::Button("Search")) doSearch = true; - ImGui::SameLine(); - if (ImGui::Button("Clear")) { - state.searchQuery[0] = '\0'; - state.searchResults.clear(); - } - - if (doSearch) { - state.searchResults = state.projectSearch.search( - state.searchQuery, - state.searchInclude, - state.searchExclude, - state.searchUseRegex); - } - - ImGui::Separator(); - ImGui::BeginChild("##searchResults", ImVec2(0, 0), false); - if (state.searchResults.empty()) { - ImGui::TextDisabled("(no results)"); - } else { - for (const auto& fileRes : state.searchResults) { - std::string label = fileRes.path + " (" + std::to_string(fileRes.matches.size()) + ")"; - if (ImGui::TreeNode(label.c_str())) { - for (const auto& match : fileRes.matches) { - std::string lineLabel = std::to_string(match.line + 1) + ":" + - std::to_string(match.col + 1) + " " + match.lineText; - if (ImGui::Selectable(lineLabel.c_str())) { - if (state.buffers.hasBuffer(fileRes.path)) state.switchToBuffer(fileRes.path); - else state.doOpen(fileRes.path); - state.jumpTo(state.active(), match.line, match.col); - } - } - ImGui::TreePop(); - } - } - } - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::End(); - } - - // --------------------------------------------------------------- - // Settings Panel - // --------------------------------------------------------------- - if (state.showSettingsPanel) { - ImGui::Begin("Settings", &state.showSettingsPanel); - ImGui::PushFont(uiFont); - bool settingsChanged = false; - bool emacsConfigChanged = false; - ImGuiIO& io = ImGui::GetIO(); - - int fontSize = state.settings.getFontSize(); - if (ImGui::SliderInt("Font Size", &fontSize, 12, 24)) { - state.settings.setFontSize(fontSize); - io.FontGlobalScale = fontSize / baseFontSize; - settingsChanged = true; - } - - int tabSize = state.settings.getTabSize(); - const int tabSizes[] = {2, 4, 8}; - int tabIndex = 1; - for (int i = 0; i < 3; ++i) { - if (tabSize == tabSizes[i]) tabIndex = i; - } - const char* tabLabels[] = {"2", "4", "8"}; - if (ImGui::Combo("Tab Size", &tabIndex, tabLabels, 3)) { - state.settings.setTabSize(tabSizes[tabIndex]); - state.applyTabSizeToBuffers(tabSizes[tabIndex]); - settingsChanged = true; - } - - std::vector themeNames = ThemeEngine::instance().listThemes(); - if (themeNames.empty()) { - themeNames = {"VSCode Dark", "VSCode Light"}; - } - std::string currentTheme = state.settings.getTheme(); - if (currentTheme == "Dark") currentTheme = "VSCode Dark"; - if (currentTheme == "Light") currentTheme = "VSCode Light"; - int themeIndex = 0; - for (size_t i = 0; i < themeNames.size(); ++i) { - if (themeNames[i] == currentTheme) { - themeIndex = (int)i; - break; - } - } - std::vector themeLabels; - themeLabels.reserve(themeNames.size()); - for (const auto& name : themeNames) themeLabels.push_back(name.c_str()); - if (ImGui::Combo("Theme", &themeIndex, themeLabels.data(), (int)themeLabels.size())) { - state.settings.setTheme(themeNames[themeIndex]); - if (!ThemeEngine::instance().applyTheme(themeNames[themeIndex])) { - if (themeNames[themeIndex].find("Light") != std::string::npos) - SetupVSCodeLightTheme(); - else - SetupVSCodeDarkTheme(); - } - settingsChanged = true; - } - - bool telemetryOptIn = state.settings.getTelemetryOptIn(); - if (ImGui::Checkbox("Telemetry (opt-in)", &telemetryOptIn)) { - state.settings.setTelemetryOptIn(telemetryOptIn); - state.telemetry.setOptIn(telemetryOptIn); - if (telemetryOptIn) { - state.telemetry.recordEvent("telemetry_opt_in", json::object()); - } - settingsChanged = true; - } - - char updateUrlBuf[256]; - std::snprintf(updateUrlBuf, sizeof(updateUrlBuf), "%s", - state.settings.getUpdateUrl().c_str()); - if (ImGui::InputText("Update URL", updateUrlBuf, sizeof(updateUrlBuf))) { - state.settings.setUpdateUrl(updateUrlBuf); - settingsChanged = true; - } - if (ImGui::Button("Check for Updates")) { - UpdateChecker checker; - auto info = checker.check(state.settings.getUpdateUrl()); - if (info.available) { - state.outputLog += "[update] Available: " + info.version + " (" + info.url + ")\n"; - } else { - state.outputLog += "[update] No update info (offline/stub).\n"; - } - } - - int autoSave = state.settings.getAutoSaveSeconds(); - if (ImGui::InputInt("Auto-save (sec)", &autoSave)) { - autoSave = std::max(0, autoSave); - state.settings.setAutoSaveSeconds(autoSave); - settingsChanged = true; - } - - bool showMinimap = state.showMinimap; - if (ImGui::Checkbox("Show Minimap", &showMinimap)) { - state.showMinimap = showMinimap; - settingsChanged = true; - } - bool showLineNumbers = state.showLineNumbers; - if (ImGui::Checkbox("Show Line Numbers", &showLineNumbers)) { - state.showLineNumbers = showLineNumbers; - settingsChanged = true; - } - - LayoutPreset preset = state.layoutPreset; - int presetIndex = 0; - if (preset == LayoutPreset::Emacs) presetIndex = 1; - else if (preset == LayoutPreset::JetBrains) presetIndex = 2; - const char* presetLabels[] = {"VSCode", "Emacs", "JetBrains"}; - if (ImGui::Combo("Layout Preset", &presetIndex, presetLabels, 3)) { - state.layoutPreset = (presetIndex == 1) ? LayoutPreset::Emacs : - (presetIndex == 2) ? LayoutPreset::JetBrains : LayoutPreset::VSCode; - settingsChanged = true; - } - - int keyProfileIndex = 0; - if (state.keys.getProfile() == KeybindingProfile::JetBrains) keyProfileIndex = 1; - else if (state.keys.getProfile() == KeybindingProfile::Emacs) keyProfileIndex = 2; - const char* keyProfiles[] = {"VSCode", "JetBrains", "Emacs"}; - if (ImGui::Combo("Keybindings", &keyProfileIndex, keyProfiles, 3)) { - KeybindingProfile profile = KeybindingProfile::VSCode; - if (keyProfileIndex == 1) profile = KeybindingProfile::JetBrains; - else if (keyProfileIndex == 2) profile = KeybindingProfile::Emacs; - state.keys.setProfile(profile); - state.registerCommands(); - settingsChanged = true; - } - - ImGui::Separator(); - ImGui::TextUnformatted("Emacs Config"); - ImGui::SetNextItemWidth(320); - if (InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable())) { - settingsChanged = true; - emacsConfigChanged = true; - } - - if (ImGui::CollapsingHeader("LSP Servers", ImGuiTreeNodeFlags_DefaultOpen)) { - for (auto& cfg : state.settings.getLSPServersMutable()) { - ImGui::PushID(cfg.language.c_str()); - ImGui::Checkbox("Enabled", &cfg.enabled); - ImGui::SameLine(); - ImGui::Text("%s", cfg.language.c_str()); - ImGui::SetNextItemWidth(320); - if (InputTextStr("Path", &cfg.path)) settingsChanged = true; - ImGui::SetNextItemWidth(320); - if (InputTextStr("Args", &cfg.argsLine)) { - state.settings.syncArgs(cfg); - settingsChanged = true; - } - ImGui::Separator(); - ImGui::PopID(); - } - } - - if (settingsChanged) { - state.saveSettingsToDisk(); - } - if (emacsConfigChanged) { - state.startEmacsDaemonFromSettings(); - } - ImGui::PopFont(); - ImGui::End(); - } - - // --------------------------------------------------------------- - // Go To Line (Ctrl+G) - // --------------------------------------------------------------- - if (state.showGoToLine) { - ImGui::OpenPopup("GoToLine"); - } - if (ImGui::BeginPopupModal("GoToLine", &state.showGoToLine, ImGuiWindowFlags_AlwaysAutoResize)) { - int totalLines = state.active() ? countLines(state.active()->editBuf) : 0; - ImGui::Text("Enter line or :line:col"); - ImGui::TextDisabled("Total lines: %d", totalLines); - ImGui::SetNextItemWidth(240); - bool submit = ImGui::InputText("##gotoLineInput", state.goToLineBuf, - sizeof(state.goToLineBuf), - ImGuiInputTextFlags_EnterReturnsTrue); - if (ImGui::Button("Go")) submit = true; - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - state.showGoToLine = false; - state.goToLineError = false; - ImGui::CloseCurrentPopup(); - } - if (submit) { - int line = 0; - int col = 0; - if (parseLineColInput(state.goToLineBuf, line, col)) { - if (totalLines > 0) line = std::max(1, std::min(line, totalLines)); - col = std::max(1, col); - if (state.active()) { - state.jumpTo(state.active(), line - 1, col - 1); - } - state.showGoToLine = false; - state.goToLineError = false; - ImGui::CloseCurrentPopup(); - } else { - state.goToLineError = true; - } - } - if (state.goToLineError) { - ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "Invalid format."); - } - ImGui::EndPopup(); - } - - // --------------------------------------------------------------- - // LSP Server Settings - // --------------------------------------------------------------- - if (state.showLspSettings) { - ImGui::Begin("LSP Servers", &state.showLspSettings); - ImGui::PushFont(uiFont); - ImGui::TextUnformatted("Emacs Config"); - ImGui::SetNextItemWidth(320); - InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable(), ImGuiInputTextFlags_None); - ImGui::Separator(); - if (ImGui::Button("Auto-Detect")) { - state.settings.autoDetect(); - } - ImGui::Separator(); - for (auto& cfg : state.settings.getLSPServersMutable()) { - ImGui::PushID(cfg.language.c_str()); - ImGui::Checkbox("Enabled", &cfg.enabled); - ImGui::SameLine(); - ImGui::Text("%s", cfg.language.c_str()); - ImGui::SetNextItemWidth(320); - InputTextStr("Path", &cfg.path); - ImGui::SetNextItemWidth(320); - if (InputTextStr("Args", &cfg.argsLine)) { - state.settings.syncArgs(cfg); - } - ImGui::Separator(); - ImGui::PopID(); - } - ImGui::PopFont(); - ImGui::End(); - } - - // --------------------------------------------------------------- - // Refactor popup - // --------------------------------------------------------------- - if (state.showRefactorPopup) { - ImGui::OpenPopup("RefactorPopup"); - } - if (ImGui::BeginPopupModal("RefactorPopup", &state.showRefactorPopup, - ImGuiWindowFlags_AlwaysAutoResize)) { - const char* title = ""; - if (state.refactorAction == 1) title = "Rename Variable"; - else if (state.refactorAction == 2) title = "Extract Function"; - else if (state.refactorAction == 3) title = "Inline Variable"; - ImGui::TextUnformatted(title); - ImGui::Separator(); - - if (state.refactorAction == 1) { - ImGui::InputText("Old Name", state.refactorNameA, sizeof(state.refactorNameA)); - ImGui::InputText("New Name", state.refactorNameB, sizeof(state.refactorNameB)); - } else if (state.refactorAction == 2) { - ImGui::InputText("New Function Name", state.refactorNameA, sizeof(state.refactorNameA)); - ImGui::TextDisabled("Extracts the first statement of the first function."); - } else if (state.refactorAction == 3) { - ImGui::InputText("Variable Name", state.refactorNameA, sizeof(state.refactorNameA)); - ImGui::TextDisabled("Removes the first matching variable declaration."); - } - - if (!state.refactorError.empty()) { - ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), - "%s", state.refactorError.c_str()); - } - - bool canRun = state.isStructured() && state.active() && !state.active()->readOnly; - if (!canRun) ImGui::BeginDisabled(); - if (ImGui::Button("Preview")) { - state.refactorError.clear(); - Module* ast = state.activeAST(); - RefactorPlan plan; - if (state.refactorAction == 1) { - plan = buildRenameVariablePlan(ast, state.refactorNameA, state.refactorNameB); - } else if (state.refactorAction == 2) { - plan = buildExtractFunctionPlan(ast, state.refactorNameA); - } else if (state.refactorAction == 3) { - plan = buildInlineVariablePlan(ast, state.refactorNameA); - } - if (!plan.success) { - state.refactorError = plan.error; - } else { - auto previewAst = cloneModule(ast); - BatchMutationAPI batch; - batch.setRoot(previewAst.get()); - auto res = batch.applySequence(plan.mutations); - if (!res.success) { - state.refactorError = res.error; - } else { - std::string beforeText = state.active()->editBuf; - std::string afterText = - generateForLanguage(previewAst.get(), state.active()->language); - state.openDiff(beforeText, afterText, true, 0, {}, plan.mutations); - state.showRefactorPopup = false; - } - } - } - ImGui::SameLine(); - if (ImGui::Button("Apply")) { - state.refactorError.clear(); - Module* ast = state.mutationAST(); - RefactorPlan plan; - if (state.refactorAction == 1) { - plan = buildRenameVariablePlan(ast, state.refactorNameA, state.refactorNameB); - } else if (state.refactorAction == 2) { - plan = buildExtractFunctionPlan(ast, state.refactorNameA); - } else if (state.refactorAction == 3) { - plan = buildInlineVariablePlan(ast, state.refactorNameA); - } - if (!plan.success) { - state.refactorError = plan.error; - } else { - BatchMutationAPI batch; - batch.setRoot(ast); - auto res = batch.applySequence(plan.mutations); - if (!res.success) { - state.refactorError = res.error; - } else { - state.outputLog += "Refactor applied (" + - std::to_string(res.appliedCount) + " mutations).\n"; - state.applyOrchestratorToActive(); - state.showRefactorPopup = false; - } - } - } - if (!canRun) ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - state.showRefactorPopup = false; - } - - ImGui::EndPopup(); - } - - // --------------------------------------------------------------- - // Command Palette - // --------------------------------------------------------------- - if (state.showCommandPalette) { - ImGui::Begin("Command Palette", &state.showCommandPalette, - ImGuiWindowFlags_AlwaysAutoResize); - ImGui::SetNextItemWidth(420.0f); - if (ImGui::IsWindowAppearing()) { - ImGui::SetKeyboardFocusHere(); - } - if (ImGui::InputText("##cmdQuery", state.commandQuery, sizeof(state.commandQuery))) { - state.commandSelected = 0; - } - - auto results = state.commandPalette.search(state.commandQuery); - int maxIndex = std::max(0, (int)results.size() - 1); - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { - state.commandSelected = std::min(state.commandSelected + 1, maxIndex); - } - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { - state.commandSelected = std::max(state.commandSelected - 1, 0); - } - bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter); - bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape); - - if (dismiss) { - state.showCommandPalette = false; - } - - for (int i = 0; i < (int)results.size(); ++i) { - const auto& cmd = results[i]; - bool selected = (i == state.commandSelected); - std::string label = cmd.label; - if (!cmd.shortcut.empty()) label += " [" + cmd.shortcut + "]"; - if (ImGui::Selectable(label.c_str(), selected)) { - state.commandSelected = i; - accept = true; - } - } - if (accept && !results.empty()) { - state.executeCommand(results[state.commandSelected].id); - state.showCommandPalette = false; - } - ImGui::End(); - } - - // --------------------------------------------------------------- - // Editor (center) — editable text area - // --------------------------------------------------------------- - ImGui::Begin("Editor"); - ImGui::PushFont(uiFont); - ImGui::BeginChild("##breadcrumbs", ImVec2(0, 26.0f), false, ImGuiWindowFlags_NoScrollbar); - if (!state.active()) { - ImGui::TextDisabled("(no file)"); - } else if (!state.isStructured()) { - ImGui::TextDisabled("Text mode"); - } else { - Module* ast = state.activeAST(); - int lineZero = std::max(0, state.active()->cursorLine - 1); - int colZero = std::max(0, state.active()->cursorCol - 1); - ASTNode* node = ast ? findNodeAtPosition(ast, lineZero, colZero) : nullptr; - if (!node) { - ImGui::TextDisabled("(no scope)"); - } else { - auto crumbs = buildBreadcrumbTrail(node); - for (size_t i = 0; i < crumbs.size(); ++i) { - if (i > 0) { - ImGui::SameLine(); - ImGui::TextUnformatted(">"); - ImGui::SameLine(); - } - ImGui::PushID((int)i); - const auto& item = crumbs[i]; - if (item.isRole) { - ImGui::TextDisabled("%s", item.label.c_str()); - } else if (item.node && item.node->hasSpan()) { - if (ImGui::SmallButton(item.label.c_str())) { - state.jumpTo(state.active(), item.node->spanStartLine, item.node->spanStartCol); - } - } else { - ImGui::TextUnformatted(item.label.c_str()); - } - ImGui::PopID(); - } - } - } - ImGui::EndChild(); - ImGui::PopFont(); - - ImGui::PushFont(monoFont); - - // Tab bar for the file - if (ImGui::BeginTabBar("EditorTabs")) { - if (state.buffers.bufferCount() == 0) { - if (ImGui::BeginTabItem("Welcome")) { - RenderWelcome(state.welcome, state, lastDialogPath); - ImGui::EndTabItem(); - } - } else { - auto openBuffers = state.buffers.getOpenBuffers(); - for (const auto& path : openBuffers) { - auto* buf = state.bufferStates[path].get(); - if (!buf) continue; - std::string tabLabel = path; - if (buf->modified) tabLabel += " *"; - bool open = true; - if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) { - if (!state.active() || state.active()->path != path) { - state.switchToBuffer(path); - } - // Editable text area fills available space - ImVec2 avail = ImGui::GetContentRegionAvail(); - avail.y -= 4; // small margin - - state.updateHighlights(); - if (buf->bufferMode == BufferManager::BufferMode::Structured) { - state.updateGenerated(); - } - std::vector errorLines; - std::vector warningLines; - std::vector diagRanges; - std::vector annoMarkers; - std::vector suggestionMarkers; - std::vector conflictMarkers; - std::string activeUri = EditorState::toFileUri(buf->path); - if (state.lsp) { - auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri); - for (const auto& d : lspDiags) { - if (d.severity == 1) errorLines.push_back(d.range.start.line); - else if (d.severity == 2) warningLines.push_back(d.range.start.line); - DiagnosticRange dr; - dr.startLine = d.range.start.line; - dr.startCol = d.range.start.character; - dr.endLine = d.range.end.line; - dr.endCol = d.range.end.character; - dr.severity = d.severity; - dr.message = d.message; - diagRanges.push_back(std::move(dr)); - } - } - for (const auto& d : state.whetstoneDiagnostics) { - if (d.uri != activeUri) continue; - if (d.severity == 1) errorLines.push_back(d.line); - else if (d.severity == 2) warningLines.push_back(d.line); - DiagnosticRange dr; - dr.startLine = d.line; - dr.startCol = d.character; - dr.endLine = d.line; - dr.endCol = d.character + 1; - dr.severity = d.severity; - dr.message = d.message; - diagRanges.push_back(std::move(dr)); - } - if (state.isStructured()) { - Module* ast = state.activeAST(); - if (ast) { - collectAnnotationMarkers(ast, annoMarkers); - std::vector conflicts; - collectAnnotationConflicts(ast, conflicts); - for (const auto& c : conflicts) { - AnnotationConflictMarker m; - m.childLine = c.childLine; - m.parentLine = c.parentLine; - m.message = c.message; - m.childAnnoId = c.childAnnoId; - m.parentAnnoId = c.parentAnnoId; - m.parentStrategy = c.parentStrategy; - conflictMarkers.push_back(std::move(m)); - } - for (const auto& s : state.suggestions) { - if (s.confidence <= 0.5) continue; - ASTNode* target = findNodeById(ast, s.nodeId); - if (!target || !target->hasSpan()) continue; - SuggestionMarker sm; - sm.line = target->spanStartLine; - sm.confidence = s.confidence; - sm.reason = s.reason; - sm.annotationType = s.annotationType; - sm.strategy = s.strategy; - sm.nodeId = s.nodeId; - sm.label = "@" + s.annotationType.substr(0, s.annotationType.find("Annotation")) + - "(" + s.strategy + ")"; - suggestionMarkers.push_back(std::move(sm)); - } - } - } - - CodeEditorResult res; - if (buf->language == "org") { - renderOrgDocument(state.orgDoc, - buf->editBuf, - state.showWhitespace, - monoFont, - uiFont, - [&](const std::string& newText) { - buf->editBuf = newText; - state.onTextChanged(); - }, - [&](int, const std::string&, const std::string&) { - buf->modified = true; - }, - [&](const std::string& lang, const std::string& code) { - return state.runOrgBlock(lang, code); - }); - res.cursorByte = buf->widget.getCursor(); - } else { - CodeEditorOptions opts; - opts.showWhitespace = state.showWhitespace; - opts.readOnly = buf->readOnly; - opts.mode = &buf->mode; - opts.enableFolding = true; - opts.showMinimap = state.showMinimap; - opts.showAnnotations = state.showAnnotations; - opts.showLineNumbers = state.showLineNumbers; - if (state.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1; - else if (state.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2; - else opts.annotationLayout = 0; - opts.errorLines = &errorLines; - opts.warningLines = &warningLines; - opts.diagnostics = &diagRanges; - opts.annotations = &annoMarkers; - opts.suggestions = &suggestionMarkers; - opts.conflicts = &conflictMarkers; - - if (buf->bufferMode == BufferManager::BufferMode::Structured) { - opts.syncScrollX = &buf->splitScrollX; - opts.syncScrollY = &buf->splitScrollY; - opts.scrollMaster = true; - - ImGui::BeginTable("##editorSplit", 2, - ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); - ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f); - ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - res = buf->widget.render("##editor", - buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont); - - ImGui::TableSetColumnIndex(1); - ImGui::BeginGroup(); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Generated"); - ImGui::SameLine(); - const char* targetLabels[] = {"Python", "C++", "Elisp"}; - const char* targetValues[] = {"python", "cpp", "elisp"}; - int targetIndex = 0; - for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) { - if (buf->generatedLanguage == targetValues[i]) { - targetIndex = i; - break; - } - } - ImGui::SetNextItemWidth(120.0f); - if (ImGui::Combo("##targetLang", &targetIndex, - targetLabels, IM_ARRAYSIZE(targetLabels))) { - buf->generatedLanguage = targetValues[targetIndex]; - buf->generatedMode.setLanguage(buf->generatedLanguage); - buf->generatedHighlightsDirty = true; - state.updateGenerated(); - } - ImVec2 genAvail = ImGui::GetContentRegionAvail(); - CodeEditorOptions genOpts; - genOpts.readOnly = true; - genOpts.showWhitespace = state.showWhitespace; - genOpts.mode = &buf->generatedMode; - genOpts.showLineNumbers = state.showLineNumbers; - genOpts.showCurrentLine = false; - genOpts.highlightLine = buf->generatedHighlightLine; - genOpts.syncScrollX = &buf->splitScrollX; - genOpts.syncScrollY = &buf->splitScrollY; - genOpts.scrollMaster = false; - buf->generatedWidget.render("##generated", - buf->generatedBuf, buf->generatedHighlights, genOpts, - ImVec2(0, genAvail.y), monoFont); - ImGui::EndGroup(); - ImGui::EndTable(); - } else { - res = buf->widget.render("##editor", - buf->editBuf, buf->highlights, opts, avail, monoFont); - } - } - - if (buf->language != "org") { - state.updateCursorPos(res.cursorByte); - if (res.ctrlClick && state.active()) { - state.goToDefinitionAt(res.ctrlClickLine, res.ctrlClickCol); - } - if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) { - Module* ast = state.activeAST(); - if (ast) { - int genLines = countLines(buf->generatedBuf); - int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1)); - buf->generatedHighlightLine = targetLine; - } else { - buf->generatedHighlightLine = -1; - } - } - if (res.changed) { - state.onTextChanged(); - state.completionPending = true; - state.completionLastChange = ImGui::GetTime(); - state.completionVisible = false; - state.completionSelected = 0; - if (state.lsp) state.lsp->clearCompletionItems(); - if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { - int cursor = state.active()->widget.getCursor(); - if (cursor > 0 && state.active()->editBuf[cursor - 1] == '(') { - int lineZero = std::max(0, state.active()->cursorLine - 1); - int colZero = std::max(0, state.active()->cursorCol - 1); - state.lsp->requestSignatureHelp(EditorState::toFileUri(state.active()->path), - lineZero, colZero); - } else if (cursor > 0 && state.active()->editBuf[cursor - 1] == ')') { - state.lsp->clearSignatureHelp(); - } - } - if (buf->bufferMode == BufferManager::BufferMode::Structured) { - state.analysisPending = true; - state.analysisLastChange = ImGui::GetTime(); - } - } - } - - if (res.suggestionClicked) { - state.suggestionPopup.annotationType = res.clickedSuggestion.annotationType; - state.suggestionPopup.strategy = res.clickedSuggestion.strategy; - state.suggestionPopup.reason = res.clickedSuggestion.reason; - state.suggestionPopup.confidence = res.clickedSuggestion.confidence; - state.suggestionPopup.nodeId = res.clickedSuggestion.nodeId; - state.showSuggestionPopup = true; - ImGui::OpenPopup("SuggestionPopup"); - } - - double now = ImGui::GetTime(); - if (state.completionPending && (now - state.completionLastChange) > 0.2) { - if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { - int lineZero = std::max(0, state.active()->cursorLine - 1); - int colZero = std::max(0, state.active()->cursorCol - 1); - state.lsp->requestCompletion(EditorState::toFileUri(state.active()->path), - lineZero, colZero); - } - state.completionPending = false; - } - - if (state.symbolsPending && (now - state.symbolsLastChange) > 0.3) { - if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { - state.lsp->requestDocumentSymbols(EditorState::toFileUri(state.active()->path)); - } - state.symbolsPending = false; - } - - if (state.analysisPending && (now - state.analysisLastChange) > 0.5) { - if (state.isStructured()) { - auto result = state.pipeline.run(state.active()->editBuf, - state.active()->language, - state.active()->language); - if (result.success) { - state.whetstoneDiagnostics = - collectWhetstoneDiagnostics(result.validationDiags, - result.violations, - EditorState::toFileUri(state.active()->path)); - state.appendUnusedImportDiagnostics(state.whetstoneDiagnostics); - } else { - state.whetstoneDiagnostics.clear(); - } - Module* ast = state.activeAST(); - if (ast) { - MemoryStrategyInference inf; - state.suggestions = inf.inferAnnotations(ast); - } else { - state.suggestions.clear(); - } - } else { - state.whetstoneDiagnostics.clear(); - state.suggestions.clear(); - } - state.analysisPending = false; - } - - if (state.lsp && state.active()) { - auto items = state.lsp->getCompletionItems(); - int cursor = state.active()->widget.getCursor(); - std::string prefix = EditorState::wordPrefixAt(state.active()->editBuf, cursor); - std::string nodeId; - if (state.activeAST()) { - ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), - std::max(0, state.active()->cursorLine - 1), - std::max(0, state.active()->cursorCol - 1)); - if (scopeNode) nodeId = scopeNode->id; - } - std::vector primitives; - auto funcs = state.primitives.getAvailableFunctions(nodeId); - auto types = state.primitives.getAvailableTypes(nodeId); - auto consts = state.primitives.getAvailableConstants(nodeId); - primitives.insert(primitives.end(), funcs.begin(), funcs.end()); - primitives.insert(primitives.end(), types.begin(), types.end()); - primitives.insert(primitives.end(), consts.begin(), consts.end()); - - auto built = buildLibraryAwareCompletions(items, primitives, prefix); - std::vector filtered; - filtered.reserve(built.items.size()); - for (const auto& item : built.items) { - const std::string& key = item.filterText.empty() ? item.label : item.filterText; - if (prefix.empty() || key.rfind(prefix, 0) == 0) filtered.push_back(item); - } - - state.completionVisible = !filtered.empty(); - if (state.completionVisible) { - ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f, - ImGui::GetWindowPos().y + 60.0f)); - ImGui::BeginChild("##completionPopup", ImVec2(300, 150), true); - int maxIndex = std::max(0, (int)filtered.size() - 1); - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { - state.completionSelected = std::min(state.completionSelected + 1, maxIndex); - } - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { - state.completionSelected = std::max(state.completionSelected - 1, 0); - } - bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Tab); - bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape); - - if (dismiss) { - state.lsp->clearCompletionItems(); - state.completionVisible = false; - } - - auto deriveLibrary = [](const std::string& name) { - auto pos = name.find("::"); - if (pos != std::string::npos) return name.substr(0, pos); - pos = name.find('.'); - if (pos != std::string::npos) return name.substr(0, pos); - return std::string(); - }; - - for (int i = 0; i < (int)filtered.size(); ++i) { - std::string itemLabel = filtered[i].label; - if (filtered[i].kind != 0) { - itemLabel = "[" + std::to_string(filtered[i].kind) + "] " + itemLabel; - } - bool selected = (i == state.completionSelected); - bool preferred = built.preferredNames.count(filtered[i].label) > 0; - std::string libHint = deriveLibrary(filtered[i].label); - if (!preferred && !libHint.empty()) itemLabel += " (auto-import)"; - if (!preferred) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); - } - if (ImGui::Selectable(itemLabel.c_str(), selected)) { - state.completionSelected = i; - accept = true; - } - if (!preferred) { - ImGui::PopStyleColor(); - } - } - - if (accept && !filtered.empty()) { - const auto& item = filtered[state.completionSelected]; - bool preferred = built.preferredNames.count(item.label) > 0; - if (!preferred) { - std::string libHint = deriveLibrary(item.label); - if (!libHint.empty()) { - state.ensureImportForSymbol(libHint, item.label); - } - } - int start = EditorState::wordStartAt(state.active()->editBuf, cursor); - state.applyCompletion(state.active(), item.insertText, start, cursor); - state.lsp->clearCompletionItems(); - state.completionVisible = false; - } - ImGui::EndChild(); - } - } - - if (state.showSuggestionPopup) { - if (ImGui::BeginPopupModal("SuggestionPopup", &state.showSuggestionPopup, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("%s(%s)", state.suggestionPopup.annotationType.c_str(), - state.suggestionPopup.strategy.c_str()); - ImGui::Separator(); - ImGui::TextWrapped("%s", state.suggestionPopup.reason.c_str()); - ImGui::Text("Confidence: %.2f", state.suggestionPopup.confidence); - if (ImGui::Button("Apply")) { - Module* ast = state.mutationAST(); - if (ast) { - ASTNode* target = findNodeById(ast, state.suggestionPopup.nodeId); - if (target) { - auto* anno = createAnnotationNode(state.suggestionPopup.annotationType, - state.suggestionPopup.strategy); - if (anno) { - if (target->hasSpan()) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - target->spanEndLine, target->spanEndCol); - } - state.mutator.setRoot(ast); - auto res2 = state.mutator.insertNode(target->id, "annotations", anno); - if (!res2.error.empty()) state.outputLog += res2.error + "\n"; - state.applyOrchestratorToActive(); - } - } - } - state.showSuggestionPopup = false; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - state.showSuggestionPopup = false; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - } - - if (state.lsp && state.active()) { - if (ImGui::IsWindowHovered()) { - ImVec2 mouse = ImGui::GetMousePos(); - if (mouse.x != state.hoverLastMouse.x || mouse.y != state.hoverLastMouse.y) { - state.hoverLastMouse = mouse; - state.hoverLastMove = ImGui::GetTime(); - state.hoverPending = true; - state.lsp->clearHover(); - } - if (state.hoverPending && (ImGui::GetTime() - state.hoverLastMove) > 0.5) { - if (state.active()->path.rfind("(untitled", 0) != 0) { - int lineZero = std::max(0, state.active()->cursorLine - 1); - int colZero = std::max(0, state.active()->cursorCol - 1); - state.lsp->requestHover(EditorState::toFileUri(state.active()->path), - lineZero, colZero); - } - state.hoverPending = false; - } - std::string hover = state.lsp->getHoverContents(); - if (!hover.empty()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(hover.c_str()); - ImGui::EndTooltip(); - } - } - - if (state.definitionPending) { - auto defs = state.lsp->getDefinitionLocations(); - if (!defs.empty()) { - state.definitionPending = false; - state.definitionPreview = defs.front(); - state.definitionPreviewValid = true; - state.lsp->clearDefinitionLocations(); - state.jumpToDefinitionLocation(state.definitionPreview); - } else if ((ImGui::GetTime() - state.definitionLastRequest) > 0.5) { - state.definitionPending = false; - state.goToDefinitionFallback(state.definitionRequestLine, - state.definitionRequestCol); - } - } - - auto sig = state.lsp->getSignatureHelp(); - if (!sig.label.empty()) { - ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f, - ImGui::GetWindowPos().y + ImGui::GetWindowHeight() - 80.0f)); - ImGui::BeginChild("##signatureHelp", ImVec2(420, 60), true); - ImGui::TextUnformatted(sig.label.c_str()); - ImGui::EndChild(); - } - } - - if (res.hoverValid && (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) { - std::string preview; - if (state.previewDefinitionAt(res.hoverLine, res.hoverCol, preview)) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(preview.c_str()); - ImGui::EndTooltip(); - } else if (state.definitionPreviewValid) { - std::string path = EditorState::fromFileUri(state.definitionPreview.uri); - std::string label = path + ":" + - std::to_string(state.definitionPreview.range.start.line + 1); - ImGui::BeginTooltip(); - ImGui::TextUnformatted(label.c_str()); - ImGui::EndTooltip(); - } - } - - if (ImGui::BeginPopupContextWindow("AnnotationContext", ImGuiPopupFlags_MouseButtonRight)) { - Module* ast = state.mutationAST(); - int lineZero = state.active() ? std::max(0, state.active()->cursorLine - 1) : 0; - ASTNode* target = ast ? findAnnotationTarget(ast, lineZero) : nullptr; - if (!target) { - ImGui::TextDisabled("No annotatable symbol on this line."); - } else { - bool mutated = false; - ImGui::Text("Target: %s", nodeDisplayName(target).c_str()); - ImGui::Separator(); - - if (ImGui::BeginMenu("Add Annotation")) { - if (ImGui::MenuItem("@Reclaim(Tracing)")) { - auto* anno = createAnnotationNode("ReclaimAnnotation", "Tracing"); - if (anno) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - 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()) mutated = true; - } - } - if (ImGui::MenuItem("@Owner(Single)")) { - auto* anno = createAnnotationNode("OwnerAnnotation", "Single"); - if (anno) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - 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()) mutated = true; - } - } - if (ImGui::MenuItem("@Deallocate(Explicit)")) { - auto* anno = createAnnotationNode("DeallocateAnnotation", "Explicit"); - if (anno) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - 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()) mutated = true; - } - } - if (ImGui::MenuItem("@Lifetime(RAII)")) { - auto* anno = createAnnotationNode("LifetimeAnnotation", "RAII"); - if (anno) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - 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()) mutated = true; - } - } - if (ImGui::MenuItem("@Allocate(Static)")) { - auto* anno = createAnnotationNode("AllocateAnnotation", "Static"); - if (anno) { - anno->setSpan(target->spanStartLine, target->spanStartCol, - 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()) mutated = true; - } - } - ImGui::EndMenu(); - } - - const auto& annos = target->getChildren("annotations"); - if (ImGui::BeginMenu("Edit Annotation", !annos.empty())) { - for (auto* anno : annos) { - std::string label = annotationLabel(anno); - if (ImGui::BeginMenu(label.c_str())) { - if (anno->conceptType == "ReclaimAnnotation") { - 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()) 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()) 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()) 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()) 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()) mutated = true; - } - } - ImGui::EndMenu(); - } - } - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Remove Annotation", !annos.empty())) { - for (auto* anno : annos) { - std::string label = annotationLabel(anno); - 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()) mutated = true; - } - } - ImGui::EndMenu(); - } - - std::vector conflicts; - collectAnnotationConflicts(ast, conflicts); - bool conflictOnLine = false; - AnnotationConflict lineConflict; - for (const auto& c : conflicts) { - if (c.childLine == lineZero || c.parentLine == lineZero) { - lineConflict = c; - conflictOnLine = true; - break; - } - } - if (ImGui::BeginMenu("Resolve Conflict", conflictOnLine)) { - if (conflictOnLine) { - 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()) mutated = true; - } - if (ImGui::MenuItem("Match parent strategy")) { - state.mutator.setRoot(ast); - auto res = state.mutator.setProperty(lineConflict.childAnnoId, - "strategy", - lineConflict.parentStrategy); - if (!res.error.empty()) state.outputLog += res.error + "\n"; - if (res.error.empty()) mutated = true; - } - } - ImGui::EndMenu(); - } - if (mutated) { - state.applyOrchestratorToActive(); - } - } - ImGui::EndPopup(); - } - - ImGui::EndTabItem(); - } - if (!open) { - state.buffers.closeBuffer(path); - state.bufferStates.erase(path); - if (path.rfind("(untitled", 0) != 0) state.watcher.unwatch(path); - if (state.buffers.bufferCount() > 0) { - state.switchToBuffer(state.buffers.getOpenBuffers().front()); - } else { - state.activeBuffer = nullptr; - } - } - } - } - ImGui::EndTabBar(); - } - - ImGui::PopFont(); - ImGui::End(); - - // --------------------------------------------------------------- - // Bottom panel — Output / AST / Highlighted Preview / Terminal - // --------------------------------------------------------------- - ImGui::Begin("Panel"); - - if (ImGui::BeginTabBar("PanelTabs")) { - // Output log - if (ImGui::BeginTabItem("Output")) { - ImGui::PushFont(monoFont); - ImGui::BeginChild("##outputScroll", ImVec2(0, 0), false); - ImGui::TextUnformatted(state.outputLog.c_str()); - // Auto-scroll to bottom - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20) - ImGui::SetScrollHereY(1.0f); - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - if (state.showTerminalPanel && ImGui::BeginTabItem("Terminal")) { - state.terminal.render(state.workspaceRoot, monoFont); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Agents")) { - ImGui::PushFont(monoFont); - bool running = state.agentServer && state.agentServer->isRunning(); - ImGui::Text("Server: %s", running ? "Running" : "Stopped"); - ImGui::SameLine(0, 20); - ImGui::Text("Port: %d", state.agentPort); - ImGui::Separator(); - - ImGui::TextUnformatted("Active Sessions"); - if (!state.agentServer || state.agentServer->getActiveSessionCount() == 0) { - ImGui::TextDisabled("(none)"); - } else { - for (const auto& s : state.agentServer->getActiveSessions()) { - std::string label = s.sessionId; - if (!s.agentName.empty()) label += " (" + s.agentName + ")"; - ImGui::TextUnformatted(label.c_str()); - ImGui::SameLine(260.0f); - AgentRole role = state.getAgentRole(s.sessionId); - int roleIndex = static_cast(role); - const char* roleLabels[] = {"Linter", "Refactor", "Generator"}; - std::string comboId = "Role##" + s.sessionId; - if (ImGui::Combo(comboId.c_str(), &roleIndex, roleLabels, 3)) { - state.setAgentRole(s.sessionId, static_cast(roleIndex)); - } - ImGui::SameLine(420.0f); - ImGui::Text("Msgs: %d", s.messageCount); - ImGui::SameLine(520.0f); - ImGui::Text("Last: %llu", (unsigned long long)s.lastMessageAtMs); - ImGui::SameLine(680.0f); - std::string btnId = "Disconnect##" + s.sessionId; - if (ImGui::Button(btnId.c_str())) { - if (state.agentTransport) { - state.agentTransport->simulateDisconnect(s.sessionId); - } else { - state.logAgentEvent("STUB: disconnect not supported for real transport"); - } - } - ImGui::Separator(); - } - } - - ImGui::Separator(); - ImGui::TextUnformatted("Activity Log"); - ImGui::BeginChild("##agentLog", ImVec2(0, 0), false); - for (const auto& line : state.agentLog) { - ImGui::TextUnformatted(line.c_str()); - } - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20) - ImGui::SetScrollHereY(1.0f); - ImGui::EndChild(); - - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Marketplace")) { - ImGui::PushFont(monoFont); - renderAgentMarketplace(state.agentMarketplace, - state.agentRegistry, - state.outputLog, - state.workspaceRoot); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Build")) { - ImGui::PushFont(uiFont); - ImGui::Text("Detected: %s", BuildSystem::typeName(state.buildType)); - auto cmds = BuildSystem::commandsFor(state.buildType); - if (cmds.empty()) { - ImGui::TextDisabled("(no build system detected)"); - } else { - for (const auto& cmd : cmds) { - if (ImGui::Button(cmd.label.c_str())) { - state.runBuildCommand(cmd); - } - ImGui::SameLine(); - ImGui::TextUnformatted(cmd.command.c_str()); - } - } - - ImGui::Separator(); - ImGui::TextUnformatted("Errors"); - if (state.buildErrors.empty()) { - ImGui::TextDisabled("(none)"); - } else { - for (const auto& err : state.buildErrors) { - std::string label = err.file + ":" + std::to_string(err.line); - if (err.col > 0) label += ":" + std::to_string(err.col); - label += " " + err.message; - if (ImGui::Selectable(label.c_str())) { - std::filesystem::path p(err.file); - std::string path = p.is_absolute() - ? p.string() - : (std::filesystem::path(state.workspaceRoot) / p).string(); - if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); - else state.doOpen(path); - if (state.active()) { - state.jumpTo(state.active(), - std::max(0, err.line - 1), - std::max(0, err.col - 1)); - } - } - } - } - - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Help")) { - ImGui::PushFont(uiFont); - renderHelpPanel(state.helpPanel, state.workspaceRoot, monoFont); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Problems (LSP diagnostics) - if (ImGui::BeginTabItem("Problems")) { - ImGui::PushFont(monoFont); - ImGui::BeginChild("##problemsScroll", ImVec2(0, 0), false); - auto diags = state.lsp ? state.lsp->getDiagnostics() : std::vector{}; - const auto& whetDiags = state.whetstoneDiagnostics; - const auto& emacsDiags = state.emacsDiagnostics; - if (diags.empty() && whetDiags.empty() && emacsDiags.empty()) { - ImGui::TextDisabled("(no diagnostics)"); - } else { - for (const auto& d : diags) { - const char* sev = "Unknown"; - if (d.severity == 1) sev = "Error"; - else if (d.severity == 2) sev = "Warning"; - else if (d.severity == 3) sev = "Info"; - else if (d.severity == 4) sev = "Hint"; - - std::string path = EditorState::fromFileUri(d.uri); - std::string label = "[" + std::string(sev) + "] " + d.message + - " (" + path + ":" + std::to_string(d.range.start.line + 1) + - ":" + std::to_string(d.range.start.character + 1) + ")"; - if (ImGui::Selectable(label.c_str())) { - if (!path.empty()) { - if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); - else state.doOpen(path); - state.jumpTo(state.active(), d.range.start.line, d.range.start.character); - } - } - } - for (const auto& d : whetDiags) { - const char* sev = "Unknown"; - if (d.severity == 1) sev = "Error"; - else if (d.severity == 2) sev = "Warning"; - else if (d.severity == 3) sev = "Info"; - - std::string path = EditorState::fromFileUri(d.uri); - std::string label = "[" + std::string(sev) + "] " + d.message + - " (" + path + ":" + std::to_string(d.line + 1) + - ":" + std::to_string(d.character + 1) + ")"; - if (ImGui::Selectable(label.c_str())) { - if (!path.empty()) { - if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); - else state.doOpen(path); - state.jumpTo(state.active(), d.line, d.character); - } - } - } - for (const auto& d : emacsDiags) { - const char* sev = "Error"; - std::string label = "[" + std::string(sev) + "] " + d.message + - " (" + d.uri + ")"; - ImGui::TextUnformatted(label.c_str()); - } - } - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Optimization controls - if (ImGui::BeginTabItem("Optimize")) { - ImGui::PushFont(uiFont); - Module* ast = state.mutationAST(); - if (!state.active()) { - ImGui::TextDisabled("(no active buffer)"); - } else if (state.active()->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(disabled in Text mode)"); - } else if (!ast) { - ImGui::TextDisabled("(no AST)"); - } else if (state.active()->readOnly) { - ImGui::TextDisabled("(read-only buffer)"); - } else { - ImGui::Checkbox("Preview changes", &state.optimizePreview); - std::string blockReason = findOptimizationBlockReason(ast); - bool blocked = !blockReason.empty(); - - auto showBlockedTooltip = [&](const std::string& reason) { - if (!reason.empty() && - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(reason.c_str()); - ImGui::EndTooltip(); - } - }; - - if (blocked) ImGui::BeginDisabled(); - if (ImGui::Button("Constant Fold")) { - std::string beforeText = state.active()->editBuf; - if (state.optimizePreview) { - auto previewAst = cloneModule(ast); - TransformEngine engine; - engine.setRoot(previewAst.get()); - engine.constantFolding(); - std::string afterText = generateForLanguage(previewAst.get(), - state.active()->language); - state.openDiff(beforeText, afterText, true, 1, {}); - } else { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tid = inc.applyTransform("constant-fold"); - auto history = inc.getTransformHistory(); - size_t nodes = 0; - for (const auto& h : history) { - if (h.transformId == tid) { - nodes = h.affectedNodeIds.size(); - break; - } - } - std::string warning = findOptimizationLockWarning(ast); - std::string summary = "Constant Fold: "; - if (nodes > 0) { - summary += "applied (" + std::to_string(nodes) + " nodes)"; - } else { - summary += "no changes"; - } - if (!warning.empty()) { - summary += " — warning: " + warning; - state.outputLog += warning + "\n"; - } - state.optimizeFoldSummary = summary; - state.outputLog += summary + "\n"; - if (nodes > 0) state.applyOrchestratorToActive(); - std::string afterText = state.active()->editBuf; - state.openDiff(beforeText, afterText, false, 1, {tid}); - } - } - showBlockedTooltip(blockReason); - if (blocked) ImGui::EndDisabled(); - if (!state.optimizeFoldSummary.empty()) { - ImGui::TextWrapped("%s", state.optimizeFoldSummary.c_str()); - } - - ImGui::Separator(); - - if (blocked) ImGui::BeginDisabled(); - if (ImGui::Button("Dead Code Elimination")) { - std::string beforeText = state.active()->editBuf; - if (state.optimizePreview) { - auto previewAst = cloneModule(ast); - TransformEngine engine; - engine.setRoot(previewAst.get()); - engine.deadCodeElimination(); - std::string afterText = generateForLanguage(previewAst.get(), - state.active()->language); - state.openDiff(beforeText, afterText, true, 2, {}); - } else { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tid = inc.applyTransform("dead-code-elim"); - auto history = inc.getTransformHistory(); - size_t nodes = 0; - for (const auto& h : history) { - if (h.transformId == tid) { - nodes = h.affectedNodeIds.size(); - break; - } - } - std::string summary = "Dead Code Elimination: "; - if (nodes > 0) { - summary += "applied (" + std::to_string(nodes) + " nodes)"; - } else { - summary += "no changes"; - } - state.optimizeDeadCodeSummary = summary; - state.outputLog += summary + "\n"; - if (nodes > 0) state.applyOrchestratorToActive(); - std::string afterText = state.active()->editBuf; - state.openDiff(beforeText, afterText, false, 2, {tid}); - } - } - showBlockedTooltip(blockReason); - if (blocked) ImGui::EndDisabled(); - if (!state.optimizeDeadCodeSummary.empty()) { - ImGui::TextWrapped("%s", state.optimizeDeadCodeSummary.c_str()); - } - - ImGui::Separator(); - - if (blocked) ImGui::BeginDisabled(); - if (ImGui::Button("Apply All")) { - std::string beforeText = state.active()->editBuf; - if (state.optimizePreview) { - auto previewAst = cloneModule(ast); - TransformEngine engine; - engine.setRoot(previewAst.get()); - engine.applyAll(); - std::string afterText = generateForLanguage(previewAst.get(), - state.active()->language); - state.openDiff(beforeText, afterText, true, 3, {}); - } else { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tidFold = inc.applyTransform("constant-fold"); - std::string tidDce = inc.applyTransform("dead-code-elim"); - auto history = inc.getTransformHistory(); - size_t totalNodes = 0; - for (const auto& h : history) { - if (h.transformId == tidFold || h.transformId == tidDce) { - totalNodes += h.affectedNodeIds.size(); - } - } - std::string warning = findOptimizationLockWarning(ast); - std::string summary = "Apply All: "; - if (totalNodes > 0) { - summary += "applied (" + std::to_string(totalNodes) + " nodes)"; - } else { - summary += "no changes"; - } - if (!warning.empty()) { - summary += " — warning: " + warning; - state.outputLog += warning + "\n"; - } - state.optimizeApplySummary = summary; - state.outputLog += summary + "\n"; - if (totalNodes > 0) state.applyOrchestratorToActive(); - std::string afterText = state.active()->editBuf; - state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce}); - } - } - showBlockedTooltip(blockReason); - if (blocked) ImGui::EndDisabled(); - if (!state.optimizeApplySummary.empty()) { - ImGui::TextWrapped("%s", state.optimizeApplySummary.c_str()); - } - } - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Transform history - if (ImGui::BeginTabItem("Transforms")) { - ImGui::PushFont(uiFont); - auto* buf = state.active(); - Module* ast = state.mutationAST(); - if (!buf) { - ImGui::TextDisabled("(no active buffer)"); - } else if (buf->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(disabled in Text mode)"); - } else if (!ast) { - ImGui::TextDisabled("(no AST)"); - } else { - auto& inc = buf->incrementalOptimizer; - inc.setRoot(ast); - auto history = inc.getTransformHistory(); - - const bool hasHistory = !history.empty(); - if (!hasHistory) ImGui::BeginDisabled(); - if (ImGui::Button("Undo All")) { - bool any = false; - while (inc.undoLast()) { - any = true; - } - if (any) state.applyOrchestratorToActive(); - } - if (!hasHistory) ImGui::EndDisabled(); - ImGui::Separator(); - - if (!hasHistory) { - ImGui::TextDisabled("(no transforms)"); - } else { - for (const auto& h : history) { - ImGui::PushID(h.transformId.c_str()); - ImVec4 color = transformColorForName(h.transformName); - ImGui::TextColored(color, "%s", h.transformName.c_str()); - ImGui::SameLine(); - ImGui::TextDisabled("@ %s", h.timestamp.c_str()); - if (!h.actor.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("by %s", h.actor.c_str()); - } - ImGui::SameLine(); - if (!h.reversible) ImGui::BeginDisabled(); - if (ImGui::Button("Undo")) { - if (inc.undoTransform(h.transformId)) { - state.applyOrchestratorToActive(); - } - } - if (!h.reversible) ImGui::EndDisabled(); - ImGui::Text("Affected: %d", (int)h.affectedNodeIds.size()); - std::string nodeList; - for (size_t i = 0; i < h.affectedNodeIds.size() && i < 5; ++i) { - if (!nodeList.empty()) nodeList += ", "; - nodeList += h.affectedNodeIds[i]; - } - if (h.affectedNodeIds.size() > 5) nodeList += ", ..."; - if (nodeList.empty()) nodeList = "(none)"; - ImGui::TextWrapped("Nodes: %s", nodeList.c_str()); - ImGui::Separator(); - ImGui::PopID(); - } - } - } - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Diff view - if (ImGui::BeginTabItem("Diff")) { - ImGui::PushFont(uiFont); - auto* buf = state.active(); - Module* ast = state.mutationAST(); - if (!buf) { - ImGui::TextDisabled("(no active buffer)"); - } else if (buf->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(diff view disabled in Text mode)"); - } else if (!state.diff.active) { - ImGui::TextDisabled("(no diff)"); - } else { - if (state.diff.preview) { - if (ImGui::Button("Apply")) { - if (ast && !buf->readOnly) { - if (state.diff.batch) { - BatchMutationAPI batch; - batch.setRoot(ast); - auto res = batch.applySequence(state.diff.batchMutations); - if (!res.success) { - state.outputLog += res.error + "\n"; - } else { - state.outputLog += "Refactor applied (" + - std::to_string(res.appliedCount) + " mutations).\n"; - state.applyOrchestratorToActive(); - } - } else { - buf->incrementalOptimizer.setRoot(ast); - if (state.diff.action == 1) { - buf->incrementalOptimizer.applyTransform("constant-fold"); - } else if (state.diff.action == 2) { - buf->incrementalOptimizer.applyTransform("dead-code-elim"); - } else if (state.diff.action == 3) { - buf->incrementalOptimizer.applyTransform("constant-fold"); - buf->incrementalOptimizer.applyTransform("dead-code-elim"); - } - state.applyOrchestratorToActive(); - } - } - state.diff.active = false; - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - state.diff.active = false; - } - } else { - if (ImGui::Button("Keep")) { - state.diff.active = false; - } - ImGui::SameLine(); - if (ImGui::Button("Undo")) { - if (ast && !buf->readOnly) { - buf->incrementalOptimizer.setRoot(ast); - for (auto it = state.diff.transformIds.rbegin(); - it != state.diff.transformIds.rend(); ++it) { - buf->incrementalOptimizer.undoTransform(*it); - } - state.applyOrchestratorToActive(); - } - state.diff.active = false; - } - } - - ImGui::Separator(); - ImGui::BeginTable("##diffSplit", 2, - ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); - ImGui::TableSetupColumn("Before", ImGuiTableColumnFlags_WidthStretch, 0.5f); - ImGui::TableSetupColumn("After", ImGuiTableColumnFlags_WidthStretch, 0.5f); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - CodeEditorOptions leftOpts; - leftOpts.readOnly = true; - leftOpts.showWhitespace = state.showWhitespace; - leftOpts.showLineNumbers = state.showLineNumbers; - leftOpts.showCurrentLine = false; - leftOpts.highlightLines = &state.diff.beforeLines; - leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120); - leftOpts.syncScrollX = &state.diffScrollX; - leftOpts.syncScrollY = &state.diffScrollY; - leftOpts.scrollMaster = true; - state.diffLeftWidget.render("##diffBefore", - state.diff.beforeText, std::vector{}, leftOpts, - ImVec2(0, 0), monoFont); - - ImGui::TableSetColumnIndex(1); - CodeEditorOptions rightOpts; - rightOpts.readOnly = true; - rightOpts.showWhitespace = state.showWhitespace; - rightOpts.showLineNumbers = state.showLineNumbers; - rightOpts.showCurrentLine = false; - rightOpts.highlightLines = &state.diff.afterLines; - rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120); - rightOpts.syncScrollX = &state.diffScrollX; - rightOpts.syncScrollY = &state.diffScrollY; - rightOpts.scrollMaster = false; - state.diffRightWidget.render("##diffAfter", - state.diff.afterText, std::vector{}, rightOpts, - ImVec2(0, 0), monoFont); - ImGui::EndTable(); - } - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // AST view - if (ImGui::BeginTabItem("AST")) { - ImGui::PushFont(monoFont); - ImGui::BeginChild("##astScroll", ImVec2(0, 0), false); - Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(disabled in Text mode)"); - } else if (ast) { - std::map transformNames; - std::map transformActors; - if (state.active()) { - auto history = state.active()->incrementalOptimizer.getTransformHistory(); - for (const auto& h : history) { - transformNames[h.transformId] = h.transformName; - transformActors[h.transformId] = h.actor; - } - } - // Show basic AST info - ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), - "Module: %s [%s]", - ast->name.c_str(), ast->targetLanguage.c_str()); - ImGui::Separator(); - auto functions = ast->getChildren("functions"); - for (size_t i = 0; i < functions.size(); ++i) { - auto* fn = static_cast(functions[i]); - std::string tid; - std::string tname; - if (state.active()) { - tid = state.active()->incrementalOptimizer.getProvenance(fn->id); - auto it = transformNames.find(tid); - if (it != transformNames.end()) tname = it->second; - } - ImVec4 fnColor = tname.empty() ? - ImVec4(0.86f, 0.86f, 0.55f, 1.0f) : - transformColorForName(tname); - ImGui::TextColored(fnColor, " Function: %s", fn->name.c_str()); - if (!tname.empty() && ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - std::string actor = transformActors[tid]; - if (actor.empty()) { - ImGui::Text("Transform: %s (%s)", tname.c_str(), tid.c_str()); - } else { - ImGui::Text("Transform: %s (%s) by %s", - tname.c_str(), tid.c_str(), actor.c_str()); - } - ImGui::EndTooltip(); - } - auto params = fn->getChildren("parameters"); - for (auto* p : params) { - auto* param = static_cast(p); - std::string pid; - std::string pname; - if (state.active()) { - pid = state.active()->incrementalOptimizer.getProvenance(param->id); - auto it = transformNames.find(pid); - if (it != transformNames.end()) pname = it->second; - } - ImVec4 pColor = pname.empty() ? - ImVec4(0.6f, 0.78f, 0.9f, 1.0f) : - transformColorForName(pname); - ImGui::TextColored(pColor, " param: %s", param->name.c_str()); - if (!pname.empty() && ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - std::string actor = transformActors[pid]; - if (actor.empty()) { - ImGui::Text("Transform: %s (%s)", pname.c_str(), pid.c_str()); - } else { - ImGui::Text("Transform: %s (%s) by %s", - pname.c_str(), pid.c_str(), actor.c_str()); - } - ImGui::EndTooltip(); - } - } - auto body = fn->getChildren("body"); - ImGui::Text(" body: %d statement(s)", (int)body.size()); - auto annos = fn->getChildren("annotations"); - for (auto* a : annos) { - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), - " @%s", a->conceptType.c_str()); - } - } - } else { - ImGui::TextDisabled("(no AST — enter some code)"); - } - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Syntax-highlighted preview - if (ImGui::BeginTabItem("Highlighted")) { - ImGui::PushFont(monoFont); - ImGui::BeginChild("##hlScroll", ImVec2(0, 0), false); - state.updateHighlights(); - if (state.active()) - RenderHighlightedText(state.active()->editBuf, state.active()->highlights); - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - // Generated code preview - if (ImGui::BeginTabItem("Generated")) { - ImGui::PushFont(monoFont); - ImGui::BeginChild("##genScroll", ImVec2(0, 0), false); - Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(disabled in Text mode)"); - } else { - state.updateGenerated(); - if (ast && state.active()) { - ImGui::TextUnformatted(state.active()->generatedBuf.c_str()); - } else { - ImGui::TextDisabled("(no AST)"); - } - } - ImGui::EndChild(); - ImGui::PopFont(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - ImGui::End(); - - // --------------------------------------------------------------- - // Memory Strategies dashboard - // --------------------------------------------------------------- - ImGui::Begin("Memory Strategies"); - ImGui::PushFont(uiFont); - Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { - ImGui::TextDisabled("(disabled in Text mode)"); - } else if (!ast) { - ImGui::TextDisabled("(no AST)"); - } else { - std::vector entries; - collectAnnotationEntries(ast, entries); - std::map counts; - for (const auto& e : entries) counts[e.label]++; - - ImGui::Text("Annotations"); - ImGui::Separator(); - for (const auto& [label, count] : counts) { - ImGui::Text("%s: %d", label.c_str(), count); - } - ImGui::Spacing(); - - ImGui::Text("Details"); - ImGui::Separator(); - for (const auto& e : entries) { - std::string lineInfo = e.line >= 0 ? ("L" + std::to_string(e.line + 1)) : "-"; - std::string row = e.label + " — " + e.nodeName + " (" + lineInfo + ")"; - if (ImGui::Selectable(row.c_str())) { - if (state.active()) state.jumpTo(state.active(), e.line, 0); - } - } - - ImGui::Spacing(); - ImGui::Text("Suggestions"); - ImGui::Separator(); - for (const auto& s : state.suggestions) { - if (s.confidence < 0.5) continue; - std::string row = s.annotationType + "(" + s.strategy + ") — " + - s.nodeId + " (" + std::to_string(s.confidence) + ")"; - ImGui::TextUnformatted(row.c_str()); - } - - if (ImGui::Button("Export JSON")) { - auto j = buildAnnotationSummaryJson(entries); - state.outputLog += "Annotation summary:\\n" + j.dump(2) + "\\n"; - } - } - ImGui::PopFont(); - ImGui::End(); - - // --------------------------------------------------------------- - // Status bar - // --------------------------------------------------------------- - { - ImGuiWindowFlags sbFlags = ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoSavedSettings; - float sbHeight = ImGui::GetFrameHeight() + 2; - ImVec2 sbPos(viewport->WorkPos.x, viewport->WorkPos.y + viewport->WorkSize.y - sbHeight); - ImVec2 sbSize(viewport->WorkSize.x, sbHeight); - ImGui::SetNextWindowPos(sbPos); - ImGui::SetNextWindowSize(sbSize); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.00f, 0.47f, 0.84f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 2)); - ImGui::Begin("##StatusBar", nullptr, sbFlags); - ImGui::PushFont(uiFont); - - // Left side: line/col - if (state.active()) - ImGui::Text("Ln %d, Col %d", state.active()->cursorLine, state.active()->cursorCol); - else - ImGui::Text("Ln -, Col -"); - ImGui::SameLine(0, 30); - - // Language - if (state.active()) - ImGui::Text("%s", state.active()->language.c_str()); - else - ImGui::Text("-"); - ImGui::SameLine(0, 30); - - // Mode - if (state.active()) { - const char* modeLabel = - state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text" : "Structured"; - ImGui::Text("Mode: %s", modeLabel); - } else { - ImGui::Text("Mode: -"); - } - ImGui::SameLine(0, 30); - - // 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)); - ImGui::SameLine(0, 30); - - // Undo depth - ImGui::Text("Undo: %d", state.active() ? state.active()->undoDepth : 0); - ImGui::SameLine(0, 30); - - if (state.runInProgress) { - ImGui::Text("Run: Running..."); - } else if (state.hasRunResult) { - ImGui::Text("Run: Exit %d", state.lastRunExitCode); - } else { - ImGui::Text("Run: -"); - } - ImGui::SameLine(0, 30); - - // Modified indicator - if (state.active() && state.active()->modified) - ImGui::Text("Modified"); - else - ImGui::Text("Saved"); - - ImGui::SameLine(0, 30); - ImGui::Text("UTF-8"); - - ImGui::PopFont(); - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - } - - // Render + // --- Render all panels --- + renderMenuBar(state); + renderExplorerPanel(state); + renderOutlinePanel(state); + renderDependenciesPanel(state); + renderLibrariesPanel(state); + renderCompositionPanel(state); + renderEmacsPackagesPanel(state); + renderEmacsBridgePanel(state); + renderMinibuffer(state); + renderFindReplaceBar(state); + renderProjectSearchPanel(state); + renderSettingsPanel(state); + renderGoToLineDialog(state); + renderLspSettingsPanel(state); + renderRefactorPopup(state); + renderCommandPalette(state); + renderEditorPanel(state); + renderBottomPanel(state); + renderMemoryStrategiesPanel(state); + renderStatusBar(state); + + // Check for exit request from menu + if (state.exitRequested) done = true; + + // --- Render frame --- ImGui::Render(); glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); glClearColor(0.12f, 0.12f, 0.12f, 1.00f); diff --git a/editor/src/panels/BottomPanel.h b/editor/src/panels/BottomPanel.h new file mode 100644 index 0000000..99be410 --- /dev/null +++ b/editor/src/panels/BottomPanel.h @@ -0,0 +1,672 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderBottomPanel(EditorState& state) { + // --------------------------------------------------------------- + // Bottom panel — Output / AST / Highlighted Preview / Terminal + // --------------------------------------------------------------- + ImGui::Begin("Panel"); + + if (ImGui::BeginTabBar("PanelTabs")) { + // Output log + 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 + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20) + ImGui::SetScrollHereY(1.0f); + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + if (state.build.showTerminalPanel && ImGui::BeginTabItem("Terminal")) { + state.build.terminal.render(state.workspaceRoot, state.monoFont); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Agents")) { + ImGui::PushFont(state.monoFont); + bool running = state.agent.server && state.agent.server->isRunning(); + ImGui::Text("Server: %s", running ? "Running" : "Stopped"); + ImGui::SameLine(0, 20); + ImGui::Text("Port: %d", state.agent.port); + ImGui::Separator(); + + ImGui::TextUnformatted("Active Sessions"); + if (!state.agent.server || state.agent.server->getActiveSessionCount() == 0) { + ImGui::TextDisabled("(none)"); + } else { + for (const auto& s : state.agent.server->getActiveSessions()) { + std::string label = s.sessionId; + if (!s.agentName.empty()) label += " (" + s.agentName + ")"; + ImGui::TextUnformatted(label.c_str()); + ImGui::SameLine(260.0f); + AgentRole role = state.getAgentRole(s.sessionId); + int roleIndex = static_cast(role); + const char* roleLabels[] = {"Linter", "Refactor", "Generator"}; + std::string comboId = "Role##" + s.sessionId; + if (ImGui::Combo(comboId.c_str(), &roleIndex, roleLabels, 3)) { + state.setAgentRole(s.sessionId, static_cast(roleIndex)); + } + ImGui::SameLine(420.0f); + ImGui::Text("Msgs: %d", s.messageCount); + ImGui::SameLine(520.0f); + ImGui::Text("Last: %llu", (unsigned long long)s.lastMessageAtMs); + ImGui::SameLine(680.0f); + std::string btnId = "Disconnect##" + s.sessionId; + if (ImGui::Button(btnId.c_str())) { + if (state.agent.transport) { + state.agent.transport->simulateDisconnect(s.sessionId); + } else { + state.logAgentEvent("STUB: disconnect not supported for real transport"); + } + } + ImGui::Separator(); + } + } + + ImGui::Separator(); + ImGui::TextUnformatted("Activity Log"); + ImGui::BeginChild("##agentLog", ImVec2(0, 0), false); + for (const auto& line : state.agent.log) { + ImGui::TextUnformatted(line.c_str()); + } + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20) + ImGui::SetScrollHereY(1.0f); + ImGui::EndChild(); + + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Marketplace")) { + ImGui::PushFont(state.monoFont); + renderAgentMarketplace(state.agent.marketplace, + state.agent.registry, + state.outputLog, + state.workspaceRoot); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Build")) { + ImGui::PushFont(state.uiFont); + ImGui::Text("Detected: %s", BuildSystem::typeName(state.build.buildType)); + auto cmds = BuildSystem::commandsFor(state.build.buildType); + if (cmds.empty()) { + ImGui::TextDisabled("(no build system detected)"); + } else { + for (const auto& cmd : cmds) { + if (ImGui::Button(cmd.label.c_str())) { + state.runBuildCommand(cmd); + } + ImGui::SameLine(); + ImGui::TextUnformatted(cmd.command.c_str()); + } + } + + ImGui::Separator(); + ImGui::TextUnformatted("Errors"); + if (state.build.buildErrors.empty()) { + ImGui::TextDisabled("(none)"); + } else { + for (const auto& err : state.build.buildErrors) { + std::string label = err.file + ":" + std::to_string(err.line); + if (err.col > 0) label += ":" + std::to_string(err.col); + label += " " + err.message; + if (ImGui::Selectable(label.c_str())) { + std::filesystem::path p(err.file); + std::string path = p.is_absolute() + ? p.string() + : (std::filesystem::path(state.workspaceRoot) / p).string(); + if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); + else state.doOpen(path); + if (state.active()) { + state.jumpTo(state.active(), + std::max(0, err.line - 1), + std::max(0, err.col - 1)); + } + } + } + } + + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Help")) { + ImGui::PushFont(state.uiFont); + renderHelpPanel(state.helpPanel, state.workspaceRoot, state.monoFont); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Problems (LSP diagnostics) + if (ImGui::BeginTabItem("Problems")) { + ImGui::PushFont(state.monoFont); + ImGui::BeginChild("##problemsScroll", ImVec2(0, 0), false); + auto diags = state.lsp ? state.lsp->getDiagnostics() : std::vector{}; + const auto& whetDiags = state.whetstoneDiagnostics; + const auto& emacsDiags = state.emacsDiagnostics; + if (diags.empty() && whetDiags.empty() && emacsDiags.empty()) { + ImGui::TextDisabled("(no diagnostics)"); + } else { + for (const auto& d : diags) { + const char* sev = "Unknown"; + if (d.severity == 1) sev = "Error"; + else if (d.severity == 2) sev = "Warning"; + else if (d.severity == 3) sev = "Info"; + else if (d.severity == 4) sev = "Hint"; + + std::string path = EditorState::fromFileUri(d.uri); + std::string label = "[" + std::string(sev) + "] " + d.message + + " (" + path + ":" + std::to_string(d.range.start.line + 1) + + ":" + std::to_string(d.range.start.character + 1) + ")"; + if (ImGui::Selectable(label.c_str())) { + if (!path.empty()) { + if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); + else state.doOpen(path); + state.jumpTo(state.active(), d.range.start.line, d.range.start.character); + } + } + } + for (const auto& d : whetDiags) { + const char* sev = "Unknown"; + if (d.severity == 1) sev = "Error"; + else if (d.severity == 2) sev = "Warning"; + else if (d.severity == 3) sev = "Info"; + + std::string path = EditorState::fromFileUri(d.uri); + std::string label = "[" + std::string(sev) + "] " + d.message + + " (" + path + ":" + std::to_string(d.line + 1) + + ":" + std::to_string(d.character + 1) + ")"; + if (ImGui::Selectable(label.c_str())) { + if (!path.empty()) { + if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); + else state.doOpen(path); + state.jumpTo(state.active(), d.line, d.character); + } + } + } + for (const auto& d : emacsDiags) { + const char* sev = "Error"; + std::string label = "[" + std::string(sev) + "] " + d.message + + " (" + d.uri + ")"; + ImGui::TextUnformatted(label.c_str()); + } + } + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Optimization controls + if (ImGui::BeginTabItem("Optimize")) { + ImGui::PushFont(state.uiFont); + Module* ast = state.mutationAST(); + if (!state.active()) { + ImGui::TextDisabled("(no active buffer)"); + } else if (state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (!ast) { + ImGui::TextDisabled("(no AST)"); + } else if (state.active()->readOnly) { + ImGui::TextDisabled("(read-only buffer)"); + } else { + ImGui::Checkbox("Preview changes", &state.optimizePreview); + std::string blockReason = findOptimizationBlockReason(ast); + bool blocked = !blockReason.empty(); + + auto showBlockedTooltip = [&](const std::string& reason) { + if (!reason.empty() && + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(reason.c_str()); + ImGui::EndTooltip(); + } + }; + + if (blocked) ImGui::BeginDisabled(); + if (ImGui::Button("Constant Fold")) { + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.constantFolding(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 1, {}); + } else { + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("constant-fold"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } + std::string warning = findOptimizationLockWarning(ast); + std::string summary = "Constant Fold: "; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; + } else { + summary += "no changes"; + } + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; + } + state.optimizeFoldSummary = summary; + state.outputLog += summary + "\n"; + if (nodes > 0) state.applyOrchestratorToActive(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 1, {tid}); + } + } + showBlockedTooltip(blockReason); + if (blocked) ImGui::EndDisabled(); + if (!state.optimizeFoldSummary.empty()) { + ImGui::TextWrapped("%s", state.optimizeFoldSummary.c_str()); + } + + ImGui::Separator(); + + if (blocked) ImGui::BeginDisabled(); + if (ImGui::Button("Dead Code Elimination")) { + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.deadCodeElimination(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 2, {}); + } else { + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } + std::string summary = "Dead Code Elimination: "; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; + } else { + summary += "no changes"; + } + state.optimizeDeadCodeSummary = summary; + state.outputLog += summary + "\n"; + if (nodes > 0) state.applyOrchestratorToActive(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 2, {tid}); + } + } + showBlockedTooltip(blockReason); + if (blocked) ImGui::EndDisabled(); + if (!state.optimizeDeadCodeSummary.empty()) { + ImGui::TextWrapped("%s", state.optimizeDeadCodeSummary.c_str()); + } + + ImGui::Separator(); + + if (blocked) ImGui::BeginDisabled(); + if (ImGui::Button("Apply All")) { + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.applyAll(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 3, {}); + } else { + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tidFold = inc.applyTransform("constant-fold"); + std::string tidDce = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t totalNodes = 0; + for (const auto& h : history) { + if (h.transformId == tidFold || h.transformId == tidDce) { + totalNodes += h.affectedNodeIds.size(); + } + } + std::string warning = findOptimizationLockWarning(ast); + std::string summary = "Apply All: "; + if (totalNodes > 0) { + summary += "applied (" + std::to_string(totalNodes) + " nodes)"; + } else { + summary += "no changes"; + } + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; + } + state.optimizeApplySummary = summary; + state.outputLog += summary + "\n"; + if (totalNodes > 0) state.applyOrchestratorToActive(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce}); + } + } + showBlockedTooltip(blockReason); + if (blocked) ImGui::EndDisabled(); + if (!state.optimizeApplySummary.empty()) { + ImGui::TextWrapped("%s", state.optimizeApplySummary.c_str()); + } + } + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Transform history + if (ImGui::BeginTabItem("Transforms")) { + ImGui::PushFont(state.uiFont); + auto* buf = state.active(); + Module* ast = state.mutationAST(); + if (!buf) { + ImGui::TextDisabled("(no active buffer)"); + } else if (buf->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (!ast) { + ImGui::TextDisabled("(no AST)"); + } else { + auto& inc = buf->incrementalOptimizer; + inc.setRoot(ast); + auto history = inc.getTransformHistory(); + + const bool hasHistory = !history.empty(); + if (!hasHistory) ImGui::BeginDisabled(); + if (ImGui::Button("Undo All")) { + bool any = false; + while (inc.undoLast()) { + any = true; + } + if (any) state.applyOrchestratorToActive(); + } + if (!hasHistory) ImGui::EndDisabled(); + ImGui::Separator(); + + if (!hasHistory) { + ImGui::TextDisabled("(no transforms)"); + } else { + for (const auto& h : history) { + ImGui::PushID(h.transformId.c_str()); + ImVec4 color = transformColorForName(h.transformName); + ImGui::TextColored(color, "%s", h.transformName.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("@ %s", h.timestamp.c_str()); + if (!h.actor.empty()) { + ImGui::SameLine(); + ImGui::TextDisabled("by %s", h.actor.c_str()); + } + ImGui::SameLine(); + if (!h.reversible) ImGui::BeginDisabled(); + if (ImGui::Button("Undo")) { + if (inc.undoTransform(h.transformId)) { + state.applyOrchestratorToActive(); + } + } + if (!h.reversible) ImGui::EndDisabled(); + ImGui::Text("Affected: %d", (int)h.affectedNodeIds.size()); + std::string nodeList; + for (size_t i = 0; i < h.affectedNodeIds.size() && i < 5; ++i) { + if (!nodeList.empty()) nodeList += ", "; + nodeList += h.affectedNodeIds[i]; + } + if (h.affectedNodeIds.size() > 5) nodeList += ", ..."; + if (nodeList.empty()) nodeList = "(none)"; + ImGui::TextWrapped("Nodes: %s", nodeList.c_str()); + ImGui::Separator(); + ImGui::PopID(); + } + } + } + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Diff view + if (ImGui::BeginTabItem("Diff")) { + ImGui::PushFont(state.uiFont); + auto* buf = state.active(); + Module* ast = state.mutationAST(); + if (!buf) { + ImGui::TextDisabled("(no active buffer)"); + } else if (buf->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(diff view disabled in Text mode)"); + } else if (!state.diff.active) { + ImGui::TextDisabled("(no diff)"); + } else { + if (state.diff.preview) { + if (ImGui::Button("Apply")) { + if (ast && !buf->readOnly) { + if (state.diff.batch) { + BatchMutationAPI batch; + batch.setRoot(ast); + auto res = batch.applySequence(state.diff.batchMutations); + if (!res.success) { + state.outputLog += res.error + "\n"; + } else { + state.outputLog += "Refactor applied (" + + std::to_string(res.appliedCount) + " mutations).\n"; + state.applyOrchestratorToActive(); + } + } else { + buf->incrementalOptimizer.setRoot(ast); + if (state.diff.action == 1) { + buf->incrementalOptimizer.applyTransform("constant-fold"); + } else if (state.diff.action == 2) { + buf->incrementalOptimizer.applyTransform("dead-code-elim"); + } else if (state.diff.action == 3) { + buf->incrementalOptimizer.applyTransform("constant-fold"); + buf->incrementalOptimizer.applyTransform("dead-code-elim"); + } + state.applyOrchestratorToActive(); + } + } + state.diff.active = false; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.diff.active = false; + } + } else { + if (ImGui::Button("Keep")) { + state.diff.active = false; + } + ImGui::SameLine(); + if (ImGui::Button("Undo")) { + if (ast && !buf->readOnly) { + buf->incrementalOptimizer.setRoot(ast); + for (auto it = state.diff.transformIds.rbegin(); + it != state.diff.transformIds.rend(); ++it) { + buf->incrementalOptimizer.undoTransform(*it); + } + state.applyOrchestratorToActive(); + } + state.diff.active = false; + } + } + + ImGui::Separator(); + ImGui::BeginTable("##diffSplit", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("Before", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn("After", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + CodeEditorOptions leftOpts; + leftOpts.readOnly = true; + leftOpts.showWhitespace = state.ui.showWhitespace; + leftOpts.showLineNumbers = state.ui.showLineNumbers; + leftOpts.showCurrentLine = false; + leftOpts.highlightLines = &state.diff.beforeLines; + leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120); + leftOpts.syncScrollX = &state.diffScrollX; + leftOpts.syncScrollY = &state.diffScrollY; + leftOpts.scrollMaster = true; + state.diffLeftWidget.render("##diffBefore", + state.diff.beforeText, std::vector{}, leftOpts, + ImVec2(0, 0), state.monoFont); + + ImGui::TableSetColumnIndex(1); + CodeEditorOptions rightOpts; + rightOpts.readOnly = true; + rightOpts.showWhitespace = state.ui.showWhitespace; + rightOpts.showLineNumbers = state.ui.showLineNumbers; + rightOpts.showCurrentLine = false; + rightOpts.highlightLines = &state.diff.afterLines; + rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120); + rightOpts.syncScrollX = &state.diffScrollX; + rightOpts.syncScrollY = &state.diffScrollY; + rightOpts.scrollMaster = false; + state.diffRightWidget.render("##diffAfter", + state.diff.afterText, std::vector{}, rightOpts, + ImVec2(0, 0), state.monoFont); + ImGui::EndTable(); + } + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // AST view + if (ImGui::BeginTabItem("AST")) { + ImGui::PushFont(state.monoFont); + ImGui::BeginChild("##astScroll", ImVec2(0, 0), false); + Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (ast) { + std::map transformNames; + std::map transformActors; + if (state.active()) { + auto history = state.active()->incrementalOptimizer.getTransformHistory(); + for (const auto& h : history) { + transformNames[h.transformId] = h.transformName; + transformActors[h.transformId] = h.actor; + } + } + // Show basic AST info + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), + "Module: %s [%s]", + ast->name.c_str(), ast->targetLanguage.c_str()); + ImGui::Separator(); + auto functions = ast->getChildren("functions"); + for (size_t i = 0; i < functions.size(); ++i) { + auto* fn = static_cast(functions[i]); + std::string tid; + std::string tname; + if (state.active()) { + tid = state.active()->incrementalOptimizer.getProvenance(fn->id); + auto it = transformNames.find(tid); + if (it != transformNames.end()) tname = it->second; + } + ImVec4 fnColor = tname.empty() ? + ImVec4(0.86f, 0.86f, 0.55f, 1.0f) : + transformColorForName(tname); + ImGui::TextColored(fnColor, " Function: %s", fn->name.c_str()); + if (!tname.empty() && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + std::string actor = transformActors[tid]; + if (actor.empty()) { + ImGui::Text("Transform: %s (%s)", tname.c_str(), tid.c_str()); + } else { + ImGui::Text("Transform: %s (%s) by %s", + tname.c_str(), tid.c_str(), actor.c_str()); + } + ImGui::EndTooltip(); + } + auto params = fn->getChildren("parameters"); + for (auto* p : params) { + auto* param = static_cast(p); + std::string pid; + std::string pname; + if (state.active()) { + pid = state.active()->incrementalOptimizer.getProvenance(param->id); + auto it = transformNames.find(pid); + if (it != transformNames.end()) pname = it->second; + } + ImVec4 pColor = pname.empty() ? + ImVec4(0.6f, 0.78f, 0.9f, 1.0f) : + transformColorForName(pname); + ImGui::TextColored(pColor, " param: %s", param->name.c_str()); + if (!pname.empty() && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + std::string actor = transformActors[pid]; + if (actor.empty()) { + ImGui::Text("Transform: %s (%s)", pname.c_str(), pid.c_str()); + } else { + ImGui::Text("Transform: %s (%s) by %s", + pname.c_str(), pid.c_str(), actor.c_str()); + } + ImGui::EndTooltip(); + } + } + auto body = fn->getChildren("body"); + ImGui::Text(" body: %d statement(s)", (int)body.size()); + auto annos = fn->getChildren("annotations"); + for (auto* a : annos) { + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), + " @%s", a->conceptType.c_str()); + } + } + } else { + ImGui::TextDisabled("(no AST — enter some code)"); + } + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Syntax-highlighted preview + if (ImGui::BeginTabItem("Highlighted")) { + ImGui::PushFont(state.monoFont); + ImGui::BeginChild("##hlScroll", ImVec2(0, 0), false); + state.updateHighlights(); + if (state.active()) + RenderHighlightedText(state.active()->editBuf, state.active()->highlights); + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + // Generated code preview + if (ImGui::BeginTabItem("Generated")) { + ImGui::PushFont(state.monoFont); + ImGui::BeginChild("##genScroll", ImVec2(0, 0), false); + Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else { + state.updateGenerated(); + if (ast && state.active()) { + ImGui::TextUnformatted(state.active()->generatedBuf.c_str()); + } else { + ImGui::TextDisabled("(no AST)"); + } + } + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); +} diff --git a/editor/src/panels/DialogPanels.h b/editor/src/panels/DialogPanels.h new file mode 100644 index 0000000..aaf3859 --- /dev/null +++ b/editor/src/panels/DialogPanels.h @@ -0,0 +1,217 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderGoToLineDialog(EditorState& state) { + if (state.search.showGoToLine) { + ImGui::OpenPopup("GoToLine"); + } + if (ImGui::BeginPopupModal("GoToLine", &state.search.showGoToLine, ImGuiWindowFlags_AlwaysAutoResize)) { + int totalLines = state.active() ? countLines(state.active()->editBuf) : 0; + ImGui::Text("Enter line or :line:col"); + ImGui::TextDisabled("Total lines: %d", totalLines); + ImGui::SetNextItemWidth(240); + bool submit = ImGui::InputText("##gotoLineInput", state.search.goToLineBuf, + sizeof(state.search.goToLineBuf), + ImGuiInputTextFlags_EnterReturnsTrue); + if (ImGui::Button("Go")) submit = true; + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.search.showGoToLine = false; + state.search.goToLineError = false; + ImGui::CloseCurrentPopup(); + } + if (submit) { + int line = 0; + int col = 0; + if (parseLineColInput(state.search.goToLineBuf, line, col)) { + if (totalLines > 0) line = std::max(1, std::min(line, totalLines)); + col = std::max(1, col); + if (state.active()) { + state.jumpTo(state.active(), line - 1, col - 1); + } + state.search.showGoToLine = false; + state.search.goToLineError = false; + ImGui::CloseCurrentPopup(); + } else { + state.search.goToLineError = true; + } + } + if (state.search.goToLineError) { + ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "Invalid format."); + } + ImGui::EndPopup(); + } +} + +static void renderLspSettingsPanel(EditorState& state) { + if (!state.ui.showLspSettings) return; + ImGui::Begin("LSP Servers", &state.ui.showLspSettings); + ImGui::PushFont(state.uiFont); + ImGui::TextUnformatted("Emacs Config"); + ImGui::SetNextItemWidth(320); + InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable(), ImGuiInputTextFlags_None); + ImGui::Separator(); + if (ImGui::Button("Auto-Detect")) { + state.settings.autoDetect(); + } + ImGui::Separator(); + for (auto& cfg : state.settings.getLSPServersMutable()) { + ImGui::PushID(cfg.language.c_str()); + ImGui::Checkbox("Enabled", &cfg.enabled); + ImGui::SameLine(); + ImGui::Text("%s", cfg.language.c_str()); + ImGui::SetNextItemWidth(320); + InputTextStr("Path", &cfg.path); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Args", &cfg.argsLine)) { + state.settings.syncArgs(cfg); + } + ImGui::Separator(); + ImGui::PopID(); + } + ImGui::PopFont(); + ImGui::End(); +} + +static void renderRefactorPopup(EditorState& state) { + if (state.showRefactorPopup) { + ImGui::OpenPopup("RefactorPopup"); + } + if (ImGui::BeginPopupModal("RefactorPopup", &state.showRefactorPopup, + ImGuiWindowFlags_AlwaysAutoResize)) { + const char* title = ""; + if (state.refactorAction == 1) title = "Rename Variable"; + else if (state.refactorAction == 2) title = "Extract Function"; + else if (state.refactorAction == 3) title = "Inline Variable"; + ImGui::TextUnformatted(title); + ImGui::Separator(); + + if (state.refactorAction == 1) { + ImGui::InputText("Old Name", state.refactorNameA, sizeof(state.refactorNameA)); + ImGui::InputText("New Name", state.refactorNameB, sizeof(state.refactorNameB)); + } else if (state.refactorAction == 2) { + ImGui::InputText("New Function Name", state.refactorNameA, sizeof(state.refactorNameA)); + ImGui::TextDisabled("Extracts the first statement of the first function."); + } else if (state.refactorAction == 3) { + ImGui::InputText("Variable Name", state.refactorNameA, sizeof(state.refactorNameA)); + ImGui::TextDisabled("Removes the first matching variable declaration."); + } + + if (!state.refactorError.empty()) { + ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), + "%s", state.refactorError.c_str()); + } + + bool canRun = state.isStructured() && state.active() && !state.active()->readOnly; + if (!canRun) ImGui::BeginDisabled(); + if (ImGui::Button("Preview")) { + state.refactorError.clear(); + Module* ast = state.activeAST(); + RefactorPlan plan; + if (state.refactorAction == 1) { + plan = buildRenameVariablePlan(ast, state.refactorNameA, state.refactorNameB); + } else if (state.refactorAction == 2) { + plan = buildExtractFunctionPlan(ast, state.refactorNameA); + } else if (state.refactorAction == 3) { + plan = buildInlineVariablePlan(ast, state.refactorNameA); + } + if (!plan.success) { + state.refactorError = plan.error; + } else { + auto previewAst = cloneModule(ast); + BatchMutationAPI batch; + batch.setRoot(previewAst.get()); + auto res = batch.applySequence(plan.mutations); + if (!res.success) { + state.refactorError = res.error; + } else { + std::string beforeText = state.active()->editBuf; + std::string afterText = + generateForLanguage(previewAst.get(), state.active()->language); + state.openDiff(beforeText, afterText, true, 0, {}, plan.mutations); + state.showRefactorPopup = false; + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Apply")) { + state.refactorError.clear(); + Module* ast = state.mutationAST(); + RefactorPlan plan; + if (state.refactorAction == 1) { + plan = buildRenameVariablePlan(ast, state.refactorNameA, state.refactorNameB); + } else if (state.refactorAction == 2) { + plan = buildExtractFunctionPlan(ast, state.refactorNameA); + } else if (state.refactorAction == 3) { + plan = buildInlineVariablePlan(ast, state.refactorNameA); + } + if (!plan.success) { + state.refactorError = plan.error; + } else { + BatchMutationAPI batch; + batch.setRoot(ast); + auto res = batch.applySequence(plan.mutations); + if (!res.success) { + state.refactorError = res.error; + } else { + state.outputLog += "Refactor applied (" + + std::to_string(res.appliedCount) + " mutations).\n"; + state.applyOrchestratorToActive(); + state.showRefactorPopup = false; + } + } + } + if (!canRun) ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.showRefactorPopup = false; + } + + ImGui::EndPopup(); + } +} + +static void renderCommandPalette(EditorState& state) { + if (!state.showCommandPalette) return; + ImGui::Begin("Command Palette", &state.showCommandPalette, + ImGuiWindowFlags_AlwaysAutoResize); + ImGui::SetNextItemWidth(420.0f); + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + if (ImGui::InputText("##cmdQuery", state.commandQuery, sizeof(state.commandQuery))) { + state.commandSelected = 0; + } + + auto results = state.commandPalette.search(state.commandQuery); + int maxIndex = std::max(0, (int)results.size() - 1); + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { + state.commandSelected = std::min(state.commandSelected + 1, maxIndex); + } + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { + state.commandSelected = std::max(state.commandSelected - 1, 0); + } + bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter); + bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape); + + if (dismiss) { + state.showCommandPalette = false; + } + + for (int i = 0; i < (int)results.size(); ++i) { + const auto& cmd = results[i]; + bool selected = (i == state.commandSelected); + std::string label = cmd.label; + if (!cmd.shortcut.empty()) label += " [" + cmd.shortcut + "]"; + if (ImGui::Selectable(label.c_str(), selected)) { + state.commandSelected = i; + accept = true; + } + } + if (accept && !results.empty()) { + state.executeCommand(results[state.commandSelected].id); + state.showCommandPalette = false; + } + ImGui::End(); +} diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h new file mode 100644 index 0000000..eadeca1 --- /dev/null +++ b/editor/src/panels/EditorPanel.h @@ -0,0 +1,725 @@ +#pragma once + +#include "../EditorState.h" +#include "../EditorUtils.h" +#include "../CompletionUtils.h" + +static void renderEditorPanel(EditorState& state) { + // --------------------------------------------------------------- + // Editor (center) -- editable text area + // --------------------------------------------------------------- + ImGui::Begin("Editor"); + ImGui::PushFont(state.uiFont); + ImGui::BeginChild("##breadcrumbs", ImVec2(0, 26.0f), false, ImGuiWindowFlags_NoScrollbar); + if (!state.active()) { + ImGui::TextDisabled("(no file)"); + } else if (!state.isStructured()) { + ImGui::TextDisabled("Text mode"); + } else { + Module* ast = state.activeAST(); + int lineZero = std::max(0, state.active()->cursorLine - 1); + int colZero = std::max(0, state.active()->cursorCol - 1); + ASTNode* node = ast ? findNodeAtPosition(ast, lineZero, colZero) : nullptr; + if (!node) { + ImGui::TextDisabled("(no scope)"); + } else { + auto crumbs = buildBreadcrumbTrail(node); + for (size_t i = 0; i < crumbs.size(); ++i) { + if (i > 0) { + ImGui::SameLine(); + ImGui::TextUnformatted(">"); + ImGui::SameLine(); + } + ImGui::PushID((int)i); + const auto& item = crumbs[i]; + if (item.isRole) { + ImGui::TextDisabled("%s", item.label.c_str()); + } else if (item.node && item.node->hasSpan()) { + if (ImGui::SmallButton(item.label.c_str())) { + state.jumpTo(state.active(), item.node->spanStartLine, item.node->spanStartCol); + } + } else { + ImGui::TextUnformatted(item.label.c_str()); + } + ImGui::PopID(); + } + } + } + ImGui::EndChild(); + ImGui::PopFont(); + + ImGui::PushFont(state.monoFont); + + // Tab bar for the file + if (ImGui::BeginTabBar("EditorTabs")) { + if (state.buffers.bufferCount() == 0) { + if (ImGui::BeginTabItem("Welcome")) { + RenderWelcome(state.welcome, state, state.lastDialogPath); + ImGui::EndTabItem(); + } + } else { + auto openBuffers = state.buffers.getOpenBuffers(); + for (const auto& path : openBuffers) { + auto* buf = state.bufferStates[path].get(); + if (!buf) continue; + std::string tabLabel = path; + if (buf->modified) tabLabel += " *"; + bool open = true; + if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) { + if (!state.active() || state.active()->path != path) { + state.switchToBuffer(path); + } + // Editable text area fills available space + ImVec2 avail = ImGui::GetContentRegionAvail(); + avail.y -= 4; // small margin + + state.updateHighlights(); + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + state.updateGenerated(); + } + std::vector errorLines; + std::vector warningLines; + std::vector diagRanges; + std::vector annoMarkers; + std::vector suggestionMarkers; + std::vector conflictMarkers; + std::string activeUri = EditorState::toFileUri(buf->path); + if (state.lsp) { + auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri); + for (const auto& d : lspDiags) { + if (d.severity == 1) errorLines.push_back(d.range.start.line); + else if (d.severity == 2) warningLines.push_back(d.range.start.line); + DiagnosticRange dr; + dr.startLine = d.range.start.line; + dr.startCol = d.range.start.character; + dr.endLine = d.range.end.line; + dr.endCol = d.range.end.character; + dr.severity = d.severity; + dr.message = d.message; + diagRanges.push_back(std::move(dr)); + } + } + for (const auto& d : state.whetstoneDiagnostics) { + if (d.uri != activeUri) continue; + if (d.severity == 1) errorLines.push_back(d.line); + else if (d.severity == 2) warningLines.push_back(d.line); + DiagnosticRange dr; + dr.startLine = d.line; + dr.startCol = d.character; + dr.endLine = d.line; + dr.endCol = d.character + 1; + dr.severity = d.severity; + dr.message = d.message; + diagRanges.push_back(std::move(dr)); + } + if (state.isStructured()) { + Module* ast = state.activeAST(); + if (ast) { + collectAnnotationMarkers(ast, annoMarkers); + std::vector conflicts; + collectAnnotationConflicts(ast, conflicts); + for (const auto& c : conflicts) { + AnnotationConflictMarker m; + m.childLine = c.childLine; + m.parentLine = c.parentLine; + m.message = c.message; + m.childAnnoId = c.childAnnoId; + m.parentAnnoId = c.parentAnnoId; + m.parentStrategy = c.parentStrategy; + conflictMarkers.push_back(std::move(m)); + } + for (const auto& s : state.suggestions) { + if (s.confidence <= 0.5) continue; + ASTNode* target = findNodeById(ast, s.nodeId); + if (!target || !target->hasSpan()) continue; + SuggestionMarker sm; + sm.line = target->spanStartLine; + sm.confidence = s.confidence; + sm.reason = s.reason; + sm.annotationType = s.annotationType; + sm.strategy = s.strategy; + sm.nodeId = s.nodeId; + sm.label = "@" + s.annotationType.substr(0, s.annotationType.find("Annotation")) + + "(" + s.strategy + ")"; + suggestionMarkers.push_back(std::move(sm)); + } + } + } + + CodeEditorResult res; + if (buf->language == "org") { + renderOrgDocument(state.emacsState.orgDoc, + buf->editBuf, + state.ui.showWhitespace, + state.monoFont, + state.uiFont, + [&](const std::string& newText) { + buf->editBuf = newText; + state.onTextChanged(); + }, + [&](int, const std::string&, const std::string&) { + buf->modified = true; + }, + [&](const std::string& lang, const std::string& code) { + return state.runOrgBlock(lang, code); + }); + res.cursorByte = buf->widget.getCursor(); + } else { + CodeEditorOptions opts; + opts.showWhitespace = state.ui.showWhitespace; + opts.readOnly = buf->readOnly; + opts.mode = &buf->mode; + opts.enableFolding = true; + opts.showMinimap = state.ui.showMinimap; + opts.showAnnotations = state.ui.showAnnotations; + opts.showLineNumbers = state.ui.showLineNumbers; + if (state.ui.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1; + else if (state.ui.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2; + else opts.annotationLayout = 0; + opts.errorLines = &errorLines; + opts.warningLines = &warningLines; + opts.diagnostics = &diagRanges; + opts.annotations = &annoMarkers; + opts.suggestions = &suggestionMarkers; + opts.conflicts = &conflictMarkers; + + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + opts.syncScrollX = &buf->splitScrollX; + opts.syncScrollY = &buf->splitScrollY; + opts.scrollMaster = true; + + ImGui::BeginTable("##editorSplit", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f); + ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + res = buf->widget.render("##editor", + buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), state.monoFont); + + ImGui::TableSetColumnIndex(1); + ImGui::BeginGroup(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Generated"); + ImGui::SameLine(); + const char* targetLabels[] = {"Python", "C++", "Elisp"}; + const char* targetValues[] = {"python", "cpp", "elisp"}; + int targetIndex = 0; + for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) { + if (buf->generatedLanguage == targetValues[i]) { + targetIndex = i; + break; + } + } + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("##targetLang", &targetIndex, + targetLabels, IM_ARRAYSIZE(targetLabels))) { + buf->generatedLanguage = targetValues[targetIndex]; + buf->generatedMode.setLanguage(buf->generatedLanguage); + buf->generatedHighlightsDirty = true; + state.updateGenerated(); + } + ImVec2 genAvail = ImGui::GetContentRegionAvail(); + CodeEditorOptions genOpts; + genOpts.readOnly = true; + genOpts.showWhitespace = state.ui.showWhitespace; + genOpts.mode = &buf->generatedMode; + genOpts.showLineNumbers = state.ui.showLineNumbers; + genOpts.showCurrentLine = false; + genOpts.highlightLine = buf->generatedHighlightLine; + genOpts.syncScrollX = &buf->splitScrollX; + genOpts.syncScrollY = &buf->splitScrollY; + genOpts.scrollMaster = false; + buf->generatedWidget.render("##generated", + buf->generatedBuf, buf->generatedHighlights, genOpts, + ImVec2(0, genAvail.y), state.monoFont); + ImGui::EndGroup(); + ImGui::EndTable(); + } else { + res = buf->widget.render("##editor", + buf->editBuf, buf->highlights, opts, avail, state.monoFont); + } + } + + if (buf->language != "org") { + state.updateCursorPos(res.cursorByte); + if (res.ctrlClick && state.active()) { + state.goToDefinitionAt(res.ctrlClickLine, res.ctrlClickCol); + } + if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) { + Module* ast = state.activeAST(); + if (ast) { + int genLines = countLines(buf->generatedBuf); + int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1)); + buf->generatedHighlightLine = targetLine; + } else { + buf->generatedHighlightLine = -1; + } + } + if (res.changed) { + state.onTextChanged(); + state.completionPending = true; + state.completionLastChange = ImGui::GetTime(); + state.completionVisible = false; + state.completionSelected = 0; + if (state.lsp) state.lsp->clearCompletionItems(); + if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { + int cursor = state.active()->widget.getCursor(); + if (cursor > 0 && state.active()->editBuf[cursor - 1] == '(') { + int lineZero = std::max(0, state.active()->cursorLine - 1); + int colZero = std::max(0, state.active()->cursorCol - 1); + state.lsp->requestSignatureHelp(EditorState::toFileUri(state.active()->path), + lineZero, colZero); + } else if (cursor > 0 && state.active()->editBuf[cursor - 1] == ')') { + state.lsp->clearSignatureHelp(); + } + } + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + state.analysisPending = true; + state.analysisLastChange = ImGui::GetTime(); + } + } + } + + if (res.suggestionClicked) { + state.suggestionPopup.annotationType = res.clickedSuggestion.annotationType; + state.suggestionPopup.strategy = res.clickedSuggestion.strategy; + state.suggestionPopup.reason = res.clickedSuggestion.reason; + state.suggestionPopup.confidence = res.clickedSuggestion.confidence; + state.suggestionPopup.nodeId = res.clickedSuggestion.nodeId; + state.showSuggestionPopup = true; + ImGui::OpenPopup("SuggestionPopup"); + } + + double now = ImGui::GetTime(); + if (state.completionPending && (now - state.completionLastChange) > 0.2) { + if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { + int lineZero = std::max(0, state.active()->cursorLine - 1); + int colZero = std::max(0, state.active()->cursorCol - 1); + state.lsp->requestCompletion(EditorState::toFileUri(state.active()->path), + lineZero, colZero); + } + state.completionPending = false; + } + + if (state.symbolsPending && (now - state.symbolsLastChange) > 0.3) { + if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { + state.lsp->requestDocumentSymbols(EditorState::toFileUri(state.active()->path)); + } + state.symbolsPending = false; + } + + if (state.analysisPending && (now - state.analysisLastChange) > 0.5) { + if (state.isStructured()) { + auto result = state.pipeline.run(state.active()->editBuf, + state.active()->language, + state.active()->language); + if (result.success) { + state.whetstoneDiagnostics = + collectWhetstoneDiagnostics(result.validationDiags, + result.violations, + EditorState::toFileUri(state.active()->path)); + state.appendUnusedImportDiagnostics(state.whetstoneDiagnostics); + } else { + state.whetstoneDiagnostics.clear(); + } + Module* ast = state.activeAST(); + if (ast) { + MemoryStrategyInference inf; + state.suggestions = inf.inferAnnotations(ast); + } else { + state.suggestions.clear(); + } + } else { + state.whetstoneDiagnostics.clear(); + state.suggestions.clear(); + } + state.analysisPending = false; + } + + if (state.lsp && state.active()) { + auto items = state.lsp->getCompletionItems(); + int cursor = state.active()->widget.getCursor(); + std::string prefix = EditorState::wordPrefixAt(state.active()->editBuf, cursor); + std::string nodeId; + if (state.activeAST()) { + ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), + std::max(0, state.active()->cursorLine - 1), + std::max(0, state.active()->cursorCol - 1)); + if (scopeNode) nodeId = scopeNode->id; + } + std::vector primitives; + auto funcs = state.library.primitives.getAvailableFunctions(nodeId); + auto types = state.library.primitives.getAvailableTypes(nodeId); + auto consts = state.library.primitives.getAvailableConstants(nodeId); + primitives.insert(primitives.end(), funcs.begin(), funcs.end()); + primitives.insert(primitives.end(), types.begin(), types.end()); + primitives.insert(primitives.end(), consts.begin(), consts.end()); + + auto built = buildLibraryAwareCompletions(items, primitives, prefix); + std::vector filtered; + filtered.reserve(built.items.size()); + for (const auto& item : built.items) { + const std::string& key = item.filterText.empty() ? item.label : item.filterText; + if (prefix.empty() || key.rfind(prefix, 0) == 0) filtered.push_back(item); + } + + state.completionVisible = !filtered.empty(); + if (state.completionVisible) { + ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f, + ImGui::GetWindowPos().y + 60.0f)); + ImGui::BeginChild("##completionPopup", ImVec2(300, 150), true); + int maxIndex = std::max(0, (int)filtered.size() - 1); + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { + state.completionSelected = std::min(state.completionSelected + 1, maxIndex); + } + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { + state.completionSelected = std::max(state.completionSelected - 1, 0); + } + bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Tab); + bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape); + + if (dismiss) { + state.lsp->clearCompletionItems(); + state.completionVisible = false; + } + + auto deriveLibrary = [](const std::string& name) { + auto pos = name.find("::"); + if (pos != std::string::npos) return name.substr(0, pos); + pos = name.find('.'); + if (pos != std::string::npos) return name.substr(0, pos); + return std::string(); + }; + + for (int i = 0; i < (int)filtered.size(); ++i) { + std::string itemLabel = filtered[i].label; + if (filtered[i].kind != 0) { + itemLabel = "[" + std::to_string(filtered[i].kind) + "] " + itemLabel; + } + bool selected = (i == state.completionSelected); + bool preferred = built.preferredNames.count(filtered[i].label) > 0; + std::string libHint = deriveLibrary(filtered[i].label); + if (!preferred && !libHint.empty()) itemLabel += " (auto-import)"; + if (!preferred) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } + if (ImGui::Selectable(itemLabel.c_str(), selected)) { + state.completionSelected = i; + accept = true; + } + if (!preferred) { + ImGui::PopStyleColor(); + } + } + + if (accept && !filtered.empty()) { + const auto& item = filtered[state.completionSelected]; + bool preferred = built.preferredNames.count(item.label) > 0; + if (!preferred) { + std::string libHint = deriveLibrary(item.label); + if (!libHint.empty()) { + state.ensureImportForSymbol(libHint, item.label); + } + } + int start = EditorState::wordStartAt(state.active()->editBuf, cursor); + state.applyCompletion(state.active(), item.insertText, start, cursor); + state.lsp->clearCompletionItems(); + state.completionVisible = false; + } + ImGui::EndChild(); + } + } + + if (state.showSuggestionPopup) { + if (ImGui::BeginPopupModal("SuggestionPopup", &state.showSuggestionPopup, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("%s(%s)", state.suggestionPopup.annotationType.c_str(), + state.suggestionPopup.strategy.c_str()); + ImGui::Separator(); + ImGui::TextWrapped("%s", state.suggestionPopup.reason.c_str()); + ImGui::Text("Confidence: %.2f", state.suggestionPopup.confidence); + if (ImGui::Button("Apply")) { + Module* ast = state.mutationAST(); + if (ast) { + ASTNode* target = findNodeById(ast, state.suggestionPopup.nodeId); + if (target) { + auto* anno = createAnnotationNode(state.suggestionPopup.annotationType, + state.suggestionPopup.strategy); + if (anno) { + if (target->hasSpan()) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + target->spanEndLine, target->spanEndCol); + } + state.mutator.setRoot(ast); + auto res2 = state.mutator.insertNode(target->id, "annotations", anno); + if (!res2.error.empty()) state.outputLog += res2.error + "\n"; + state.applyOrchestratorToActive(); + } + } + } + state.showSuggestionPopup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.showSuggestionPopup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + + if (state.lsp && state.active()) { + if (ImGui::IsWindowHovered()) { + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x != state.hoverLastMouse.x || mouse.y != state.hoverLastMouse.y) { + state.hoverLastMouse = mouse; + state.hoverLastMove = ImGui::GetTime(); + state.hoverPending = true; + state.lsp->clearHover(); + } + if (state.hoverPending && (ImGui::GetTime() - state.hoverLastMove) > 0.5) { + if (state.active()->path.rfind("(untitled", 0) != 0) { + int lineZero = std::max(0, state.active()->cursorLine - 1); + int colZero = std::max(0, state.active()->cursorCol - 1); + state.lsp->requestHover(EditorState::toFileUri(state.active()->path), + lineZero, colZero); + } + state.hoverPending = false; + } + std::string hover = state.lsp->getHoverContents(); + if (!hover.empty()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(hover.c_str()); + ImGui::EndTooltip(); + } + } + + if (state.definitionPending) { + auto defs = state.lsp->getDefinitionLocations(); + if (!defs.empty()) { + state.definitionPending = false; + state.definitionPreview = defs.front(); + state.definitionPreviewValid = true; + state.lsp->clearDefinitionLocations(); + state.jumpToDefinitionLocation(state.definitionPreview); + } else if ((ImGui::GetTime() - state.definitionLastRequest) > 0.5) { + state.definitionPending = false; + state.goToDefinitionFallback(state.definitionRequestLine, + state.definitionRequestCol); + } + } + + auto sig = state.lsp->getSignatureHelp(); + if (!sig.label.empty()) { + ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f, + ImGui::GetWindowPos().y + ImGui::GetWindowHeight() - 80.0f)); + ImGui::BeginChild("##signatureHelp", ImVec2(420, 60), true); + ImGui::TextUnformatted(sig.label.c_str()); + ImGui::EndChild(); + } + } + + if (res.hoverValid && (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) { + std::string preview; + if (state.previewDefinitionAt(res.hoverLine, res.hoverCol, preview)) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(preview.c_str()); + ImGui::EndTooltip(); + } else if (state.definitionPreviewValid) { + std::string path = EditorState::fromFileUri(state.definitionPreview.uri); + std::string label = path + ":" + + std::to_string(state.definitionPreview.range.start.line + 1); + ImGui::BeginTooltip(); + ImGui::TextUnformatted(label.c_str()); + ImGui::EndTooltip(); + } + } + + if (ImGui::BeginPopupContextWindow("AnnotationContext", ImGuiPopupFlags_MouseButtonRight)) { + Module* ast = state.mutationAST(); + int lineZero = state.active() ? std::max(0, state.active()->cursorLine - 1) : 0; + ASTNode* target = ast ? findAnnotationTarget(ast, lineZero) : nullptr; + if (!target) { + ImGui::TextDisabled("No annotatable symbol on this line."); + } else { + bool mutated = false; + ImGui::Text("Target: %s", nodeDisplayName(target).c_str()); + ImGui::Separator(); + + if (ImGui::BeginMenu("Add Annotation")) { + if (ImGui::MenuItem("@Reclaim(Tracing)")) { + auto* anno = createAnnotationNode("ReclaimAnnotation", "Tracing"); + if (anno) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + 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()) mutated = true; + } + } + if (ImGui::MenuItem("@Owner(Single)")) { + auto* anno = createAnnotationNode("OwnerAnnotation", "Single"); + if (anno) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + 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()) mutated = true; + } + } + if (ImGui::MenuItem("@Deallocate(Explicit)")) { + auto* anno = createAnnotationNode("DeallocateAnnotation", "Explicit"); + if (anno) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + 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()) mutated = true; + } + } + if (ImGui::MenuItem("@Lifetime(RAII)")) { + auto* anno = createAnnotationNode("LifetimeAnnotation", "RAII"); + if (anno) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + 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()) mutated = true; + } + } + if (ImGui::MenuItem("@Allocate(Static)")) { + auto* anno = createAnnotationNode("AllocateAnnotation", "Static"); + if (anno) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + 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()) mutated = true; + } + } + ImGui::EndMenu(); + } + + const auto& annos = target->getChildren("annotations"); + if (ImGui::BeginMenu("Edit Annotation", !annos.empty())) { + for (auto* anno : annos) { + std::string label = annotationLabel(anno); + if (ImGui::BeginMenu(label.c_str())) { + if (anno->conceptType == "ReclaimAnnotation") { + 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()) 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()) 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()) 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()) 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()) mutated = true; + } + } + ImGui::EndMenu(); + } + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Remove Annotation", !annos.empty())) { + for (auto* anno : annos) { + std::string label = annotationLabel(anno); + 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()) mutated = true; + } + } + ImGui::EndMenu(); + } + + std::vector conflicts; + collectAnnotationConflicts(ast, conflicts); + bool conflictOnLine = false; + AnnotationConflict lineConflict; + for (const auto& c : conflicts) { + if (c.childLine == lineZero || c.parentLine == lineZero) { + lineConflict = c; + conflictOnLine = true; + break; + } + } + if (ImGui::BeginMenu("Resolve Conflict", conflictOnLine)) { + if (conflictOnLine) { + 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()) mutated = true; + } + if (ImGui::MenuItem("Match parent strategy")) { + state.mutator.setRoot(ast); + auto res = state.mutator.setProperty(lineConflict.childAnnoId, + "strategy", + lineConflict.parentStrategy); + if (!res.error.empty()) state.outputLog += res.error + "\n"; + if (res.error.empty()) mutated = true; + } + } + ImGui::EndMenu(); + } + if (mutated) { + state.applyOrchestratorToActive(); + } + } + ImGui::EndPopup(); + } + + ImGui::EndTabItem(); + } + if (!open) { + state.buffers.closeBuffer(path); + state.bufferStates.erase(path); + if (path.rfind("(untitled", 0) != 0) state.watcher.unwatch(path); + if (state.buffers.bufferCount() > 0) { + state.switchToBuffer(state.buffers.getOpenBuffers().front()); + } else { + state.activeBuffer = nullptr; + } + } + } + } + ImGui::EndTabBar(); + } + + ImGui::PopFont(); + ImGui::End(); +} diff --git a/editor/src/panels/ExplorerPanel.h b/editor/src/panels/ExplorerPanel.h new file mode 100644 index 0000000..5f793fb --- /dev/null +++ b/editor/src/panels/ExplorerPanel.h @@ -0,0 +1,30 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderExplorerPanel(EditorState& state) { + ImGui::Begin("Explorer"); + ImGui::PushFont(state.uiFont); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "OPEN EDITORS"); + ImGui::Separator(); + for (const auto& path : state.buffers.getOpenBuffers()) { + auto* buf = state.bufferStates[path].get(); + std::string label = path; + if (buf && buf->modified) label += " *"; + bool selected = state.active() && state.active()->path == path; + if (ImGui::Selectable(label.c_str(), selected)) { + state.switchToBuffer(path); + } + } + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "FILES"); + ImGui::Separator(); + state.refreshFileTree(); + if (state.fileTreeRoot.path.empty()) { + ImGui::TextDisabled("(no workspace)"); + } else { + RenderFileTree(state.fileTreeRoot, state); + } + ImGui::PopFont(); + ImGui::End(); +} diff --git a/editor/src/panels/MenuBarPanel.h b/editor/src/panels/MenuBarPanel.h new file mode 100644 index 0000000..06c5287 --- /dev/null +++ b/editor/src/panels/MenuBarPanel.h @@ -0,0 +1,239 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" +#include "../ThemeEngine.h" + +static void renderMenuBar(EditorState& state) { + if (!ImGui::BeginMainMenuBar()) return; + + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("New", state.keys.getBinding("file.new").toString().c_str())) + { + std::string lang = state.active() ? state.active()->language : "python"; + state.createBuffer(state.makeUntitledName(), "", lang); + } + if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str())) + { + auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go","*.org"}, state.lastDialogPath}); + if (!path.empty()) { + state.lastDialogPath = path; + state.doOpen(path); + } + } + if (ImGui::MenuItem("Open Project...")) { + auto path = FileDialog::openFile({"Open Project", {"*.whetstone"}, state.lastDialogPath}); + if (!path.empty()) { + if (!state.loadProject(path)) { + state.outputLog += "Failed to open project: " + path + "\n"; + } else { + state.lastDialogPath = path; + } + } + } + if (ImGui::MenuItem("Open Folder...")) { + auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot}); + if (!path.empty()) { + state.workspaceRoot = path; + state.fileTreeDirty = true; + state.search.projectSearch.setRoot(state.workspaceRoot); + state.lastDialogPath = path; + state.refreshBuildSystem(); + } + } + if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str())) + { + if (!state.active()) { + // no-op + } else if (state.active()->path.rfind("(untitled", 0) == 0) { + auto path = FileDialog::saveFile({"Save File", {"*.*"}, state.lastDialogPath}); + if (!path.empty()) { + state.active()->path = path; + state.lastDialogPath = path; + state.doSave(); + } + } else { + state.doSave(); + } + } + if (ImGui::MenuItem("Save Project...")) { + auto path = FileDialog::saveFile({"Save Project", {"*.whetstone"}, state.lastDialogPath}); + if (!path.empty()) { + if (!state.saveProject(path)) { + state.outputLog += "Failed to save project: " + path + "\n"; + } else { + state.lastDialogPath = path; + } + } + } + ImGui::Separator(); + if (ImGui::MenuItem("Exit")) state.exitRequested = true; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Undo", state.keys.getBinding("edit.undo").toString().c_str(), + false, state.active() ? state.active()->orchestrator.getUndoDepth() > 0 : false)) + state.doUndo(); + if (ImGui::MenuItem("Redo", state.keys.getBinding("edit.redo").toString().c_str(), + false, state.active() ? state.active()->orchestrator.getRedoDepth() > 0 : false)) + state.doRedo(); + ImGui::Separator(); + if (ImGui::MenuItem("Find/Replace", state.keys.getBinding("search.find").toString().c_str())) + state.search.showFind = !state.search.showFind; + if (ImGui::MenuItem("Find in Files", state.keys.getBinding("search.findInFiles").toString().c_str())) + state.search.showProjectSearch = !state.search.showProjectSearch; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Show Whitespace", nullptr, &state.ui.showWhitespace); + ImGui::MenuItem("Show Minimap", nullptr, &state.ui.showMinimap); + ImGui::MenuItem("Show Annotations", nullptr, &state.ui.showAnnotations); + ImGui::MenuItem("Show Outline", nullptr, &state.ui.showOutline); + ImGui::MenuItem("Terminal", state.keys.getBinding("view.toggleTerminal").toString().c_str(), + &state.build.showTerminalPanel); + ImGui::MenuItem("Dependencies", nullptr, &state.library.showDependencyPanel); + ImGui::MenuItem("Libraries", nullptr, &state.library.showLibraryBrowserPanel); + ImGui::MenuItem("Compose", nullptr, &state.library.showCompositionPanel); + ImGui::MenuItem("Emacs Packages", nullptr, &state.emacsState.showEmacsPackagesPanel); + ImGui::MenuItem("Emacs Bridge", nullptr, &state.emacsState.showEmacsBridgePanel); + ImGui::MenuItem("Settings", nullptr, &state.ui.showSettingsPanel); + ImGui::MenuItem("LSP Servers...", nullptr, &state.ui.showLspSettings); + if (state.active()) { + bool textMode = state.active()->bufferMode == BufferManager::BufferMode::Text; + if (ImGui::MenuItem("Text-Editor Mode", nullptr, textMode)) { + state.active()->bufferMode = textMode ? + BufferManager::BufferMode::Structured : + BufferManager::BufferMode::Text; + state.buffers.setBufferMode(state.active()->path, state.active()->bufferMode); + if (state.active()->bufferMode == BufferManager::BufferMode::Text) { + state.suggestions.clear(); + state.whetstoneDiagnostics.clear(); + state.analysisPending = false; + } else { + state.onTextChanged(); + } + if (state.active()->path.rfind("(untitled", 0) != 0) { + state.welcome.addRecentFile(state.active()->path, + state.active()->language, + bufferModeToString(state.active()->bufferMode)); + state.saveRecentFiles(); + } + } + } + if (ImGui::BeginMenu("Layout")) { + if (ImGui::MenuItem("VSCode", nullptr, state.ui.layoutPreset == LayoutPreset::VSCode)) + state.ui.layoutPreset = LayoutPreset::VSCode; + if (ImGui::MenuItem("Emacs", nullptr, state.ui.layoutPreset == LayoutPreset::Emacs)) + state.ui.layoutPreset = LayoutPreset::Emacs; + if (ImGui::MenuItem("JetBrains", nullptr, state.ui.layoutPreset == LayoutPreset::JetBrains)) + state.ui.layoutPreset = LayoutPreset::JetBrains; + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Navigate")) { + if (ImGui::MenuItem("Go to Line...", state.keys.getBinding("nav.goToLine").toString().c_str())) { + state.search.showGoToLine = true; + state.search.goToLineBuf[0] = '\0'; + state.search.goToLineError = false; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Build")) { + if (ImGui::MenuItem("Run", state.keys.getBinding("build.run").toString().c_str())) { + state.runActiveFile(false); + } + if (ImGui::MenuItem("Build", state.keys.getBinding("build.build").toString().c_str())) { + state.runActiveFile(true); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Language")) { + if (ImGui::MenuItem("Python", nullptr, state.active() && state.active()->language == "python")) + state.setLanguage("python"); + if (ImGui::MenuItem("C++", nullptr, state.active() && state.active()->language == "cpp")) + state.setLanguage("cpp"); + if (ImGui::MenuItem("Elisp", nullptr, state.active() && state.active()->language == "elisp")) + state.setLanguage("elisp"); + if (ImGui::MenuItem("JavaScript", nullptr, state.active() && state.active()->language == "javascript")) + state.setLanguage("javascript"); + if (ImGui::MenuItem("TypeScript", nullptr, state.active() && state.active()->language == "typescript")) + state.setLanguage("typescript"); + if (ImGui::MenuItem("Java", nullptr, state.active() && state.active()->language == "java")) + state.setLanguage("java"); + if (ImGui::MenuItem("Rust", nullptr, state.active() && state.active()->language == "rust")) + state.setLanguage("rust"); + if (ImGui::MenuItem("Go", nullptr, state.active() && state.active()->language == "go")) + state.setLanguage("go"); + if (ImGui::MenuItem("Org", nullptr, state.active() && state.active()->language == "org")) + state.setLanguage("org"); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Refactor")) { + bool canRefactor = state.isStructured() && state.active() && !state.active()->readOnly; + if (!canRefactor) ImGui::BeginDisabled(); + if (ImGui::MenuItem("Rename Variable")) { + state.refactorAction = 1; + state.showRefactorPopup = true; + state.refactorNameA[0] = '\0'; + state.refactorNameB[0] = '\0'; + ImGui::OpenPopup("RefactorPopup"); + } + if (ImGui::MenuItem("Extract Function")) { + state.refactorAction = 2; + state.showRefactorPopup = true; + state.refactorNameA[0] = '\0'; + state.refactorNameB[0] = '\0'; + ImGui::OpenPopup("RefactorPopup"); + } + if (ImGui::MenuItem("Inline Variable")) { + state.refactorAction = 3; + state.showRefactorPopup = true; + state.refactorNameA[0] = '\0'; + state.refactorNameB[0] = '\0'; + ImGui::OpenPopup("RefactorPopup"); + } + if (!canRefactor) ImGui::EndDisabled(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Keybindings")) { + for (auto p : KeybindingManager::availableProfiles()) { + if (ImGui::MenuItem(KeybindingManager::profileName(p), nullptr, + state.keys.getProfile() == p)) { + state.keys.setProfile(p); + state.registerCommands(); + } + } + ImGui::EndMenu(); + } + + const bool canProject = state.activeAST() != nullptr; + ImGui::SameLine(); + ImGui::Dummy(ImVec2(12.0f, 0.0f)); + ImGui::SameLine(); + if (!canProject) ImGui::BeginDisabled(); + if (ImGui::Button("Project to...")) { + ImGui::OpenPopup("ProjectToPopup"); + } + if (ImGui::BeginPopup("ProjectToPopup")) { + if (ImGui::MenuItem("Python")) state.projectToLanguage("python"); + if (ImGui::MenuItem("C++")) state.projectToLanguage("cpp"); + if (ImGui::MenuItem("Elisp")) state.projectToLanguage("elisp"); + ImGui::EndPopup(); + } + if (!canProject) ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(12.0f, 0.0f)); + ImGui::SameLine(); + bool canRunFile = state.active() && !state.active()->readOnly; + if (!canRunFile) ImGui::BeginDisabled(); + if (ImGui::Button("Run >")) { + state.runActiveFile(false); + } + ImGui::SameLine(); + if (ImGui::Button("Build")) { + state.runActiveFile(true); + } + if (!canRunFile) ImGui::EndDisabled(); + ImGui::EndMainMenuBar(); +} diff --git a/editor/src/panels/SearchPanels.h b/editor/src/panels/SearchPanels.h new file mode 100644 index 0000000..cf648fd --- /dev/null +++ b/editor/src/panels/SearchPanels.h @@ -0,0 +1,80 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderFindReplaceBar(EditorState& state) { + if (!state.search.showFind) return; + ImGui::Begin("Find & Replace", &state.search.showFind, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::PushFont(state.uiFont); + ImGui::SetNextItemWidth(300); + if (ImGui::InputText("Find", state.search.findBuf, sizeof(state.search.findBuf), + ImGuiInputTextFlags_EnterReturnsTrue)) { + state.doFind(); + } + ImGui::SameLine(); + if (ImGui::Button("Find Next")) state.doFind(); + + ImGui::SetNextItemWidth(300); + ImGui::InputText("Replace", state.search.replaceBuf, sizeof(state.search.replaceBuf)); + ImGui::SameLine(); + if (ImGui::Button("Replace All")) state.doReplaceAll(); + ImGui::PopFont(); + ImGui::End(); +} + +static void renderProjectSearchPanel(EditorState& state) { + if (!state.search.showProjectSearch) return; + ImGui::Begin("Search", &state.search.showProjectSearch); + ImGui::PushFont(state.uiFont); + bool doSearch = false; + ImGui::SetNextItemWidth(420); + if (ImGui::InputText("Query", state.search.searchQuery, sizeof(state.search.searchQuery), + ImGuiInputTextFlags_EnterReturnsTrue)) { + doSearch = true; + } + ImGui::SetNextItemWidth(420); + ImGui::InputText("Include (glob)", state.search.searchInclude, sizeof(state.search.searchInclude)); + ImGui::SetNextItemWidth(420); + ImGui::InputText("Exclude (glob)", state.search.searchExclude, sizeof(state.search.searchExclude)); + ImGui::Checkbox("Regex", &state.search.searchUseRegex); + ImGui::SameLine(); + if (ImGui::Button("Search")) doSearch = true; + ImGui::SameLine(); + if (ImGui::Button("Clear")) { + state.search.searchQuery[0] = '\0'; + state.search.searchResults.clear(); + } + + if (doSearch) { + state.search.searchResults = state.search.projectSearch.search( + state.search.searchQuery, + state.search.searchInclude, + state.search.searchExclude, + state.search.searchUseRegex); + } + + ImGui::Separator(); + ImGui::BeginChild("##searchResults", ImVec2(0, 0), false); + if (state.search.searchResults.empty()) { + ImGui::TextDisabled("(no results)"); + } else { + for (const auto& fileRes : state.search.searchResults) { + std::string label = fileRes.path + " (" + std::to_string(fileRes.matches.size()) + ")"; + if (ImGui::TreeNode(label.c_str())) { + for (const auto& match : fileRes.matches) { + std::string lineLabel = std::to_string(match.line + 1) + ":" + + std::to_string(match.col + 1) + " " + match.lineText; + if (ImGui::Selectable(lineLabel.c_str())) { + if (state.buffers.hasBuffer(fileRes.path)) state.switchToBuffer(fileRes.path); + else state.doOpen(fileRes.path); + state.jumpTo(state.active(), match.line, match.col); + } + } + ImGui::TreePop(); + } + } + } + ImGui::EndChild(); + ImGui::PopFont(); + ImGui::End(); +} diff --git a/editor/src/panels/SettingsPanel.h b/editor/src/panels/SettingsPanel.h new file mode 100644 index 0000000..1257483 --- /dev/null +++ b/editor/src/panels/SettingsPanel.h @@ -0,0 +1,165 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" +#include "../ThemeEngine.h" + +static void renderSettingsPanel(EditorState& state) { + if (!state.ui.showSettingsPanel) return; + ImGui::Begin("Settings", &state.ui.showSettingsPanel); + ImGui::PushFont(state.uiFont); + bool settingsChanged = false; + bool emacsConfigChanged = false; + ImGuiIO& io = ImGui::GetIO(); + + int fontSize = state.settings.getFontSize(); + if (ImGui::SliderInt("Font Size", &fontSize, 12, 24)) { + state.settings.setFontSize(fontSize); + io.FontGlobalScale = fontSize / state.baseFontSize; + settingsChanged = true; + } + + int tabSize = state.settings.getTabSize(); + const int tabSizes[] = {2, 4, 8}; + int tabIndex = 1; + for (int i = 0; i < 3; ++i) { + if (tabSize == tabSizes[i]) tabIndex = i; + } + const char* tabLabels[] = {"2", "4", "8"}; + if (ImGui::Combo("Tab Size", &tabIndex, tabLabels, 3)) { + state.settings.setTabSize(tabSizes[tabIndex]); + state.applyTabSizeToBuffers(tabSizes[tabIndex]); + settingsChanged = true; + } + + std::vector themeNames = ThemeEngine::instance().listThemes(); + if (themeNames.empty()) { + themeNames = {"VSCode Dark", "VSCode Light"}; + } + std::string currentTheme = state.settings.getTheme(); + if (currentTheme == "Dark") currentTheme = "VSCode Dark"; + if (currentTheme == "Light") currentTheme = "VSCode Light"; + int themeIndex = 0; + for (size_t i = 0; i < themeNames.size(); ++i) { + if (themeNames[i] == currentTheme) { + themeIndex = (int)i; + break; + } + } + std::vector themeLabels; + themeLabels.reserve(themeNames.size()); + for (const auto& name : themeNames) themeLabels.push_back(name.c_str()); + if (ImGui::Combo("Theme", &themeIndex, themeLabels.data(), (int)themeLabels.size())) { + state.settings.setTheme(themeNames[themeIndex]); + if (!ThemeEngine::instance().applyTheme(themeNames[themeIndex])) { + if (themeNames[themeIndex].find("Light") != std::string::npos) + SetupVSCodeLightTheme(); + else + SetupVSCodeDarkTheme(); + } + settingsChanged = true; + } + + bool telemetryOptIn = state.settings.getTelemetryOptIn(); + if (ImGui::Checkbox("Telemetry (opt-in)", &telemetryOptIn)) { + state.settings.setTelemetryOptIn(telemetryOptIn); + state.telemetry.setOptIn(telemetryOptIn); + if (telemetryOptIn) { + state.telemetry.recordEvent("telemetry_opt_in", json::object()); + } + settingsChanged = true; + } + + char updateUrlBuf[256]; + std::snprintf(updateUrlBuf, sizeof(updateUrlBuf), "%s", + state.settings.getUpdateUrl().c_str()); + if (ImGui::InputText("Update URL", updateUrlBuf, sizeof(updateUrlBuf))) { + state.settings.setUpdateUrl(updateUrlBuf); + settingsChanged = true; + } + if (ImGui::Button("Check for Updates")) { + UpdateChecker checker; + auto info = checker.check(state.settings.getUpdateUrl()); + if (info.available) { + state.outputLog += "[update] Available: " + info.version + " (" + info.url + ")\n"; + } else { + state.outputLog += "[update] No update info (offline/stub).\n"; + } + } + + int autoSave = state.settings.getAutoSaveSeconds(); + if (ImGui::InputInt("Auto-save (sec)", &autoSave)) { + autoSave = std::max(0, autoSave); + state.settings.setAutoSaveSeconds(autoSave); + settingsChanged = true; + } + + bool showMinimap = state.ui.showMinimap; + if (ImGui::Checkbox("Show Minimap", &showMinimap)) { + state.ui.showMinimap = showMinimap; + settingsChanged = true; + } + bool showLineNumbers = state.ui.showLineNumbers; + if (ImGui::Checkbox("Show Line Numbers", &showLineNumbers)) { + state.ui.showLineNumbers = showLineNumbers; + settingsChanged = true; + } + + LayoutPreset preset = state.ui.layoutPreset; + int presetIndex = 0; + if (preset == LayoutPreset::Emacs) presetIndex = 1; + else if (preset == LayoutPreset::JetBrains) presetIndex = 2; + const char* presetLabels[] = {"VSCode", "Emacs", "JetBrains"}; + if (ImGui::Combo("Layout Preset", &presetIndex, presetLabels, 3)) { + state.ui.layoutPreset = (presetIndex == 1) ? LayoutPreset::Emacs : + (presetIndex == 2) ? LayoutPreset::JetBrains : LayoutPreset::VSCode; + settingsChanged = true; + } + + int keyProfileIndex = 0; + if (state.keys.getProfile() == KeybindingProfile::JetBrains) keyProfileIndex = 1; + else if (state.keys.getProfile() == KeybindingProfile::Emacs) keyProfileIndex = 2; + const char* keyProfiles[] = {"VSCode", "JetBrains", "Emacs"}; + if (ImGui::Combo("Keybindings", &keyProfileIndex, keyProfiles, 3)) { + KeybindingProfile profile = KeybindingProfile::VSCode; + if (keyProfileIndex == 1) profile = KeybindingProfile::JetBrains; + else if (keyProfileIndex == 2) profile = KeybindingProfile::Emacs; + state.keys.setProfile(profile); + state.registerCommands(); + settingsChanged = true; + } + + ImGui::Separator(); + ImGui::TextUnformatted("Emacs Config"); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable())) { + settingsChanged = true; + emacsConfigChanged = true; + } + + if (ImGui::CollapsingHeader("LSP Servers", ImGuiTreeNodeFlags_DefaultOpen)) { + for (auto& cfg : state.settings.getLSPServersMutable()) { + ImGui::PushID(cfg.language.c_str()); + ImGui::Checkbox("Enabled", &cfg.enabled); + ImGui::SameLine(); + ImGui::Text("%s", cfg.language.c_str()); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Path", &cfg.path)) settingsChanged = true; + ImGui::SetNextItemWidth(320); + if (InputTextStr("Args", &cfg.argsLine)) { + state.settings.syncArgs(cfg); + settingsChanged = true; + } + ImGui::Separator(); + ImGui::PopID(); + } + } + + if (settingsChanged) { + state.saveSettingsToDisk(); + } + if (emacsConfigChanged) { + state.startEmacsDaemonFromSettings(); + } + ImGui::PopFont(); + ImGui::End(); +} diff --git a/editor/src/panels/SidePanels.h b/editor/src/panels/SidePanels.h new file mode 100644 index 0000000..90bb9b2 --- /dev/null +++ b/editor/src/panels/SidePanels.h @@ -0,0 +1,245 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderOutlinePanel(EditorState& state) { + if (!state.ui.showOutline) return; + ImGui::Begin("Outline", &state.ui.showOutline); + ImGui::PushFont(state.uiFont); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputText("Filter", state.outlineFilter, sizeof(state.outlineFilter)); + ImGui::Separator(); + + std::string filter = toLowerCopy(state.outlineFilter); + if (!state.active()) { + ImGui::TextDisabled("(no file)"); + } else { + std::vector lspSymbols = + state.lsp ? state.lsp->getDocumentSymbols() : std::vector{}; + + if (!lspSymbols.empty()) { + int outlineId = 0; + std::function renderLsp; + renderLsp = [&](const LSPClient::DocumentSymbol& sym) { + if (!lspSymbolMatchesFilter(sym, filter)) return; + ImGui::PushID(outlineId++); + std::string label = std::string(lspSymbolKindLabel(sym.kind)) + ": " + sym.name; + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth; + if (sym.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + bool open = ImGui::TreeNodeEx(label.c_str(), flags); + if (ImGui::IsItemClicked()) { + state.jumpTo(state.active(), sym.selectionRange.start.line, + sym.selectionRange.start.character); + } + if (!sym.children.empty() && open) { + for (const auto& child : sym.children) { + renderLsp(child); + } + ImGui::TreePop(); + } + ImGui::PopID(); + }; + for (const auto& sym : lspSymbols) renderLsp(sym); + } else if (state.isStructured()) { + std::vector outline; + collectOutlineFromAST(state.activeAST(), outline); + if (outline.empty()) { + ImGui::TextDisabled("(no symbols)"); + } else { + int outlineId = 0; + std::function renderItem; + renderItem = [&](const OutlineItem& item) { + if (!outlineMatchesFilter(item, filter)) return; + ImGui::PushID(outlineId++); + std::string label = item.kind + ": " + item.name; + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth; + if (item.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + bool open = ImGui::TreeNodeEx(label.c_str(), flags); + if (ImGui::IsItemClicked() && item.line >= 0) { + state.jumpTo(state.active(), item.line, item.col); + } + if (!item.children.empty() && open) { + for (const auto& child : item.children) { + renderItem(child); + } + ImGui::TreePop(); + } + ImGui::PopID(); + }; + for (const auto& item : outline) renderItem(item); + } + } else { + ImGui::TextDisabled("(no symbols)"); + } + } + ImGui::PopFont(); + ImGui::End(); +} + +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); + ImGui::PopFont(); + ImGui::End(); + if (state.library.dependencyPanel.needsIndex) { + state.requestLibraryIndex(); + state.library.dependencyPanel.needsIndex = false; + } +} + +static void renderLibrariesPanel(EditorState& state) { + if (!state.library.showLibraryBrowserPanel) return; + ImGui::Begin("Libraries", &state.library.showLibraryBrowserPanel); + ImGui::PushFont(state.uiFont); + std::string insertText; + std::string insertLibrary; + if (renderLibraryBrowser(state.library.libraryBrowser, + state.activeAST(), + state.library.libraryIndex, + insertText, + insertLibrary, + state.outputLog)) { + state.ensureImportForSymbol(insertLibrary, insertText); + state.insertTextAtCursor(insertText); + } + ImGui::PopFont(); + ImGui::End(); +} + +static void renderCompositionPanel(EditorState& state) { + if (!state.library.showCompositionPanel) return; + ImGui::Begin("Compose", &state.library.showCompositionPanel); + ImGui::PushFont(state.uiFont); + std::string nodeId; + if (state.activeAST()) { + ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), + std::max(0, state.active()->cursorLine - 1), + std::max(0, state.active()->cursorCol - 1)); + if (scopeNode) nodeId = scopeNode->id; + } + std::vector primitives; + 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)) { + state.insertTextAtCursor(code); + } + ImGui::PopFont(); + ImGui::End(); +} + +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)) { + state.emacsState.emacsFunctionIndexDirty = true; + } + ImGui::PopFont(); + ImGui::End(); +} + +static void renderEmacsBridgePanel(EditorState& state) { + if (!state.emacsState.showEmacsBridgePanel) return; + ImGui::Begin("Emacs Bridge", &state.emacsState.showEmacsBridgePanel); + ImGui::PushFont(state.uiFont); + if (state.active()) { + ImGui::Text("Active file: %s", state.active()->path.c_str()); + } else { + ImGui::TextDisabled("(no active buffer)"); + } + ImGui::Separator(); + if (ImGui::Button("Open Emacs Frame")) { + state.openInEmacsFrame(); + } + ImGui::SameLine(); + if (ImGui::Button("Pull From Emacs")) { + state.pullFromEmacs(); + } + ImGui::SameLine(); + if (ImGui::Button("Push To Emacs")) { + state.pushToEmacs(); + } + ImGui::PopFont(); + ImGui::End(); +} + +static void renderMinibuffer(EditorState& state) { + if (!(state.ui.layoutPreset == LayoutPreset::Emacs && state.emacsState.emacsKeys.minibufferActive)) + return; + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + 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(state.uiFont); + ImGui::TextUnformatted(state.emacsState.emacsKeys.minibufferPrompt.c_str()); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::InputText("##minibuffer", + state.emacsState.emacsKeys.minibufferBuf, + sizeof(state.emacsState.emacsKeys.minibufferBuf), + ImGuiInputTextFlags_EnterReturnsTrue)) { + emacsExecuteMinibuffer(state.emacsState.emacsKeys, state.emacsState.emacs, state.outputLog); + } + ImGui::PopFont(); + ImGui::End(); +} + +static void renderMemoryStrategiesPanel(EditorState& state) { + ImGui::Begin("Memory Strategies"); + ImGui::PushFont(state.uiFont); + Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (!ast) { + ImGui::TextDisabled("(no AST)"); + } else { + std::vector entries; + collectAnnotationEntries(ast, entries); + std::map counts; + for (const auto& e : entries) counts[e.label]++; + + ImGui::Text("Annotations"); + ImGui::Separator(); + for (const auto& [label, count] : counts) { + ImGui::Text("%s: %d", label.c_str(), count); + } + ImGui::Spacing(); + + ImGui::Text("Details"); + ImGui::Separator(); + for (const auto& e : entries) { + std::string lineInfo = e.line >= 0 ? ("L" + std::to_string(e.line + 1)) : "-"; + std::string row = e.label + " — " + e.nodeName + " (" + lineInfo + ")"; + if (ImGui::Selectable(row.c_str())) { + if (state.active()) state.jumpTo(state.active(), e.line, 0); + } + } + + ImGui::Spacing(); + ImGui::Text("Suggestions"); + ImGui::Separator(); + for (const auto& s : state.suggestions) { + if (s.confidence < 0.5) continue; + std::string row = s.annotationType + "(" + s.strategy + ") — " + + s.nodeId + " (" + std::to_string(s.confidence) + ")"; + ImGui::TextUnformatted(row.c_str()); + } + + if (ImGui::Button("Export JSON")) { + auto j = buildAnnotationSummaryJson(entries); + state.outputLog += "Annotation summary:\\n" + j.dump(2) + "\\n"; + } + } + ImGui::PopFont(); + ImGui::End(); +} diff --git a/editor/src/panels/StatusBarPanel.h b/editor/src/panels/StatusBarPanel.h new file mode 100644 index 0000000..84e0186 --- /dev/null +++ b/editor/src/panels/StatusBarPanel.h @@ -0,0 +1,75 @@ +#pragma once +#include "../EditorState.h" +#include "../EditorUtils.h" + +static void renderStatusBar(EditorState& state) { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGuiWindowFlags sbFlags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoSavedSettings; + float sbHeight = ImGui::GetFrameHeight() + 2; + ImVec2 sbPos(viewport->WorkPos.x, viewport->WorkPos.y + viewport->WorkSize.y - sbHeight); + ImVec2 sbSize(viewport->WorkSize.x, sbHeight); + ImGui::SetNextWindowPos(sbPos); + ImGui::SetNextWindowSize(sbSize); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.00f, 0.47f, 0.84f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 2)); + ImGui::Begin("##StatusBar", nullptr, sbFlags); + ImGui::PushFont(state.uiFont); + + if (state.active()) + ImGui::Text("Ln %d, Col %d", state.active()->cursorLine, state.active()->cursorCol); + else + ImGui::Text("Ln -, Col -"); + ImGui::SameLine(0, 30); + + if (state.active()) + ImGui::Text("%s", state.active()->language.c_str()); + else + ImGui::Text("-"); + ImGui::SameLine(0, 30); + + if (state.active()) { + const char* modeLabel = + state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text" : "Structured"; + ImGui::Text("Mode: %s", modeLabel); + } else { + ImGui::Text("Mode: -"); + } + ImGui::SameLine(0, 30); + + ImGui::Text("Keys: %s", KeybindingManager::profileName(state.keys.getProfile())); + ImGui::SameLine(0, 30); + if (state.ui.layoutPreset == LayoutPreset::Emacs && !state.emacsState.emacsKeys.modeLine.empty()) { + ImGui::Text("Mode: %s", state.emacsState.emacsKeys.modeLine.c_str()); + ImGui::SameLine(0, 30); + } + + ImGui::Text("Zoom: %d%%", zoomPercent(state.settings.getFontSize(), state.baseFontSize)); + ImGui::SameLine(0, 30); + + ImGui::Text("Undo: %d", state.active() ? state.active()->undoDepth : 0); + ImGui::SameLine(0, 30); + + if (state.build.runInProgress) { + ImGui::Text("Run: Running..."); + } else if (state.build.hasRunResult) { + ImGui::Text("Run: Exit %d", state.build.lastRunExitCode); + } else { + ImGui::Text("Run: -"); + } + ImGui::SameLine(0, 30); + + if (state.active() && state.active()->modified) + ImGui::Text("Modified"); + else + ImGui::Text("Saved"); + + ImGui::SameLine(0, 30); + ImGui::Text("UTF-8"); + + ImGui::PopFont(); + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); +} diff --git a/editor/src/state/AgentState.h b/editor/src/state/AgentState.h new file mode 100644 index 0000000..f7e530f --- /dev/null +++ b/editor/src/state/AgentState.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include +#include "WebSocketServer.h" +#include "WorkflowRecorder.h" +#include "AgentRegistry.h" +#include "AgentMarketplace.h" +#include "AgentPermissionPolicy.h" + +struct AgentState { + std::unique_ptr server; + MockWebSocketTransport* transport = nullptr; + int port = 8765; + std::vector log; + std::map roles; + WorkflowRecorder workflowRecorder; + AgentRegistry registry; + AgentMarketplaceState marketplace; +}; diff --git a/editor/src/state/BuildState.h b/editor/src/state/BuildState.h new file mode 100644 index 0000000..05980fe --- /dev/null +++ b/editor/src/state/BuildState.h @@ -0,0 +1,17 @@ +#pragma once +#include "TerminalPanel.h" +#include "BuildSystem.h" + +struct BuildState { + bool showTerminalPanel = false; + TerminalPanel terminal; + bool runInProgress = false; + bool hasRunResult = false; + int lastRunExitCode = 0; + std::string lastRunCommand; + + BuildSystem::Type buildType = BuildSystem::Type::None; + std::vector buildErrors; + std::string lastBuildOutput; + std::string lastBuildCommand; +}; diff --git a/editor/src/state/EmacsState.h b/editor/src/state/EmacsState.h new file mode 100644 index 0000000..bc83bab --- /dev/null +++ b/editor/src/state/EmacsState.h @@ -0,0 +1,19 @@ +#pragma once +#include "EmacsIntegration.h" +#include "EmacsPackageBrowser.h" +#include "EmacsFunctionDiscovery.h" +#include "EmacsKeybinding.h" +#include "OrgMode.h" + +struct EmacsState { + EmacsConnection emacs; + bool showEmacsPackagesPanel = false; + EmacsPackageBrowserState emacsPackages; + EmacsFunctionIndex emacsFunctionIndex; + bool emacsFunctionIndexDirty = true; + EmacsKeybindingState emacsKeys; + bool showEmacsBridgePanel = false; + bool showOrgPanel = true; + OrgDocumentState orgDoc; + int orgTempCounter = 0; +}; diff --git a/editor/src/state/LibraryState.h b/editor/src/state/LibraryState.h new file mode 100644 index 0000000..ee48b5d --- /dev/null +++ b/editor/src/state/LibraryState.h @@ -0,0 +1,27 @@ +#pragma once +#include "DependencyPanel.h" +#include "LibraryBrowserPanel.h" +#include "CompositionPanel.h" +#include "LibraryIndexer.h" +#include "PrimitivesRegistry.h" + +struct LibraryState { + bool showDependencyPanel = true; + DependencyPanelState dependencyPanel; + bool showLibraryBrowserPanel = true; + LibraryBrowserState libraryBrowser; + bool showCompositionPanel = false; + CompositionPanelState compositionPanel; + + struct LibraryIndexRequest { + std::string name; + std::string version; + std::string source; + int symbolsRequestId = -1; + int completionRequestId = -1; + }; + + std::vector libraryIndexRequests; + LibraryIndexData libraryIndex; + PrimitivesRegistry primitives; +}; diff --git a/editor/src/state/SearchState.h b/editor/src/state/SearchState.h new file mode 100644 index 0000000..0da229f --- /dev/null +++ b/editor/src/state/SearchState.h @@ -0,0 +1,22 @@ +#pragma once +#include "ProjectSearch.h" + +struct SearchState { + bool showFind = false; + char findBuf[256] = {}; + char replaceBuf[256] = {}; + int lastFindPos = 0; + + bool showProjectSearch = false; + char searchQuery[256] = {}; + char searchInclude[256] = {}; + char searchExclude[256] = {}; + bool searchUseRegex = true; + std::vector searchResults; + + bool showGoToLine = false; + char goToLineBuf[64] = {}; + bool goToLineError = false; + + ProjectSearch projectSearch; +}; diff --git a/editor/src/state/UIFlags.h b/editor/src/state/UIFlags.h new file mode 100644 index 0000000..2cbd8a5 --- /dev/null +++ b/editor/src/state/UIFlags.h @@ -0,0 +1,14 @@ +#pragma once +#include "LayoutManager.h" + +struct UIFlags { + bool showWhitespace = false; + bool showMinimap = false; + bool showAnnotations = false; + bool showOutline = true; + bool showLineNumbers = true; + bool showLspSettings = false; + bool showSettingsPanel = false; + int bottomTab = 0; // 0=Output,1=AST,2=Highlighted + LayoutPreset layoutPreset = LayoutPreset::VSCode; +}; diff --git a/editor/tests/step166_test.cpp b/editor/tests/step166_test.cpp new file mode 100644 index 0000000..1fd6a72 --- /dev/null +++ b/editor/tests/step166_test.cpp @@ -0,0 +1,87 @@ +// Step 166: Extract main.cpp into panel headers +// Verify: main.cpp size reduced, panel headers exist and compile, +// EditorState has rendering context fields, StringUtils shared. + +#include +#include +#include +#include +#include + +// Verify panel headers compile +#include "StringUtils.h" +#include "GoToLine.h" +#include "EmacsPackageBrowser.h" + +namespace fs = std::filesystem; + +static int countFileLines(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) return -1; + int n = 0; + std::string line; + while (std::getline(f, line)) ++n; + return n; +} + +int main() { + // 1. main.cpp should be under 400 lines (was 2736, now ~330) + int mainLines = countFileLines("src/main.cpp"); + assert(mainLines > 0 && "main.cpp must exist"); + assert(mainLines < 400 && "main.cpp must be under 400 lines after extraction"); + + // 2. All 9 panel headers must exist + const char* panelHeaders[] = { + "src/panels/MenuBarPanel.h", + "src/panels/ExplorerPanel.h", + "src/panels/SidePanels.h", + "src/panels/SearchPanels.h", + "src/panels/SettingsPanel.h", + "src/panels/DialogPanels.h", + "src/panels/EditorPanel.h", + "src/panels/BottomPanel.h", + "src/panels/StatusBarPanel.h", + }; + for (const char* p : panelHeaders) { + assert(fs::exists(p) && "Panel header must exist"); + int lines = countFileLines(p); + assert(lines > 5 && "Panel header must have content"); + } + + // 3. StringUtils.h provides trimCopy (compile-time check + runtime) + assert(trimCopy(" hello ") == "hello"); + assert(trimCopy("") == ""); + assert(trimCopy(" ") == ""); + assert(trimCopy("no-trim") == "no-trim"); + + // 4. GoToLine.h still works (uses trimCopy from StringUtils.h) + int line = 0, col = 0; + assert(parseLineColInput("42", line, col)); + assert(line == 42 && col == 1); + assert(parseLineColInput("10:5", line, col)); + assert(line == 10 && col == 5); + assert(parseLineColInput(" :7:3 ", line, col)); + assert(line == 7 && col == 3); + + // 5. EditorState rendering context fields exist (compile-time check) + // Verified by panels compiling and referencing state.monoFont, state.uiFont, etc. + + // 6. main.cpp includes all panel headers + { + std::ifstream f("src/main.cpp"); + assert(f.is_open()); + std::string content((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + assert(content.find("#include \"panels/MenuBarPanel.h\"") != std::string::npos); + assert(content.find("#include \"panels/EditorPanel.h\"") != std::string::npos); + assert(content.find("#include \"panels/BottomPanel.h\"") != std::string::npos); + assert(content.find("#include \"panels/StatusBarPanel.h\"") != std::string::npos); + assert(content.find("#include \"panels/DialogPanels.h\"") != std::string::npos); + // main.cpp should NOT contain renderMenuBar definition + assert(content.find("static void renderMenuBar") == std::string::npos); + assert(content.find("static void renderEditorPanel") == std::string::npos); + } + + printf("step166_test: all assertions passed\n"); + return 0; +} diff --git a/editor/tests/step167_test.cpp b/editor/tests/step167_test.cpp new file mode 100644 index 0000000..541a24c --- /dev/null +++ b/editor/tests/step167_test.cpp @@ -0,0 +1,83 @@ +// Step 167: Split EditorState into sub-state structs. +// Verify: state headers exist, EditorState uses sub-states, panels reference new fields. + +#include +#include +#include +#include + +#include "state/SearchState.h" +#include "state/AgentState.h" +#include "state/BuildState.h" +#include "state/LibraryState.h" +#include "state/EmacsState.h" +#include "state/UIFlags.h" + +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(f)), + std::istreambuf_iterator()); +} + +static void assertContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) != std::string::npos); +} + +static void assertNotContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) == std::string::npos); +} + +int main() { + // 1) State headers exist. + const char* stateHeaders[] = { + "src/state/SearchState.h", + "src/state/AgentState.h", + "src/state/BuildState.h", + "src/state/LibraryState.h", + "src/state/EmacsState.h", + "src/state/UIFlags.h", + }; + for (const char* p : stateHeaders) { + assert(fs::exists(p) && "State header must exist"); + } + + // 2) EditorState uses sub-state fields. + SearchState search; + AgentState agent; + BuildState build; + LibraryState library; + EmacsState emacs; + UIFlags ui; + search.showFind = true; + agent.port = 1234; + build.showTerminalPanel = true; + library.showLibraryBrowserPanel = true; + emacs.showEmacsPackagesPanel = true; + ui.showMinimap = true; + + // 3) EditorState header reflects new sub-state fields. + const std::string editorState = readFile("src/EditorState.h"); + assertContains(editorState, "SearchState"); + assertContains(editorState, "AgentState"); + assertContains(editorState, "BuildState"); + assertContains(editorState, "LibraryState"); + assertContains(editorState, "EmacsState"); + assertContains(editorState, "UIFlags"); + assertNotContains(editorState, "ProjectSearch projectSearch"); + + // 4) Panels reference sub-states (spot check). + const std::string searchPanels = readFile("src/panels/SearchPanels.h"); + assertContains(searchPanels, "state.search"); + const std::string sidePanels = readFile("src/panels/SidePanels.h"); + assertContains(sidePanels, "state.library"); + assertContains(sidePanels, "state.emacsState"); + const std::string statusBar = readFile("src/panels/StatusBarPanel.h"); + assertContains(statusBar, "state.build"); + assertContains(statusBar, "state.ui"); + + printf("step167_test: all assertions passed\n"); + return 0; +} diff --git a/sprint6_plan.md b/sprint6_plan.md new file mode 100644 index 0000000..3e0c769 --- /dev/null +++ b/sprint6_plan.md @@ -0,0 +1,525 @@ +# Sprint 6: UX & Editor Polish — Plan + +> **Goal:** Transform Whetstone from a feature-complete prototype into a +> polished, discoverable, accessible editor that users actually enjoy using. +> Address accumulated structural debt, add visual refinement, improve +> onboarding, and integrate the security/semantic feature requests. +> +> **Prerequisites:** Sprint 5 Phase 5a–5b complete (library-aware coding flow). +> Sprint 5 Phases 5c–5f may still be in progress — Sprint 6 work is +> independent and can proceed in parallel. +> +> **Key themes:** +> 1. Structural refactor — enforce architecture limits, enable faster UI iteration +> 2. Theme engine and visual design system +> 3. Editor UX enhancements (multi-cursor, find/replace, diagnostics) +> 4. Onboarding, discoverability, and documentation +> 5. Security vulnerability awareness (from feature requests) +> 6. Accessibility and performance + +--- + +## Phase 6a: Structural Refactor (Steps 166–170) + +The foundation: main.cpp is 2600+ lines and EditorState.h is 1800+ lines, +both far over the architecture limits (1500 and 600 respectively). Fix this +first — all UX work becomes easier when panels are modular. + +- [ ] **Step 166: Extract main.cpp into panel headers** + Create `editor/src/panels/` directory. Extract each major UI section from + main.cpp's render loop into its own header with a `renderXxx(EditorState&)` + free function: + - `panels/MenuBarPanel.h` — menu bar and toolbar + - `panels/ExplorerPanel.h` — file tree and workspace browser + - `panels/EditorPanel.h` — code editor area, tabs, split view + - `panels/BottomPanel.h` — output, AST, terminal, problems, agents tabs + - `panels/StatusBarPanel.h` — status bar with mode/language/position + - `panels/SidePanels.h` — outline, dependencies, library browser, etc. + main.cpp reduced to: init, event loop, docking layout, panel dispatch. + Target: main.cpp under 800 lines. + *New:* `panels/` directory with 6+ headers. *Modifies:* `main.cpp` + +- [x] **Step 167: Split EditorState into focused sub-states** + Break the 1800-line EditorState.h into composable sub-structs: + - `SearchState` — find/replace, project search, go-to-line fields + - `AgentState` — WebSocket server, agent log, permissions + - `BuildState` — build system, errors, run state + - `LibraryState` — dependency panel, library browser, composition panel + - `EmacsState` — packages, function index, keybinding state, org doc + - `UIFlags` — panel visibility toggles, bottom tab selection + EditorState composes these via named members. Target: EditorState.h under + 400 lines, each sub-state under 200 lines. + *Modifies:* `EditorState.h`. *New:* sub-state headers or inline structs. + +- [ ] **Step 168: Split oversized component headers** + Enforce the 600-line limit on remaining violators: + - `CodeEditorWidget.h` (1112 lines): extract rendering helpers into + `CodeEditorRendering.h`, selection logic into `CodeEditorSelection.h` + - `ast/Parser.h` (1093 lines): extract per-language CST-to-AST into + `ast/PythonParser.h`, `ast/CppParser.h`, `ast/ElispParser.h` etc. + - `SyntaxHighlighter.h` (762 lines): extract language keyword tables + into `SyntaxLanguages.h` + - `ast/CppGenerator.h` (708 lines): extract helper functions + All files under 600 lines after split. + *Modifies:* 5+ oversized headers + +- [ ] **Step 169: Notification / toast system** + Replace ad-hoc `outputLog +=` with a structured notification system. + `NotificationSystem.h`: + - Notification types: Success, Warning, Error, Info + - Toast popups in bottom-right corner, auto-dismiss after 4s (configurable) + - Notification history accessible from status bar icon + - Click toast to navigate to source (e.g., click error toast → jump to line) + - Panels use `notify(level, message)` instead of appending to outputLog + *New:* `NotificationSystem.h`. *Modifies:* panels that use outputLog + +- [ ] **Step 170: UI event bus for decoupled updates** + Create `UIEventBus.h` — lightweight pub/sub for UI events: + - Events: ASTChanged, BufferSwitched, DiagnosticsUpdated, ThemeChanged, + SettingsChanged, FileModified, AgentConnected, NotificationPosted + - Panels subscribe to events they care about + - Replaces polling patterns and tightly-coupled re-render triggers + - Debounce support for high-frequency events (typing, diagnostics) + *New:* `UIEventBus.h`. *Modifies:* panels to subscribe instead of poll + +--- + +## Phase 6b: Theme Engine & Visual Design (Steps 171–176) + +Full visual customization. Ship with popular themes. Make the editor +look professional and feel personal. + +- [ ] **Step 171: Theme engine core** + `ThemeEngine.h` with theme data model: + - Color categories: editor (bg, fg, cursor, selection, currentLine), + syntax (keyword, string, comment, number, type, function, operator), + gutter (bg, lineNumber, marker colors), panels (bg, border, header), + annotations (per-strategy colors), statusBar, scrollbar, tooltip + - Load from JSON files in `~/.whetstone/themes/` + - Bundled themes embedded as defaults + - Hot-reload on file change + - `ThemeEngine::getColor(ThemeColor::Keyword)` API for all panels + *New:* `ThemeEngine.h` + +- [ ] **Step 172: Bundled theme pack** + Ship 7 themes, each a JSON file with full color definitions: + - **Whetstone Dark** (default) — custom dark theme with annotation-aware + accent colors + - **Whetstone Light** — clean light theme + - **Monokai Pro** — warm dark with vivid syntax colors + - **Solarized Dark** / **Solarized Light** + - **Dracula** + - **Nord** + Each theme tested with all annotation types visible. + *New:* 7 theme JSON files + +- [ ] **Step 173: Theme gallery UI** + Settings > Themes section with visual browser: + - Grid of theme cards showing name + color swatch preview + - Live preview on hover (entire editor updates temporarily) + - "Apply" confirms, "Reset" reverts to previous + - "Open Themes Folder" button for custom theme creation + - Active theme highlighted with checkmark + *Modifies:* Settings panel, `ThemeEngine.h` + +- [ ] **Step 174: Icon system** + `IconSet.h` — consistent icons rendered via ImGui drawing primitives: + - File type icons (Python, C++, JS, Rust, Go, Java, Elisp, JSON, etc.) + - Panel icons (explorer, outline, terminal, agents, dependencies, etc.) + - Diagnostic icons (error circle, warning triangle, info, hint lightbulb) + - Annotation strategy icons (distinct shapes, not just colors) + - Icons scale with zoom level + - Theme-aware: icon colors adapt to current theme + *New:* `IconSet.h` + +- [ ] **Step 175: Typography and spacing** + Configurable typography beyond just font size: + - Code font family selection (from system monospace fonts or bundled) + - UI font separate from code font + - Line height multiplier (1.0–2.0) + - Letter spacing adjustment + - Consistent panel padding/margins defined in theme + - Settings UI for all typography options + *Modifies:* `SettingsManager.h`, `CodeEditorWidget.h`, `ThemeEngine.h` + +- [ ] **Step 176: Smooth UI transitions** + Subtle animations that make the editor feel polished: + - Panel open/close: smooth slide (not instant pop) + - Tab switch: brief cross-fade on editor content + - Tooltip: fade in after delay, fade out on leave + - Notification toasts: slide in from right, fade out on dismiss + - Search match highlight: brief pulse on "next result" navigation + - Cursor blink: configurable rate, smooth fade (not hard on/off) + All animations respect a "Reduce Motion" accessibility setting. + *New:* `AnimationUtils.h`. *Modifies:* panels using transitions + +--- + +## Phase 6c: Editor UX Enhancements (Steps 177–183) + +Core editing improvements that make daily use smooth. + +- [ ] **Step 177: Enhanced find/replace** + Upgrade the find/replace bar: + - Match count display ("3 of 17 results") + - Live regex preview with captured groups highlighted + - Replace preview: show inline what the replacement will look like + - Find-in-selection mode + - Toggle buttons with clear visual state: Match Case [Aa], Whole Word [W], + Regex [.*] + - Ctrl+H opens replace (Ctrl+F opens find-only) + - Preserve search history (last 10 queries, accessible via dropdown) + *Modifies:* `CodeEditorWidget.h`, find/replace rendering + +- [ ] **Step 178: Multi-cursor editing** + Support for multiple simultaneous cursors: + - Ctrl+D: select next occurrence of current selection (add cursor) + - Alt+Click: add cursor at click position + - Ctrl+Alt+Up/Down: add cursor above/below + - Alt+Shift+Drag: column/rectangular selection + - All cursors type, delete, and navigate simultaneously + - Escape to collapse to single cursor + - Works with auto-indent and bracket completion + *Modifies:* `CodeEditorWidget.h` + +- [ ] **Step 179: Rainbow brackets and delimiter intelligence** + Smart bracket handling: + - Rainbow bracket coloring: nested brackets get rotating colors + (configurable, theme-aware, toggleable) + - Bracket pair highlight: matching bracket highlighted on cursor adjacency + - Bracket scope highlight: subtle background tint for current block + - Auto-surround: select text then type `(` → wraps selection in parens + - Configurable auto-close per language (e.g., disable for Lisps) + *Modifies:* `CodeEditorWidget.h`, `EditorMode.h` + +- [ ] **Step 180: Rich tooltip system** + Replace basic ImGui tooltips with rich formatted tooltips: + - Markdown rendering in tooltips (bold, code, lists) + - Multi-section tooltips (e.g., type info + docs + source link) + - "Pin" button to keep tooltip open while reading + - Click-through links in tooltips (navigate to definition) + - Configurable hover delay (default 500ms) + - Max width/height with scrolling for long content + *New:* `RichTooltip.h`. *Modifies:* hover handlers in editor/panels + +- [ ] **Step 181: Enhanced status bar** + Context-rich status bar showing everything relevant at a glance: + - Left: mode indicator (Text/Structured), language selector (clickable), + encoding (UTF-8, clickable to change), line ending (LF/CRLF, clickable) + - Center: notification area (last toast message, click for history) + - Right: Ln/Col, selection count (chars/lines), zoom %, git branch/status + - Background color subtly adapts to state (normal, error, running) + - Each segment is clickable for relevant actions + *Modifies:* `panels/StatusBarPanel.h` (extracted in Step 166) + +- [ ] **Step 182: Improved diagnostics experience** + Make errors and warnings easier to understand and act on: + - Problems panel with sortable columns (Severity, Source, File, Message) + - Group by file with collapsible sections + - Quick-fix actions inline ("Apply suggestion" button on fixable diagnostics) + - "Fix All" button for batch-applicable fixes of same type + - Peek view: Ctrl+Shift+M shows diagnostic inline without leaving context + - Diagnostic count badges on file tabs (red dot = errors, yellow = warnings) + *Modifies:* diagnostic rendering, `panels/BottomPanel.h` + +- [ ] **Step 183: Tab and panel drag-and-drop** + Fluid drag interactions for workspace customization: + - Drag tabs to reorder within the tab bar + - Drag tab to split view (left/right/top/bottom drop zones) + - Drag files from explorer to specific tab position + - Drag panel headers to rearrange docked layout + - Visual feedback during drag: ghost preview, drop zone highlights + - Double-click tab → rename (for untitled files) + *Modifies:* tab rendering, `LayoutManager.h` + +--- + +## Phase 6d: Onboarding & Discoverability (Steps 184–189) + +Help users discover what Whetstone can do. Reduce the learning curve +without being annoying. + +- [ ] **Step 184: First-run experience** + Interactive setup wizard on first launch (no session.json found): + 1. "Welcome to Whetstone" splash with logo + 2. Choose layout preset (VSCode/Emacs/JetBrains) with visual previews + 3. Choose theme (grid of 4 most popular themes) + 4. Choose keybinding profile with common shortcut preview + 5. "Open Example Project" or "Open Folder" or "New File" + Settings persisted immediately. Can be re-run from Help > Setup Wizard. + *New:* `FirstRunWizard.h` + +- [ ] **Step 185: Contextual feature hints** + Non-intrusive discovery system: + - Hints appear as subtle bar below toolbar, not modal dialogs + - Triggered by context: first time opening annotations panel → + "Tip: Right-click a function to add memory annotations" + - First time connecting an agent → "Tip: Agents can query and modify + your AST via the JSON-RPC API" + - Max 1 hint per session, max 3 per day + - "Got it" dismisses, "Don't show tips" disables permanently + - Hint state tracked in `~/.whetstone/hints.json` + *New:* `FeatureHints.h` + +- [ ] **Step 186: Keyboard shortcut reference** + Dedicated shortcut panel (Ctrl+K Ctrl+S to open): + - Full list grouped by category (File, Edit, View, Navigate, Annotate, + Agent, Debug) + - Search/filter box + - Shows current keybinding profile's mappings + - "Record Keys" mode: press any key combo → shows what it does + - Highlights customized vs default bindings + - Export to printable HTML/PDF + *New:* `ShortcutReference.h` + +- [ ] **Step 187: In-editor help system** + Help > Documentation opens a dockable panel: + - Searchable documentation browser + - Sections: Getting Started, Annotation Reference, Language Support, + Agent API Quick Reference, Keyboard Shortcuts + - Annotation Reference: every annotation type with code examples, + valid strategies, language-specific behavior + - Content authored in Markdown, rendered in ImGui with basic formatting + (headings, code blocks, lists, bold/italic) + - "Open in Browser" button for full online docs + *New:* `HelpPanel.h`, `MarkdownRenderer.h` + +- [ ] **Step 188: Enhanced command palette** + Upgrade the existing command palette: + - Commands grouped by category with section headers + - "Recently Used" section pinned at top (last 5 commands) + - Context-aware: shows different commands based on active panel + - Inline parameter input: "Go to Line" → type number inline + - ">" prefix for commands, no prefix for file search (like VSCode) + - Fuzzy matching highlights matched characters + *Modifies:* `CommandPalette.h` + +- [ ] **Step 189: Guided workflow wizards** + Multi-step wizards for complex operations: + - **"Annotate File"** wizard: iterates through unannotated functions, + shows inference suggestions, user confirms/skips each. Summary at end. + - **"Cross-Language Project"** wizard: select source language → target → + preview annotation adaptation → confirm → new tab with projected code + - **"Connect Agent"** wizard: endpoint URL → test connection → set + permissions → show API capabilities + - Each wizard shows step progress (Step 2 of 4) and allows Back/Skip + *New:* `WizardFramework.h`, wizard implementations + +--- + +## Phase 6e: Security & Library UX (Steps 190–195) + +Integrate the security vulnerability awareness and semantic annotation +feature requests with polished UX. + +- [ ] **Step 190: Vulnerability knowledge base** + `VulnerabilityDatabase.h`: + - Data model: `VulnerabilityRecord` (ecosystem, package, affected_versions, + severity, CVE ID, summary, references, fix_versions) + - Parse OSV (Open Source Vulnerabilities) JSON format + - Local cache in `~/.whetstone/vuln_cache/` with TTL (default 24h) + - Offline mode: use cached data when no network + - Lookup: `query(ecosystem, package, version) → vector` + - Background refresh on editor startup (non-blocking) + *New:* `VulnerabilityDatabase.h` + +- [ ] **Step 191: Dependency security badges** + Enhance the Dependencies panel with security awareness: + - Warning badge (shield icon) on packages with known vulnerabilities + - Badge color by max severity: red (Critical/High), orange (Medium), + gray (Low) + - Click badge → expandable advisory details (CVE, severity, summary, + affected versions, fixed version) + - "Upgrade to Safe Version" button (writes to dependency file) + - "Ignore" option with reason (persisted to `.whetstone/vuln_ignore.json`) + *Modifies:* `DependencyPanel.h`, `VulnerabilityDatabase.h` + +- [ ] **Step 192: Security diagnostics integration** + Security findings appear alongside other diagnostics: + - Problems panel: `[Security]` tagged diagnostics with severity + - Gutter: shield icon on `import` lines of vulnerable packages + - Tooltip on gutter icon shows vulnerability summary + - Agent hints: when `preferImports` is active, deprioritize vulnerable + package symbols in completion ranking + - Configurable: "Block vulnerable imports" option (default off, warns only) + *Modifies:* diagnostic rendering, `PrimitivesRegistry.h` + +- [ ] **Step 193: Semantic annotation tags for libraries** + `SemanticTags.h`: + - Predefined tag vocabulary: `@serialize`, `@crypto`, `@io`, `@network`, + `@math`, `@collection`, `@concurrency`, `@test`, `@ui`, `@parse` + - Tag storage: `~/.whetstone/semantic_tags.json` mapping + `{ library: { symbol: [tags...] } }` + - Auto-tag common libraries (numpy→@math, requests→@network, + cryptography→@crypto, json→@serialize, etc.) + - Tag inference from function names and docstrings (heuristic) + - Tags attached to `ExternalModule` / `TypeSignature` AST nodes + *New:* `SemanticTags.h` + +- [ ] **Step 194: Semantic-filtered library browser** + Enhance Library Browser with tag-based discovery: + - Tag filter bar above symbol list: click tags to filter + - Active tags shown as chips, click X to remove + - Symbol list shows tag badges next to each item + - "Show @crypto functions" → filters across all imported libraries + - Completion ranking boosted when tag matches user's context + (editing a serialize function → @serialize symbols rank higher) + - Agent prompts include semantic tag context for better suggestions + *Modifies:* `LibraryBrowserPanel.h`, completion system + +- [ ] **Step 195: Security & semantic UX tests** + End-to-end tests: + 1. Add vulnerable package → badge appears → advisory details correct + 2. Security diagnostic appears in Problems panel with correct severity + 3. Semantic tag auto-assignment for numpy → @math + 4. Library browser tag filter reduces visible symbols correctly + 5. Agent completion deprioritizes vulnerable package symbols + 6. "Upgrade to Safe Version" writes correct version to dependency file + *New:* `step195_test.cpp` + +--- + +## Phase 6f: Accessibility & Performance (Steps 196–201) + +Make Whetstone usable by everyone and fast with large files. + +- [ ] **Step 196: High contrast and colorblind modes** + Accessibility-first visual options: + - "High Contrast" theme (WCAG AAA contrast ratios, bold borders) + - Colorblind-safe annotation markers: use **shapes** (circle, triangle, + square, diamond, star) in addition to colors + - Diagnostic markers use patterns (solid, striped, dotted) not just color + - Configurable: "Use shapes for annotations" toggle (default on) + - Test: all information conveyed by color also conveyed by shape/pattern + *New:* High contrast theme. *Modifies:* annotation/diagnostic rendering + +- [ ] **Step 197: Keyboard navigation completeness audit** + Ensure every feature is keyboard-accessible: + - All panels navigable with Tab/Shift+Tab + - Focus ring indicator on active element (visible in all themes) + - Escape consistently returns focus to editor from any panel + - All context menu actions have keyboard alternatives + - F6 cycles between major panels (editor, explorer, bottom, side) + - All dialog buttons reachable via Tab, Enter to confirm, Escape to cancel + - Audit checklist: test every panel with mouse unplugged + *Modifies:* all panels for focus handling + +- [ ] **Step 198: Virtual scrolling for large files** + Performance optimization for files with 10k+ lines: + - Only render visible lines + small buffer (±50 lines) + - Virtual scroll: total scroll range = total lines, rendered = viewport + - Syntax highlighting computed lazily (visible range + prefetch ahead) + - Line number gutter only draws visible range + - Smooth scroll maintained at all file sizes + - Benchmark: 50k line file opens in <500ms, scrolls at 60fps + *Modifies:* `CodeEditorWidget.h` + +- [ ] **Step 199: Large file handling** + Graceful degradation for very large files: + - Files >1MB: show size warning, offer "Open in Text Mode" + - Files >5MB: auto-open in Text Mode (no AST sync, no annotations) + - Files >10MB: disable syntax highlighting, show "Large File Mode" badge + - Memory usage indicator in status bar + - Configurable thresholds in Settings + - "Reopen in Structured Mode" option if user wants full features + *Modifies:* buffer opening logic, `panels/StatusBarPanel.h` + +- [ ] **Step 200: Startup and panel performance** + Faster startup and smoother runtime: + - Lazy-load panels: only parse/render on first show + - Background grammar loading (don't block editor open for tree-sitter init) + - Session restore: open tabs immediately with cached text, parse AST + asynchronously + - Debounce settings: configurable delays for LSP requests, diagnostics, + syntax highlighting refresh + - Frame time budget monitor in debug builds (warn if frame >16ms) + *Modifies:* initialization code, panel rendering + +- [ ] **Step 201: Sprint 6 integration tests** + Comprehensive tests covering the full Sprint 6 feature set: + 1. Panel extraction: main.cpp compiles and all panels render + 2. Theme switch: all annotation colors, syntax colors, panel colors update + 3. Multi-cursor: 3 cursors typing simultaneously produce correct text + 4. First-run wizard: all settings persisted correctly + 5. Vulnerability badge: shows on import of vulnerable package + 6. Keyboard-only: complete a full workflow without mouse + 7. Large file (50k lines): opens, scrolls, searches within time budget + 8. Notification toast: appears, auto-dismisses, appears in history + *New:* `step201_test.cpp` + +--- + +## Summary + +| Phase | Steps | Description | +|-------|-------|-------------| +| 6a | 166–170 | Structural refactor (panel extraction, state split, notifications, event bus) | +| 6b | 171–176 | Theme engine & visual design (themes, icons, typography, animations) | +| 6c | 177–183 | Editor UX enhancements (find/replace, multi-cursor, brackets, tooltips, diagnostics) | +| 6d | 184–189 | Onboarding & discoverability (first-run, hints, shortcuts, help, wizards) | +| 6e | 190–195 | Security & library UX (vulnerability DB, security badges, semantic tags) | +| 6f | 196–201 | Accessibility & performance (high contrast, keyboard nav, virtual scroll, large files) | + +**Total: 36 steps across 6 phases.** + +--- + +## Context for Agents + +### What exists after Sprint 5 (prerequisites): +- 65+ header files in editor/src/ with 20 AST headers +- Library-aware constructive coding flow (PrimitivesRegistry, library completion) +- 8+ language grammars with syntax highlighting +- LSP client, WebSocket agent server, cross-language projection +- Dependencies panel, Library Browser, Composition Builder +- Emacs ecosystem integration (packages, functions, keybindings) +- Full test coverage (step-per-test pattern, real assertions) + +### Architecture principles (reinforced in Sprint 6): +1. **600-line header limit** — split files that exceed this +2. **1500-line main.cpp limit** — extract panels to `panels/` directory +3. **50-field struct limit** — compose sub-structs for large state +4. **Panel extraction pattern** — `void renderXxx(EditorState&)` free functions +5. **Event-driven updates** — use UIEventBus, not polling +6. **Notification system** — use `notify()`, not `outputLog +=` +7. **Theme-aware rendering** — use `ThemeEngine::getColor()`, not hardcoded colors + +### Key new files introduced in Sprint 6: +| File | What it does | +|------|-------------| +| `panels/*.h` | Extracted panel rendering functions | +| `ThemeEngine.h` | Theme loading, color lookup, hot-reload | +| `IconSet.h` | Consistent file/panel/diagnostic icons | +| `NotificationSystem.h` | Toast notifications with history | +| `UIEventBus.h` | Pub/sub for decoupled panel updates | +| `RichTooltip.h` | Markdown-capable hover tooltips | +| `VulnerabilityDatabase.h` | CVE/OSV lookup and caching | +| `SemanticTags.h` | Library symbol semantic annotations | +| `FirstRunWizard.h` | First-launch setup experience | +| `FeatureHints.h` | Contextual tip system | +| `HelpPanel.h` | In-editor documentation browser | +| `MarkdownRenderer.h` | Markdown rendering in ImGui | +| `WizardFramework.h` | Multi-step wizard UI component | +| `ShortcutReference.h` | Keyboard shortcut browser panel | +| `AnimationUtils.h` | Smooth transitions and easing functions | + +### Build notes: +- No new external dependencies required for Phase 6a–6d +- Phase 6e (vulnerability DB): may add a JSON HTTP client for OSV API + (or reuse existing HTTP if available), otherwise offline-first with + bundled snapshot +- All themes are JSON data files, no code changes needed to add more + +--- + +## Sprint 7 Preview (for planning) + +**Sprint 7: MCP Bridge & Synthetic Training Data** + +Planned themes (to be fully specified after Sprint 6): +1. Document the JSON-RPC agent API with schemas and examples +2. MCP server wrapper exposing editor operations as MCP tools/resources +3. Synthetic interaction trace generator for fine-tuning tool-use models +4. Agent API evaluation harness (test suites for LLM tool use accuracy) +5. Claude/Codex-specific tool definitions and prompts +6. Training data pipeline: record real sessions → anonymize → export