Files
whetstone_DSL/editor/src/BufferOps.h

547 lines
21 KiB
C
Raw Normal View History

#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,
size_t fileSizeBytes,
bool largeFileMode,
bool disableSyntaxHighlight,
bool deferAstSync) {
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<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()) {
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,
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;
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"