2026-02-10 11:22:44 -07:00
|
|
|
#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,
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
BufferManager::BufferMode mode,
|
|
|
|
|
size_t fileSizeBytes,
|
|
|
|
|
bool largeFileMode,
|
|
|
|
|
bool disableSyntaxHighlight,
|
|
|
|
|
bool deferAstSync) {
|
2026-02-10 11:22:44 -07:00
|
|
|
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,
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
BufferManager::BufferMode modeOverride,
|
|
|
|
|
bool deferAstSync) {
|
2026-02-10 11:22:44 -07:00
|
|
|
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"
|