#pragma once // --- ImGui (needed for ImVec2, ImGui::GetTime) --- #include "imgui.h" // --- Editor components --- #include "TextEditor.h" #include "TextASTSync.h" #include "SyntaxHighlighter.h" #include "KeybindingManager.h" #include "BufferManager.h" #include "CodeEditorWidget.h" #include "EditorMode.h" #include "FileDialog.h" #include "FileTree.h" #include "WelcomeScreen.h" #include "DragDropHandler.h" #include "FileWatcher.h" #include "LSPClient.h" #include "Pipeline.h" #include "Diagnostics.h" #include "SettingsManager.h" #include "LayoutManager.h" #include "ASTMutationAPI.h" #include "MemoryStrategyInference.h" #include "AnnotationConflict.h" #include "StrategyDashboard.h" #include "TransformEngine.h" #include "StrategyAwareOptimizer.h" #include "CrossLanguageProjector.h" #include "DiffUtils.h" #include "EditorModePolicy.h" #include "RefactorActions.h" #include "CommandPalette.h" #include "ContextAPI.h" #include "Breadcrumbs.h" #include "ProjectSearch.h" #include "GoToLine.h" #include "Orchestrator.h" #include "ProjectManager.h" #include "SessionManager.h" #include "ZoomUtils.h" #include "TerminalPanel.h" #include "IncrementalOptimizer.h" #include "ast/Serialization.h" #include "ast/Generator.h" #include "ast/Annotation.h" // --- Standard library --- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Forward declarations for utility functions defined in EditorUtils.h // (used inside EditorState inline method bodies) // --------------------------------------------------------------------------- static std::string generateForLanguage(const Module* ast, const std::string& language); static std::unique_ptr cloneModule(const Module* ast); static ASTNode* findNodeAtPosition(ASTNode* node, int line, int col); static ASTNode* findNodeById(ASTNode* node, const std::string& id); static int countAnnotationNodes(const ASTNode* node); // --------------------------------------------------------------------------- // Simple helpers used by EditorState methods // --------------------------------------------------------------------------- static inline std::string bufferModeToString(BufferManager::BufferMode mode) { return mode == BufferManager::BufferMode::Text ? "text" : "structured"; } static inline BufferManager::BufferMode bufferModeFromString(const std::string& mode) { if (mode == "text") return BufferManager::BufferMode::Text; return BufferManager::BufferMode::Structured; } // --------------------------------------------------------------------------- // Editor application state // --------------------------------------------------------------------------- struct BufferState { TextEditor editor; TextASTSync sync; CodeEditorWidget widget; CodeEditorWidget generatedWidget; EditorMode mode; EditorMode generatedMode; std::string language = "python"; std::string generatedLanguage = "python"; std::string path = "(untitled)"; bool readOnly = false; BufferManager::BufferMode bufferMode = BufferManager::BufferMode::Structured; bool modified = false; int cursorLine = 1; int cursorCol = 1; int lspVersion = 1; std::string editBuf; std::string generatedBuf; std::vector highlights; std::vector generatedHighlights; bool highlightsDirty = true; bool generatedHighlightsDirty = true; int generatedHighlightLine = -1; float splitScrollX = 0.0f; float splitScrollY = 0.0f; IncrementalOptimizer incrementalOptimizer; Orchestrator orchestrator; bool orchestratorDirty = true; int undoDepth = 0; }; struct EditorState { KeybindingManager keys; BufferManager buffers; std::map> bufferStates; BufferState* activeBuffer = nullptr; // 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; // Bottom panel int bottomTab = 0; // 0=Output, 1=AST, 2=Highlighted std::string outputLog; bool showTerminalPanel = false; TerminalPanel terminal; // 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; double symbolsLastChange = 0.0; bool completionPending = false; bool completionVisible = false; int completionSelected = 0; double completionLastChange = 0.0; bool hoverPending = false; double hoverLastMove = 0.0; ImVec2 hoverLastMouse = ImVec2(-1.0f, -1.0f); bool definitionPending = false; double definitionLastRequest = 0.0; int definitionRequestLine = 0; int definitionRequestCol = 0; std::string definitionRequestPath; LSPClient::DefinitionLocation definitionPreview; bool definitionPreviewValid = false; Pipeline pipeline; bool analysisPending = false; double analysisLastChange = 0.0; std::vector whetstoneDiagnostics; SettingsManager settings; bool showLspSettings = false; bool showSettingsPanel = false; double lastAutoSave = 0.0; ASTMutationAPI mutator; std::vector suggestions; bool showSuggestionPopup = false; MemoryStrategyInference::Suggestion suggestionPopup; std::string optimizeFoldSummary; std::string optimizeDeadCodeSummary; std::string optimizeApplySummary; bool optimizePreview = false; bool showRefactorPopup = false; int refactorAction = 0; // 1=rename,2=extract,3=inline char refactorNameA[128] = {}; char refactorNameB[128] = {}; std::string refactorError; CommandPalette commandPalette; std::unordered_map> commandHandlers; bool showCommandPalette = false; char commandQuery[128] = {}; int commandSelected = 0; struct DiffState { bool active = false; bool preview = false; int action = 0; // 1=constant-fold, 2=dead-code-elim, 3=apply-all bool batch = false; std::string beforeText; std::string afterText; std::vector transformIds; std::vector batchMutations; std::vector beforeLines; std::vector afterLines; } diff; CodeEditorWidget diffLeftWidget; CodeEditorWidget diffRightWidget; float diffScrollX = 0.0f; float diffScrollY = 0.0f; BufferState* active() { return activeBuffer; } bool isStructured() const { return activeBuffer && allowStructuredFeatures(activeBuffer->bufferMode); } Module* activeAST() { if (!activeBuffer) return nullptr; if (!allowStructuredFeatures(activeBuffer->bufferMode)) return nullptr; return activeBuffer->sync.getAST(); } static std::string toFileUri(const std::string& path) { std::filesystem::path p(path); return "file:///" + p.generic_string(); } static std::string fromFileUri(const std::string& uri) { const std::string prefix = "file:///"; if (uri.rfind(prefix, 0) != 0) return uri; std::string path = uri.substr(prefix.size()); std::replace(path.begin(), path.end(), '/', '\\'); return path; } static int byteOffsetForLineCol(const std::string& text, int lineZero, int colZero) { if (lineZero < 0) lineZero = 0; if (colZero < 0) colZero = 0; int line = 0; int pos = 0; while (pos < (int)text.size() && line < lineZero) { if (text[pos] == '\n') ++line; ++pos; } int col = 0; while (pos < (int)text.size() && text[pos] != '\n' && col < colZero) { ++pos; ++col; } return pos; } static int wordStartAt(const std::string& text, int pos) { pos = std::max(0, std::min(pos, (int)text.size())); while (pos > 0) { char c = text[pos - 1]; if (std::isalnum((unsigned char)c) || c == '_') { --pos; } else { break; } } return pos; } static bool isWordChar(char c) { return std::isalnum((unsigned char)c) || c == '_'; } static std::string wordAt(const std::string& text, int pos) { if (text.empty()) return ""; pos = std::max(0, std::min(pos, (int)text.size())); if (pos == (int)text.size()) pos = (int)text.size() - 1; if (pos < 0) return ""; if (!isWordChar(text[pos]) && pos > 0 && isWordChar(text[pos - 1])) { --pos; } if (!isWordChar(text[pos])) return ""; int start = pos; while (start > 0 && isWordChar(text[start - 1])) --start; int end = pos; while (end < (int)text.size() && isWordChar(text[end])) ++end; return text.substr(start, end - start); } static std::string wordAtLineCol(const std::string& text, int lineZero, int colZero) { int pos = byteOffsetForLineCol(text, lineZero, colZero); return wordAt(text, pos); } static std::string wordPrefixAt(const std::string& text, int pos) { int start = wordStartAt(text, pos); return text.substr(start, pos - start); } void jumpTo(BufferState* buf, int lineZero, int colZero) { if (!buf) return; int pos = byteOffsetForLineCol(buf->editBuf, lineZero, colZero); buf->widget.setCursor(pos); buf->cursorLine = lineZero + 1; buf->cursorCol = colZero + 1; } void applyCompletion(BufferState* buf, const std::string& insertText, int replaceStart, int replaceEnd) { if (!buf) return; replaceStart = std::max(0, std::min(replaceStart, (int)buf->editBuf.size())); replaceEnd = std::max(replaceStart, std::min(replaceEnd, (int)buf->editBuf.size())); buf->editBuf.replace(replaceStart, replaceEnd - replaceStart, insertText); int newPos = replaceStart + (int)insertText.size(); buf->widget.setCursor(newPos); activeBuffer = buf; onTextChanged(); } std::string makeUntitledName() const { if (!buffers.hasBuffer("(untitled)")) return "(untitled)"; int i = 1; while (true) { std::string name = "(untitled-" + std::to_string(i) + ")"; if (!buffers.hasBuffer(name)) return name; ++i; } } void createBuffer(const std::string& path, const std::string& content, const std::string& language, BufferManager::BufferMode mode = BufferManager::BufferMode::Structured) { if (buffers.hasBuffer(path)) { buffers.switchToBuffer(path); activeBuffer = bufferStates[path].get(); return; } auto state = std::make_unique(); state->path = path; state->language = language; state->generatedLanguage = language; state->mode.setLanguage(language); state->generatedMode.setLanguage(language); state->editor.setContent(content, language); state->sync.setText(content, language); state->sync.syncNow(); state->incrementalOptimizer.setRoot(state->sync.getAST()); state->editBuf = content; state->highlightsDirty = true; state->generatedHighlightsDirty = true; state->modified = false; state->lspVersion = 1; buffers.openBuffer(path, content, language, mode); state->bufferMode = mode; activeBuffer = state.get(); bufferStates[path] = std::move(state); active()->orchestratorDirty = true; recordUndoSnapshot(); if (path.rfind("(untitled", 0) != 0) watcher.watch(path); if (lsp && path.rfind("(untitled", 0) != 0) { lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion); } symbolsPending = true; symbolsLastChange = ImGui::GetTime(); if (lsp) lsp->clearDocumentSymbols(); } bool jumpToDefinitionLocation(const LSPClient::DefinitionLocation& loc) { if (loc.uri.empty()) return false; std::string path = fromFileUri(loc.uri); if (path.empty()) return false; if (buffers.hasBuffer(path)) switchToBuffer(path); else doOpen(path); jumpTo(active(), loc.range.start.line, loc.range.start.character); return true; } bool goToDefinitionFallback(int lineZero, int colZero) { if (!isStructured()) return false; Module* ast = activeAST(); if (!ast || !active()) return false; std::string word = wordAtLineCol(active()->editBuf, lineZero, colZero); if (word.empty()) return false; ASTNode* scopeNode = findNodeAtPosition(ast, lineZero, colZero); if (!scopeNode) scopeNode = ast; ContextAPI ctx; ctx.setRoot(ast); auto symbols = ctx.getInScopeSymbols(scopeNode->id); for (const auto& sym : symbols) { if (sym.name != word) continue; ASTNode* target = findNodeById(ast, sym.nodeId); if (target && target->hasSpan()) { jumpTo(active(), target->spanStartLine, target->spanStartCol); return true; } } return false; } bool goToDefinitionAt(int lineZero, int colZero) { if (!active()) return false; if (lsp && lspTransport && lspTransport->isOpen() && active()->path.rfind("(untitled", 0) != 0) { definitionPreviewValid = false; definitionPending = true; definitionLastRequest = ImGui::GetTime(); definitionRequestLine = lineZero; definitionRequestCol = colZero; definitionRequestPath = active()->path; lsp->clearDefinitionLocations(); lsp->requestDefinition(toFileUri(active()->path), lineZero, colZero); return true; } return goToDefinitionFallback(lineZero, colZero); } bool previewDefinitionAt(int lineZero, int colZero, std::string& outLabel) { if (!isStructured() || !active()) return false; Module* ast = activeAST(); if (!ast) return false; std::string word = wordAtLineCol(active()->editBuf, lineZero, colZero); if (word.empty()) return false; ASTNode* scopeNode = findNodeAtPosition(ast, lineZero, colZero); if (!scopeNode) scopeNode = ast; ContextAPI ctx; ctx.setRoot(ast); auto symbols = ctx.getInScopeSymbols(scopeNode->id); for (const auto& sym : symbols) { if (sym.name != word) continue; ASTNode* target = findNodeById(ast, sym.nodeId); if (target && target->hasSpan()) { outLabel = word + " -> line " + std::to_string(target->spanStartLine + 1); return true; } } return false; } void switchToBuffer(const std::string& path) { if (!buffers.hasBuffer(path)) return; buffers.switchToBuffer(path); activeBuffer = bufferStates[path].get(); if (active()) active()->orchestratorDirty = true; symbolsPending = true; symbolsLastChange = ImGui::GetTime(); if (lsp) lsp->clearDocumentSymbols(); } std::filesystem::path configDir() const { const char* home = std::getenv("USERPROFILE"); if (!home) home = std::getenv("HOME"); std::filesystem::path base = home ? std::filesystem::path(home) : std::filesystem::current_path(); return base / ".whetstone"; } std::filesystem::path recentFilePath() const { return configDir() / "recent.json"; } void loadRecentFiles() { std::filesystem::path p = recentFilePath(); if (!std::filesystem::exists(p)) return; std::ifstream in(p.string()); if (!in.is_open()) return; nlohmann::json j; in >> j; if (!j.is_array()) return; for (const auto& item : j) { if (!item.contains("path")) continue; std::string path = item.value("path", ""); std::string lang = item.value("language", ""); std::string mode = item.value("mode", "structured"); if (!path.empty()) welcome.addRecentFile(path, lang, mode); } } void saveRecentFiles() { std::filesystem::path p = recentFilePath(); std::filesystem::create_directories(p.parent_path()); nlohmann::json j = nlohmann::json::array(); for (const auto& rf : welcome.getRecentFiles()) { j.push_back({{"path", rf.path}, {"language", rf.language}, {"mode", rf.mode}}); } std::ofstream out(p.string(), std::ios::binary); out << j.dump(2); } void closeAllBuffers() { auto open = buffers.getOpenBuffers(); for (const auto& path : open) { buffers.closeBuffer(path); bufferStates.erase(path); if (path.rfind("(untitled", 0) != 0) watcher.unwatch(path); } activeBuffer = nullptr; } bool saveProject(const std::string& path) { ProjectFile project; project.workspaceRoot = workspaceRoot; project.activePath = active() ? active()->path : ""; for (const auto& bufPath : buffers.getOpenBuffers()) { auto it = bufferStates.find(bufPath); if (it == bufferStates.end()) continue; ProjectBufferInfo info; info.path = bufPath; info.language = it->second->language; info.mode = bufferModeToString(it->second->bufferMode); project.buffers.push_back(std::move(info)); } if (isStructured()) { syncOrchestratorFromActive(); if (orchestrator.getAST()) { project.ast = cloneModule(orchestrator.getAST()); } } try { nlohmann::json j = projectToJson(project); std::ofstream out(path, std::ios::binary); if (!out.is_open()) return false; out << j.dump(2); return true; } catch (...) { return false; } } bool loadProject(const std::string& path) { try { std::ifstream in(path, std::ios::binary); if (!in.is_open()) return false; nlohmann::json j; in >> j; ProjectFile project = projectFromJson(j); closeAllBuffers(); if (!project.workspaceRoot.empty()) { workspaceRoot = project.workspaceRoot; fileTreeDirty = true; projectSearch.setRoot(workspaceRoot); } std::string activePath = project.activePath; for (const auto& buf : project.buffers) { if (buf.path == activePath && project.ast) { std::string lang = buf.language.empty() ? project.ast->targetLanguage : buf.language; std::string generated = generateForLanguage(project.ast.get(), lang); createBuffer(buf.path, generated, lang, bufferModeFromString(buf.mode)); if (active()) { active()->sync.setAST(std::move(project.ast)); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->editBuf = active()->sync.getText(); active()->editor.setContent(active()->editBuf, lang); active()->mode.setLanguage(lang); active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = false; recordUndoSnapshot(); } } else { doOpen(buf.path, bufferModeFromString(buf.mode)); } } if (!activePath.empty() && buffers.hasBuffer(activePath)) { switchToBuffer(activePath); } if (active()) active()->orchestratorDirty = true; return true; } catch (...) { return false; } } std::filesystem::path sessionFilePath() const { return configDir() / "session.json"; } std::filesystem::path settingsFilePath() const { return configDir() / "settings.json"; } bool saveSession(const std::string& imguiIni) { SessionData session; session.workspaceRoot = workspaceRoot; session.activePath = active() ? active()->path : ""; session.layoutPreset = LayoutManager::presetName(layoutPreset); session.imguiIni = imguiIni; for (const auto& bufPath : buffers.getOpenBuffers()) { auto it = bufferStates.find(bufPath); if (it == bufferStates.end()) continue; const auto* buf = it->second.get(); SessionBufferState entry; entry.path = bufPath; entry.language = buf->language; entry.mode = bufferModeToString(buf->bufferMode); entry.cursorLine = buf->cursorLine; entry.cursorCol = buf->cursorCol; entry.foldedLines = buf->widget.getFoldedLines(); session.buffers.push_back(std::move(entry)); } try { std::ofstream out(sessionFilePath().string(), std::ios::binary); if (!out.is_open()) return false; out << sessionToJson(session).dump(2); return true; } catch (...) { return false; } } bool loadSession(SessionData& out) { try { std::ifstream in(sessionFilePath().string(), std::ios::binary); if (!in.is_open()) return false; nlohmann::json j; in >> j; out = sessionFromJson(j); return true; } catch (...) { return false; } } void applySession(const SessionData& session) { closeAllBuffers(); if (!session.workspaceRoot.empty()) { workspaceRoot = session.workspaceRoot; fileTreeDirty = true; projectSearch.setRoot(workspaceRoot); } layoutPreset = LayoutManager::presetFromName(session.layoutPreset); for (const auto& buf : session.buffers) { doOpen(buf.path, bufferModeFromString(buf.mode)); auto it = bufferStates.find(buf.path); if (it != bufferStates.end()) { auto* bs = it->second.get(); bs->cursorLine = buf.cursorLine; bs->cursorCol = buf.cursorCol; bs->widget.setDesiredFoldedLines(buf.foldedLines); jumpTo(bs, std::max(0, buf.cursorLine - 1), std::max(0, buf.cursorCol - 1)); } } if (!session.activePath.empty() && buffers.hasBuffer(session.activePath)) { switchToBuffer(session.activePath); } } void applyTabSizeToBuffers(int size) { for (auto& kv : bufferStates) { kv.second->mode.setTabSize(size); kv.second->generatedMode.setTabSize(size); } } void applySettingsToState() { showMinimap = settings.getShowMinimap(); showLineNumbers = settings.getShowLineNumbers(); layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset()); keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile())); registerCommands(); applyTabSizeToBuffers(settings.getTabSize()); } void loadSettingsFromDisk() { settings.loadFromFile(settingsFilePath().string()); applySettingsToState(); } void saveSettingsToDisk() { settings.setShowMinimap(showMinimap); settings.setShowLineNumbers(showLineNumbers); settings.setLayoutPreset(LayoutManager::presetName(layoutPreset)); settings.setKeybindingProfile(KeybindingManager::profileName(keys.getProfile())); std::filesystem::create_directories(settingsFilePath().parent_path()); settings.saveToFile(settingsFilePath().string()); } void init() { outputLog = "Whetstone Editor ready.\n"; workspaceRoot = std::filesystem::current_path().string(); projectSearch.setRoot(workspaceRoot); fileTreeDirty = true; loadRecentFiles(); loadSettingsFromDisk(); registerCommands(); } void syncOrchestratorFromActive() { if (!active()) return; if (!isStructured()) return; Module* ast = active()->sync.getAST(); if (!ast) return; active()->orchestrator.setAST(cloneModule(ast)); active()->orchestratorDirty = false; } void applyOrchestratorToActive() { if (!active()) return; if (!isStructured()) return; Module* ast = active()->orchestrator.getAST(); if (!ast) return; active()->sync.setAST(cloneModule(ast)); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); refreshActiveTextFromAST(); active()->orchestratorDirty = false; recordUndoSnapshot(); } Module* mutationAST() { if (!active()) return nullptr; if (!isStructured()) return nullptr; if (active()->orchestratorDirty || !active()->orchestrator.getAST()) { syncOrchestratorFromActive(); } return active()->orchestrator.getAST(); } void recordUndoSnapshot() { if (!active()) return; Module* ast = isStructured() ? active()->sync.getAST() : nullptr; active()->orchestrator.recordSnapshot(active()->editBuf, ast); active()->undoDepth = active()->orchestrator.getUndoDepth(); } void applySnapshotToActive(const std::string& text, std::unique_ptr ast) { if (!active()) return; active()->editBuf = text; active()->editor.setContent(active()->editBuf, active()->language); if (active()->bufferMode == BufferManager::BufferMode::Structured) { if (ast) { active()->orchestrator.setAST(cloneModule(ast.get())); active()->sync.setAST(std::move(ast)); } else { active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); active()->orchestrator.setAST(cloneModule(active()->sync.getAST())); } active()->incrementalOptimizer.setRoot(active()->sync.getAST()); } active()->orchestratorDirty = false; active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; if (lsp && active()->path.rfind("(untitled", 0) != 0) { active()->lspVersion += 1; lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); } } void registerCommand(const std::string& id, const std::string& label, const std::string& shortcut, std::function fn) { commandPalette.registerCommand(id, label, shortcut); commandHandlers[id] = std::move(fn); } void registerCommands() { commandPalette = CommandPalette(); commandHandlers.clear(); registerCommand("file.new", "File: New File", keys.getBinding("file.new").toString(), [this]() { std::string lang = active() ? active()->language : "python"; createBuffer(makeUntitledName(), "", lang); }); registerCommand("file.open", "File: Open...", keys.getBinding("file.open").toString(), [this]() { std::string path = FileDialog::openFile( {"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"}, std::string()}); if (!path.empty()) doOpen(path); }); registerCommand("file.save", "File: Save", keys.getBinding("file.save").toString(), [this]() { doSave(); }); registerCommand("project.open", "Project: Open...", "", [this]() { std::string path = FileDialog::openFile( {"Open Project", {"*.whetstone"}, std::string()}); if (!path.empty()) { if (!loadProject(path)) { outputLog += "Failed to open project: " + path + "\n"; } } }); registerCommand("project.save", "Project: Save...", "", [this]() { std::string path = FileDialog::saveFile( {"Save Project", {"*.whetstone"}, std::string()}); if (!path.empty()) { if (!saveProject(path)) { outputLog += "Failed to save project: " + path + "\n"; } } }); registerCommand("edit.undo", "Edit: Undo", keys.getBinding("edit.undo").toString(), [this]() { doUndo(); }); registerCommand("edit.redo", "Edit: Redo", keys.getBinding("edit.redo").toString(), [this]() { doRedo(); }); registerCommand("search.find", "Search: Find/Replace", keys.getBinding("search.find").toString(), [this]() { showFind = !showFind; }); registerCommand("search.findInFiles", "Search: Find in Files", keys.getBinding("search.findInFiles").toString(), [this]() { showProjectSearch = !showProjectSearch; }); registerCommand("nav.goToLine", "Navigate: Go to Line", keys.getBinding("nav.goToLine").toString(), [this]() { showGoToLine = true; goToLineBuf[0] = '\0'; goToLineError = false; }); registerCommand("view.whitespace", "View: Toggle Whitespace", "", [this]() { showWhitespace = !showWhitespace; }); registerCommand("view.minimap", "View: Toggle Minimap", "", [this]() { showMinimap = !showMinimap; }); registerCommand("view.annotations", "View: Toggle Annotations", "", [this]() { showAnnotations = !showAnnotations; }); registerCommand("view.outline", "View: Toggle Outline", "", [this]() { showOutline = !showOutline; }); registerCommand("view.toggleTerminal", "View: Toggle Terminal", keys.getBinding("view.toggleTerminal").toString(), [this]() { showTerminalPanel = !showTerminalPanel; }); registerCommand("view.lsp", "View: LSP Servers...", "", [this]() { showLspSettings = !showLspSettings; }); registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "", [this]() { if (!active()) return; active()->bufferMode = active()->bufferMode == BufferManager::BufferMode::Text ? BufferManager::BufferMode::Structured : BufferManager::BufferMode::Text; buffers.setBufferMode(active()->path, active()->bufferMode); if (active()->bufferMode == BufferManager::BufferMode::Text) { suggestions.clear(); whetstoneDiagnostics.clear(); analysisPending = false; } else { onTextChanged(); } }); registerCommand("refactor.rename", "Refactor: Rename Variable", "", [this]() { refactorAction = 1; showRefactorPopup = true; refactorNameA[0] = '\0'; refactorNameB[0] = '\0'; }); registerCommand("refactor.extract", "Refactor: Extract Function", "", [this]() { refactorAction = 2; showRefactorPopup = true; refactorNameA[0] = '\0'; refactorNameB[0] = '\0'; }); registerCommand("refactor.inline", "Refactor: Inline Variable", "", [this]() { refactorAction = 3; showRefactorPopup = true; refactorNameA[0] = '\0'; refactorNameB[0] = '\0'; }); } void executeCommand(const std::string& id) { auto it = commandHandlers.find(id); if (it == commandHandlers.end()) return; it->second(); commandPalette.markUsed(id); } void setLanguage(const std::string& lang) { if (!active()) return; std::string prevLang = active()->language; active()->language = lang; active()->mode.setLanguage(lang); if (active()->generatedLanguage == prevLang) { active()->generatedLanguage = lang; active()->generatedMode.setLanguage(lang); active()->generatedHighlightsDirty = true; } active()->editor.setContent(active()->editBuf, lang); if (active()->bufferMode == BufferManager::BufferMode::Structured) { active()->sync.setText(active()->editBuf, lang); active()->sync.syncNow(); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); } active()->orchestratorDirty = true; active()->highlightsDirty = true; } // Called after editBuf changes (from ImGui input) void onTextChanged() { if (!active()) return; active()->editor.setContent(active()->editBuf, active()->language); if (active()->bufferMode == BufferManager::BufferMode::Structured) { active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); } active()->orchestratorDirty = true; active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; symbolsPending = true; symbolsLastChange = ImGui::GetTime(); if (lsp) lsp->clearDocumentSymbols(); if (lsp && active()->path.rfind("(untitled", 0) != 0) { active()->lspVersion += 1; lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); } recordUndoSnapshot(); } void doUndo() { if (!active()) return; std::string text; std::unique_ptr ast; if (active()->orchestrator.undoSnapshot(text, ast)) { applySnapshotToActive(text, std::move(ast)); } active()->undoDepth = active()->orchestrator.getUndoDepth(); } void doRedo() { if (!active()) return; std::string text; std::unique_ptr ast; if (active()->orchestrator.redoSnapshot(text, ast)) { applySnapshotToActive(text, std::move(ast)); } active()->undoDepth = active()->orchestrator.getUndoDepth(); } void doFind() { if (!active()) return; if (strlen(findBuf) == 0) return; int pos = active()->editor.find(findBuf, lastFindPos); if (pos >= 0) { 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 " + std::to_string(line) + ", col " + std::to_string(col) + "\n"; } else { lastFindPos = 0; outputLog += "\"" + std::string(findBuf) + "\" not found.\n"; } } void doReplaceAll() { if (!active()) return; if (strlen(findBuf) == 0) return; int count = active()->editor.replaceAll(findBuf, replaceBuf); if (count > 0) { active()->editBuf = active()->editor.getContent(); if (active()->bufferMode == BufferManager::BufferMode::Structured) { active()->sync.setText(active()->editBuf, active()->language); active()->sync.syncNow(); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); } active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; } outputLog += "Replaced " + std::to_string(count) + " occurrence(s).\n"; } void doSave() { if (!active()) return; if (active()->path.rfind("(untitled", 0) == 0) return; std::ofstream out(active()->path); if (out.is_open()) { out << active()->editBuf; out.close(); active()->modified = false; outputLog += "Saved: " + active()->path + "\n"; if (lsp) lsp->didSave(toFileUri(active()->path), active()->editBuf); } else { outputLog += "Error saving: " + active()->path + "\n"; } } void doOpen(const std::string& path, BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured) { std::ifstream in(path); if (in.is_open()) { std::ostringstream ss; ss << in.rdbuf(); std::string content = ss.str(); std::string language = "python"; if (path.size() > 3 && path.substr(path.size() - 3) == ".py") language = "python"; else if (path.size() > 4 && path.substr(path.size() - 4) == ".cpp") language = "cpp"; else if (path.size() > 2 && path.substr(path.size() - 2) == ".h") language = "cpp"; else if (path.size() > 3 && path.substr(path.size() - 3) == ".el") language = "elisp"; else if (path.size() > 3 && path.substr(path.size() - 3) == ".js") language = "javascript"; else if (path.size() > 3 && path.substr(path.size() - 3) == ".ts") language = "typescript"; else if (path.size() > 5 && path.substr(path.size() - 5) == ".java") language = "java"; else if (path.size() > 3 && path.substr(path.size() - 3) == ".rs") language = "rust"; else if (path.size() > 3 && path.substr(path.size() - 3) == ".go") language = "go"; createBuffer(path, content, language, modeOverride); welcome.addRecentFile(path, language, bufferModeToString(modeOverride)); saveRecentFiles(); outputLog += "Opened: " + path + "\n"; } else { outputLog += "Error opening: " + path + "\n"; } } void reloadBuffer(const std::string& path) { auto it = bufferStates.find(path); if (it == bufferStates.end()) return; std::ifstream in(path); if (!in.is_open()) return; std::ostringstream ss; ss << in.rdbuf(); auto* buf = it->second.get(); buf->editBuf = ss.str(); buf->editor.setContent(buf->editBuf, buf->language); if (buf->bufferMode == BufferManager::BufferMode::Structured) { buf->sync.setText(buf->editBuf, buf->language); buf->sync.syncNow(); buf->incrementalOptimizer.setRoot(buf->sync.getAST()); } buf->highlightsDirty = true; buf->generatedHighlightsDirty = true; buf->modified = false; outputLog += "Reloaded: " + path + "\n"; } void handleFileChanges() { auto changed = watcher.poll(); for (const auto& path : changed) { auto it = bufferStates.find(path); if (it == bufferStates.end()) continue; if (it->second->modified) { outputLog += "File changed on disk (dirty): " + path + "\n"; } else { reloadBuffer(path); } } } void updateHighlights() { if (!active()) return; if (!active()->highlightsDirty) return; active()->highlights = SyntaxHighlighter::highlight(active()->editBuf, active()->language); active()->highlightsDirty = false; } void updateGenerated() { if (!active()) return; if (active()->bufferMode == BufferManager::BufferMode::Text) return; Module* ast = active()->sync.getAST(); std::string generated = generateForLanguage(ast, active()->generatedLanguage); if (generated != active()->generatedBuf) { active()->generatedBuf = generated; active()->generatedHighlightsDirty = true; } if (active()->generatedHighlightsDirty) { active()->generatedHighlights = SyntaxHighlighter::highlight(active()->generatedBuf, active()->generatedLanguage); active()->generatedHighlightsDirty = false; } } void projectToLanguage(const std::string& targetLanguage) { if (!active()) return; Module* ast = activeAST(); if (!ast) { outputLog += "Project to " + targetLanguage + ": no AST available.\n"; return; } CrossLanguageProjector projector; auto projected = projector.project(ast, targetLanguage); if (!projected) { outputLog += "Project to " + targetLanguage + ": failed to project AST.\n"; return; } const int srcAnnoCount = countAnnotationNodes(ast); const int projAnnoCount = countAnnotationNodes(projected.get()); const bool preserved = projector.annotationsPreserved(ast, projected.get()); std::string generated = generateForLanguage(projected.get(), targetLanguage); std::string baseName = "(untitled-projection:" + targetLanguage + ")"; std::string projName = baseName; int suffix = 1; while (buffers.hasBuffer(projName)) { projName = baseName + "-" + std::to_string(suffix++); } createBuffer(projName, generated, targetLanguage); if (active()) { active()->readOnly = true; active()->sync.setText(generated, targetLanguage); active()->sync.setAST(std::move(projected)); active()->incrementalOptimizer.setRoot(active()->sync.getAST()); active()->editBuf = generated; active()->editor.setContent(active()->editBuf, targetLanguage); active()->mode.setLanguage(targetLanguage); active()->highlightsDirty = true; active()->generatedLanguage = targetLanguage; active()->generatedMode.setLanguage(targetLanguage); active()->generatedHighlightsDirty = true; active()->modified = false; } outputLog += "Projected to " + targetLanguage + " in " + projName + " (annotations " + std::to_string(srcAnnoCount) + " -> " + std::to_string(projAnnoCount) + ", types preserved: " + (preserved ? "yes" : "no") + ").\n"; } void refreshActiveTextFromAST() { if (!active()) return; if (!isStructured()) return; active()->editBuf = active()->sync.getText(); active()->editor.setContent(active()->editBuf, active()->language); active()->highlightsDirty = true; active()->modified = true; if (lsp && active()->path.rfind("(untitled", 0) != 0) { active()->lspVersion += 1; lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); } analysisPending = true; analysisLastChange = ImGui::GetTime(); } void openDiff(const std::string& beforeText, const std::string& afterText, bool preview, int action, const std::vector& transformIds, const std::vector& batchMutations = {}) { diff.active = true; diff.preview = preview; diff.action = action; diff.batch = !batchMutations.empty(); diff.beforeText = beforeText; diff.afterText = afterText; diff.transformIds = transformIds; diff.batchMutations = batchMutations; LineDiff lineDiff = buildLineDiff(beforeText, afterText); diff.beforeLines = std::move(lineDiff.beforeLines); diff.afterLines = std::move(lineDiff.afterLines); } void updateCursorPos(int bytePos) { if (!active()) return; active()->cursorLine = 1; active()->cursorCol = 1; for (int i = 0; i < bytePos && i < (int)active()->editBuf.size(); ++i) { if (active()->editBuf[i] == '\n') { ++active()->cursorLine; active()->cursorCol = 1; } else { ++active()->cursorCol; } } } void refreshFileTree() { if (!fileTreeDirty) return; fileTreeRoot = fileTree.build(workspaceRoot); fileTreeDirty = false; } }; struct NullLSPTransport : public LSPTransport { void send(const std::string& msg) override { (void)msg; } bool receive(std::string& out) override { (void)out; return false; } bool isOpen() const override { return false; } void close() override {} };