Files
whetstone_DSL/editor/src/BufferOps.h
Bill 1696b92bb8 Add Sprints 46-59: governance, porting foundation, and language graduation prep (Steps 689-828)
Sprints 46-58 implement the cross-language porting foundation: language-to-IR
adapters, equivalence checking, gate validation, legacy ingestion, managed/dynamic
families, low-level/logic-actor semantics, debug workflow tooling, AST-native
family tools, Rust/CPP raising tools, system-level orchestration, query family,
and porting gates. Sprint 59 adds the governance layer (policy packs, review
boards, waiver packets, ambiguity triage, decision ledger) with the
whetstone_review_porting_decision MCP tool.

Also includes: sprint plans 46-130, MCP taskitem pipeline scripts,
CLAUDE.md, docs, and full test matrix (steps 689-828).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:18:10 -07:00

712 lines
26 KiB
C++

#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<char>(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), "&lt;", "<");
s = replaceAll(std::move(s), "&gt;", ">");
s = replaceAll(std::move(s), "&amp;", "&");
s = replaceAll(std::move(s), "&quot;", "\"");
s = replaceAll(std::move(s), "&apos;", "'");
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), "<w:tab/>", "\t");
xml = replaceAll(std::move(xml), "<w:br/>", "\n");
xml = replaceAll(std::move(xml), "<w:cr/>", "\n");
xml = replaceAll(std::move(xml), "</w:p>", "\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<BufferState>();
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<size_t>(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<char>(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"