#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 detectLanguageFromPath(const std::string& path) { std::filesystem::path p(path); std::string ext = p.extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (ext == ".py") return "python"; if (ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" || ext == ".h" || ext == ".hpp" || ext == ".hh") return "cpp"; if (ext == ".el") return "elisp"; if (ext == ".js" || ext == ".mjs" || ext == ".cjs") return "javascript"; if (ext == ".ts" || ext == ".tsx") return "typescript"; if (ext == ".java") return "java"; if (ext == ".rs") return "rust"; if (ext == ".go") return "go"; if (ext == ".org") return "org"; if (ext == ".md" || ext == ".markdown") return "markdown"; if (ext == ".txt" || ext == ".rst") return "text"; if (ext == ".docx") return "text"; if (ext == ".json" || ext == ".yaml" || ext == ".yml" || ext == ".toml" || ext == ".xml" || ext == ".ini" || ext == ".cfg" || ext == ".conf") return "text"; return "text"; } inline bool supportsStructuredMode(const std::string& language) { return language == "python" || language == "cpp" || language == "elisp" || language == "javascript" || language == "typescript" || language == "java" || language == "rust" || language == "go"; } inline std::string replaceAll(std::string s, const std::string& from, const std::string& to) { if (from.empty()) return s; size_t pos = 0; while ((pos = s.find(from, pos)) != std::string::npos) { s.replace(pos, from.size(), to); pos += to.size(); } return s; } inline std::string decodeXmlEntities(std::string s) { s = replaceAll(std::move(s), "<", "<"); s = replaceAll(std::move(s), ">", ">"); s = replaceAll(std::move(s), "&", "&"); s = replaceAll(std::move(s), """, "\""); s = replaceAll(std::move(s), "'", "'"); return s; } inline std::string shellSingleQuote(const std::string& path) { std::string out; out.reserve(path.size() + 8); out.push_back('\''); for (char c : path) { if (c == '\'') out += "'\\''"; else out.push_back(c); } out.push_back('\''); return out; } inline bool readDocxAsText(const std::string& path, std::string& outText, std::string& outError) { outText.clear(); outError.clear(); #ifdef _WIN32 outError = "DOCX import is currently available on Linux/macOS builds only"; return false; #else std::string command = "unzip -p " + shellSingleQuote(path) + " word/document.xml 2>/dev/null"; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { outError = "failed to run unzip"; return false; } char buffer[4096]; std::string xml; while (fgets(buffer, sizeof(buffer), pipe)) { xml += buffer; } int rc = pclose(pipe); if (rc != 0 || xml.empty()) { outError = "could not extract DOCX XML (is unzip installed?)"; return false; } xml = replaceAll(std::move(xml), "", "\t"); xml = replaceAll(std::move(xml), "", "\n"); xml = replaceAll(std::move(xml), "", "\n"); xml = replaceAll(std::move(xml), "", "\n\n"); std::string plain; plain.reserve(xml.size()); bool inTag = false; for (char c : xml) { if (c == '<') { inTag = true; continue; } if (c == '>') { inTag = false; continue; } if (!inTag) plain.push_back(c); } plain = decodeXmlEntities(std::move(plain)); while (plain.find("\n\n\n") != std::string::npos) { plain = replaceAll(std::move(plain), "\n\n\n", "\n\n"); } if (plain.empty()) { outError = "DOCX contained no readable text"; return false; } outText = std::move(plain); return true; #endif } 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, size_t fileSizeBytes, bool largeFileMode, bool disableSyntaxHighlight, bool deferAstSync) { BufferManager::BufferMode effectiveMode = mode; if (!supportsStructuredMode(language)) 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()) { std::string rootError; if (!setWorkspaceRoot(project.workspaceRoot, &rootError)) { notify(NotificationLevel::Warning, "Project workspace is unavailable: " + rootError); } } else { std::filesystem::path projectPath(path); std::string parent = projectPath.parent_path().string(); if (!parent.empty()) { setWorkspaceRoot(parent, nullptr); } } 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()) { setWorkspaceRoot(session.workspaceRoot, nullptr); } 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.showCompletionHelper = settings.getShowCompletionHelper(); 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.setShowCompletionHelper(ui.showCompletionHelper); 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, bool deferAstSync) { 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; const std::string language = detectLanguageFromPath(path); BufferManager::BufferMode effectiveMode = modeOverride; if (!supportsStructuredMode(language)) { effectiveMode = BufferManager::BufferMode::Text; } if (autoText && modeOverride == BufferManager::BufferMode::Structured) { effectiveMode = BufferManager::BufferMode::Text; } std::string content; bool opened = false; std::string openError; std::string ext = std::filesystem::path(path).extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (ext == ".docx") { opened = readDocxAsText(path, content, openError); } else { std::ifstream in(path); if (in.is_open()) { std::ostringstream ss; ss << in.rdbuf(); content = ss.str(); opened = true; } } if (opened) { 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 + (openError.empty() ? "" : " (" + openError + ")")); } } 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; namespace fs = std::filesystem; if (workspaceRoot.empty()) { fileTreeRoot = FileNode{}; fileTreeDirty = false; return; } std::error_code ec; fs::path rootPath = fs::path(workspaceRoot); if (!fs::exists(rootPath, ec) || !fs::is_directory(rootPath, ec)) { fileTreeRoot = FileNode{}; fileTreeDirty = false; return; } try { fileTreeRoot = fileTree.build(rootPath.string()); } catch (...) { fileTreeRoot = FileNode{}; } fileTreeDirty = false; } inline bool EditorState::setWorkspaceRoot(const std::string& path, std::string* error) { if (error) error->clear(); namespace fs = std::filesystem; if (path.empty()) { workspaceRoot.clear(); search.projectSearch.setRoot(""); fileTreeRoot = FileNode{}; fileTreeDirty = false; return true; } std::error_code ec; fs::path canonical = fs::weakly_canonical(fs::path(path), ec); if (ec || !fs::exists(canonical, ec) || !fs::is_directory(canonical, ec)) { if (error) *error = "invalid workspace path"; return false; } workspaceRoot = canonical.string(); fileTreeDirty = true; search.projectSearch.setRoot(workspaceRoot); refreshBuildSystem(); return true; } 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"