#pragma once // --- BufferOps.h --- // Extracted from EditorState.h (Sprint 8, Step 236). // Buffer, file, project, and session operations. // Included from EditorState.h after the EditorState struct definition. inline std::string EditorState::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; } } inline BufferManager::BufferMode EditorState::defaultBufferMode() const { return bufferModeFromString(settings.getDefaultBufferMode()); } inline void EditorState::createBuffer(const std::string& path, const std::string& content, const std::string& language, BufferManager::BufferMode mode = BufferManager::BufferMode::Text, size_t fileSizeBytes = 0, bool largeFileMode = false, bool disableSyntaxHighlight = false, bool deferAstSync = false) { BufferManager::BufferMode effectiveMode = mode; if (language == "org") effectiveMode = BufferManager::BufferMode::Text; 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->fileSizeBytes = fileSizeBytes; state->largeFileMode = largeFileMode; state->disableSyntaxHighlight = disableSyntaxHighlight; state->mode.setLanguage(language); state->generatedMode.setLanguage(language); state->editor.setContent(content, language); if (effectiveMode == BufferManager::BufferMode::Structured) { if (deferAstSync) { state->pendingAstSync = true; state->pendingAstStart = ImGui::GetTime(); } else { 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, effectiveMode); state->bufferMode = effectiveMode; activeBuffer = state.get(); bufferStates[path] = std::move(state); active()->orchestratorDirty = true; recordUndoSnapshot(); if (language == "elisp") { emacsState.emacsFunctionIndexDirty = true; } if (path.rfind("(untitled", 0) != 0) watcher.watch(path); if (lsp && path.rfind("(untitled", 0) != 0) { lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion); } events.publish(UIEventType::BufferSwitched, path, {}, ImGui::GetTime()); uiAnimations.recordTabSwitch(path, ImGui::GetTime()); } inline void EditorState::switchToBuffer(const std::string& path) { if (!buffers.hasBuffer(path)) return; buffers.switchToBuffer(path); activeBuffer = bufferStates[path].get(); if (active()) active()->orchestratorDirty = true; events.publish(UIEventType::BufferSwitched, path, {}, ImGui::GetTime()); uiAnimations.recordTabSwitch(path, ImGui::GetTime()); } inline std::filesystem::path EditorState::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"; } inline std::filesystem::path EditorState::recentFilePath() const { return configDir() / "recent.json"; } inline void EditorState::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); } } inline void EditorState::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); } inline void EditorState::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; } inline bool EditorState::renameBufferPath(const std::string& oldPath, const std::string& newPath) { if (oldPath == newPath) return false; if (!buffers.hasBuffer(oldPath) || buffers.hasBuffer(newPath)) return false; auto it = bufferStates.find(oldPath); if (it == bufferStates.end()) return false; auto node = std::move(it->second); bufferStates.erase(it); node->path = newPath; bufferStates[newPath] = std::move(node); buffers.renameBuffer(oldPath, newPath); if (oldPath.rfind("(untitled", 0) != 0) watcher.unwatch(oldPath); if (newPath.rfind("(untitled", 0) != 0) watcher.watch(newPath); if (active() && active()->path == oldPath) { active()->path = newPath; } notify(NotificationLevel::Success, "Renamed buffer to " + newPath); return true; } inline bool EditorState::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() && active()) { syncOrchestratorFromActive(); if (active()->orchestrator.getAST()) { project.ast = cloneModule(active()->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; } } inline bool EditorState::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; search.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; } } inline std::filesystem::path EditorState::sessionFilePath() const { return configDir() / "session.json"; } inline std::filesystem::path EditorState::settingsFilePath() const { return configDir() / "settings.json"; } inline bool EditorState::saveSession(const std::string& imguiIni) { SessionData session; session.workspaceRoot = workspaceRoot; session.activePath = active() ? active()->path : ""; session.layoutPreset = LayoutManager::presetName(ui.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; } } inline bool EditorState::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; } } inline void EditorState::applySession(const SessionData& session) { closeAllBuffers(); if (!session.workspaceRoot.empty()) { workspaceRoot = session.workspaceRoot; fileTreeDirty = true; search.projectSearch.setRoot(workspaceRoot); } ui.layoutPreset = LayoutManager::presetFromName(session.layoutPreset); for (const auto& buf : session.buffers) { doOpen(buf.path, bufferModeFromString(buf.mode), true); 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); } } inline void EditorState::applySettingsToState() { ui.showMinimap = settings.getShowMinimap(); ui.showLineNumbers = settings.getShowLineNumbers(); ui.layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset()); keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile())); registerCommands(); applyTabSizeToBuffers(settings.getTabSize()); library.primitives.setBlockVulnerableImports(settings.getBlockVulnerableImports()); } inline void EditorState::loadSettingsFromDisk() { settings.loadFromFile(settingsFilePath().string()); applySettingsToState(); } inline void EditorState::saveSettingsToDisk() { 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()); } inline void EditorState::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; notify(NotificationLevel::Success, "Saved: " + active()->path); recordEditorEvent("file_saved", {{"path", active()->path}}); if (lsp) lsp->didSave(toFileUri(active()->path), active()->editBuf); } else { notify(NotificationLevel::Error, "Error saving: " + active()->path); } } inline void EditorState::doOpen(const std::string& path, BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Text, bool deferAstSync = false) { std::error_code sizeErr; size_t fileSizeBytes = 0; if (!path.empty()) { auto bytes = std::filesystem::file_size(path, sizeErr); if (!sizeErr) fileSizeBytes = static_cast(bytes); } const size_t mb = 1024 * 1024; const size_t warnBytes = (size_t)std::max(1, settings.getLargeFileWarnMB()) * mb; const size_t textBytes = (size_t)std::max(1, settings.getLargeFileTextMB()) * mb; const size_t disableHlBytes = (size_t)std::max(1, settings.getLargeFileDisableHighlightMB()) * mb; bool warnLarge = fileSizeBytes >= warnBytes; bool autoText = fileSizeBytes >= textBytes; bool disableHighlight = fileSizeBytes >= disableHlBytes; bool largeFileMode = disableHighlight; BufferManager::BufferMode effectiveMode = modeOverride; if (autoText && modeOverride == BufferManager::BufferMode::Structured) { effectiveMode = BufferManager::BufferMode::Text; } 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"; else if (path.size() > 4 && path.substr(path.size() - 4) == ".org") language = "org"; createBuffer(path, content, language, effectiveMode, fileSizeBytes, largeFileMode, disableHighlight, deferAstSync); welcome.addRecentFile(path, language, bufferModeToString(effectiveMode)); saveRecentFiles(); if (autoText && modeOverride == BufferManager::BufferMode::Structured) { notify(NotificationLevel::Warning, "Large file opened in Text mode (" + std::to_string(fileSizeBytes / mb) + " MB)."); } else if (warnLarge && modeOverride == BufferManager::BufferMode::Structured) { showLargeFilePrompt = true; largeFilePromptPath = path; largeFilePromptBytes = fileSizeBytes; } notify(NotificationLevel::Success, "Opened: " + path); recordEditorEvent("file_opened", { {"path", path}, {"language", language}, {"mode", bufferModeToString(effectiveMode)}, {"sizeBytes", fileSizeBytes} }); } else { notify(NotificationLevel::Error, "Error opening: " + path); } } inline void EditorState::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->highlightRequestTime = ImGui::GetTime(); buf->generatedHighlightsDirty = true; buf->modified = false; notify(NotificationLevel::Info, "Reloaded: " + path); } inline void EditorState::handleFileChanges() { auto changed = watcher.poll(); for (const auto& path : changed) { auto it = bufferStates.find(path); if (it == bufferStates.end()) continue; if (it->second->modified) { notify(NotificationLevel::Warning, "File changed on disk (dirty): " + path); } else { reloadBuffer(path); } } } inline void EditorState::updateHighlights() { if (!active()) return; if (!active()->highlightsDirty) return; double now = ImGui::GetTime(); double delay = settings.getHighlightDebounceMs() / 1000.0; if (active()->highlightRequestTime > 0.0 && (now - active()->highlightRequestTime) < delay) { return; } if (active()->disableSyntaxHighlight) { active()->highlights.clear(); active()->highlightsDirty = false; return; } active()->highlights = SyntaxHighlighter::highlight(active()->editBuf, active()->language); active()->highlightsDirty = false; } inline void EditorState::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()->disableSyntaxHighlight) { active()->generatedHighlights.clear(); active()->generatedHighlightsDirty = false; return; } if (active()->generatedHighlightsDirty) { active()->generatedHighlights = SyntaxHighlighter::highlight(active()->generatedBuf, active()->generatedLanguage); active()->generatedHighlightsDirty = false; } } inline void EditorState::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; } } } inline void EditorState::refreshFileTree() { if (!fileTreeDirty) return; fileTreeRoot = fileTree.build(workspaceRoot); fileTreeDirty = false; } inline void EditorState::navigateToTarget(const NotificationTarget& target) { if (target.path.empty()) return; std::string path = target.path; if (buffers.hasBuffer(path)) switchToBuffer(path); else doOpen(path, defaultBufferMode()); if (active()) { int line = std::max(0, target.line); int col = std::max(0, target.col); jumpTo(active(), line, col); } } inline void EditorState::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(); } inline void EditorState::insertTextAtCursor(const std::string& text) { if (!active() || text.empty()) return; int cursor = active()->widget.getCursor(); cursor = std::max(0, std::min(cursor, (int)active()->editBuf.size())); active()->editBuf.insert(cursor, text); active()->widget.setCursor(cursor + (int)text.size()); onTextChanged(); } // --- Projection and lifecycle helpers (extracted) --- #include "BufferOpsProjection.h" #include "BufferOpsLifecycle.h"