Files
whetstone_DSL/editor/src/EditorState.h

1469 lines
58 KiB
C
Raw Normal View History

#pragma once
// --- ImGui (needed for ImVec2, ImGui::GetTime) ---
#include "imgui.h"
// --- Editor components ---
#include "TextEditor.h"
#include "TextASTSync.h"
#include "SyntaxHighlighter.h"
#include "KeybindingManager.h"
#include "BufferManager.h"
#include "CodeEditorWidget.h"
#include "EditorMode.h"
#include "FileDialog.h"
#include "FileTree.h"
#include "WelcomeScreen.h"
#include "DragDropHandler.h"
#include "FileWatcher.h"
#include "LSPClient.h"
#include "Pipeline.h"
#include "Diagnostics.h"
#include "SettingsManager.h"
#include "LayoutManager.h"
#include "ASTMutationAPI.h"
#include "MemoryStrategyInference.h"
#include "AnnotationConflict.h"
#include "StrategyDashboard.h"
#include "TransformEngine.h"
#include "StrategyAwareOptimizer.h"
#include "CrossLanguageProjector.h"
#include "DiffUtils.h"
#include "EditorModePolicy.h"
#include "RefactorActions.h"
#include "CommandPalette.h"
#include "ContextAPI.h"
#include "Breadcrumbs.h"
#include "ProjectSearch.h"
#include "GoToLine.h"
#include "Orchestrator.h"
#include "ProjectManager.h"
#include "SessionManager.h"
#include "ZoomUtils.h"
#include "TerminalPanel.h"
#include "WebSocketServer.h"
#include "BuildSystem.h"
#include "IncrementalOptimizer.h"
#include "ast/Serialization.h"
#include "ast/Generator.h"
#include "ast/Annotation.h"
// --- Standard library ---
#include <cstdio>
#include <string>
#include <vector>
#include <algorithm>
#include <fstream>
#include <sstream>
#include <cstring>
#include <cctype>
#include <climits>
#include <filesystem>
#include <map>
#include <memory>
#include <cstdlib>
#include <functional>
#include <unordered_map>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// ---------------------------------------------------------------------------
// Forward declarations for utility functions defined in EditorUtils.h
// (used inside EditorState inline method bodies)
// ---------------------------------------------------------------------------
static std::string generateForLanguage(const Module* ast, const std::string& language);
static std::unique_ptr<Module> cloneModule(const Module* ast);
static ASTNode* findNodeAtPosition(ASTNode* node, int line, int col);
static ASTNode* findNodeById(ASTNode* node, const std::string& id);
static int countAnnotationNodes(const ASTNode* node);
// ---------------------------------------------------------------------------
// Simple helpers used by EditorState methods
// ---------------------------------------------------------------------------
static inline std::string bufferModeToString(BufferManager::BufferMode mode) {
return mode == BufferManager::BufferMode::Text ? "text" : "structured";
}
static inline BufferManager::BufferMode bufferModeFromString(const std::string& mode) {
if (mode == "text") return BufferManager::BufferMode::Text;
return BufferManager::BufferMode::Structured;
}
// ---------------------------------------------------------------------------
// Editor application state
// ---------------------------------------------------------------------------
struct BufferState {
TextEditor editor;
TextASTSync sync;
CodeEditorWidget widget;
CodeEditorWidget generatedWidget;
EditorMode mode;
EditorMode generatedMode;
std::string language = "python";
std::string generatedLanguage = "python";
std::string path = "(untitled)";
bool readOnly = false;
BufferManager::BufferMode bufferMode = BufferManager::BufferMode::Structured;
bool modified = false;
int cursorLine = 1;
int cursorCol = 1;
int lspVersion = 1;
std::string editBuf;
std::string generatedBuf;
std::vector<HighlightSpan> highlights;
std::vector<HighlightSpan> generatedHighlights;
bool highlightsDirty = true;
bool generatedHighlightsDirty = true;
int generatedHighlightLine = -1;
float splitScrollX = 0.0f;
float splitScrollY = 0.0f;
IncrementalOptimizer incrementalOptimizer;
Orchestrator orchestrator;
bool orchestratorDirty = true;
int undoDepth = 0;
};
struct EditorState {
KeybindingManager keys;
BufferManager buffers;
std::map<std::string, std::unique_ptr<BufferState>> bufferStates;
BufferState* activeBuffer = nullptr;
// Find/Replace state
bool showFind = false;
char findBuf[256] = {};
char replaceBuf[256] = {};
int lastFindPos = 0;
bool showProjectSearch = false;
char searchQuery[256] = {};
char searchInclude[256] = {};
char searchExclude[256] = {};
bool searchUseRegex = true;
std::vector<ProjectSearch::FileResult> searchResults;
bool showGoToLine = false;
char goToLineBuf[64] = {};
bool goToLineError = false;
// Bottom panel
int bottomTab = 0; // 0=Output, 1=AST, 2=Highlighted
std::string outputLog;
bool showTerminalPanel = false;
TerminalPanel terminal;
bool runInProgress = false;
bool hasRunResult = false;
int lastRunExitCode = 0;
std::string lastRunCommand;
std::unique_ptr<WebSocketAgentServer> agentServer;
MockWebSocketTransport* agentTransport = nullptr;
int agentPort = 8765;
std::vector<std::string> agentLog;
std::map<std::string, bool> agentMutationPermissions;
BuildSystem::Type buildType = BuildSystem::Type::None;
std::vector<BuildError> buildErrors;
std::string lastBuildOutput;
std::string lastBuildCommand;
// Custom editor widget state
bool showWhitespace = false;
bool showMinimap = false;
bool showAnnotations = false;
bool showOutline = true;
bool showLineNumbers = true;
char outlineFilter[128] = {};
LayoutPreset layoutPreset = LayoutPreset::VSCode;
WelcomeScreen welcome;
std::string workspaceRoot;
FileTree fileTree;
FileNode fileTreeRoot;
bool fileTreeDirty = true;
FileWatcher watcher;
ProjectSearch projectSearch;
std::shared_ptr<LSPTransport> lspTransport;
std::shared_ptr<LSPClient> lsp;
bool symbolsPending = false;
double symbolsLastChange = 0.0;
bool completionPending = false;
bool completionVisible = false;
int completionSelected = 0;
double completionLastChange = 0.0;
bool hoverPending = false;
double hoverLastMove = 0.0;
ImVec2 hoverLastMouse = ImVec2(-1.0f, -1.0f);
bool definitionPending = false;
double definitionLastRequest = 0.0;
int definitionRequestLine = 0;
int definitionRequestCol = 0;
std::string definitionRequestPath;
LSPClient::DefinitionLocation definitionPreview;
bool definitionPreviewValid = false;
Pipeline pipeline;
bool analysisPending = false;
double analysisLastChange = 0.0;
std::vector<EditorDiagnostic> whetstoneDiagnostics;
SettingsManager settings;
bool showLspSettings = false;
bool showSettingsPanel = false;
double lastAutoSave = 0.0;
ASTMutationAPI mutator;
std::vector<MemoryStrategyInference::Suggestion> suggestions;
bool showSuggestionPopup = false;
MemoryStrategyInference::Suggestion suggestionPopup;
std::string optimizeFoldSummary;
std::string optimizeDeadCodeSummary;
std::string optimizeApplySummary;
bool optimizePreview = false;
bool showRefactorPopup = false;
int refactorAction = 0; // 1=rename,2=extract,3=inline
char refactorNameA[128] = {};
char refactorNameB[128] = {};
std::string refactorError;
CommandPalette commandPalette;
std::unordered_map<std::string, std::function<void()>> commandHandlers;
bool showCommandPalette = false;
char commandQuery[128] = {};
int commandSelected = 0;
struct DiffState {
bool active = false;
bool preview = false;
int action = 0; // 1=constant-fold, 2=dead-code-elim, 3=apply-all
bool batch = false;
std::string beforeText;
std::string afterText;
std::vector<std::string> transformIds;
std::vector<BatchMutationAPI::Mutation> batchMutations;
std::vector<int> beforeLines;
std::vector<int> afterLines;
} diff;
CodeEditorWidget diffLeftWidget;
CodeEditorWidget diffRightWidget;
float diffScrollX = 0.0f;
float diffScrollY = 0.0f;
BufferState* active() { return activeBuffer; }
bool isStructured() const {
return activeBuffer && allowStructuredFeatures(activeBuffer->bufferMode);
}
Module* activeAST() {
if (!activeBuffer) return nullptr;
if (!allowStructuredFeatures(activeBuffer->bufferMode)) return nullptr;
return activeBuffer->sync.getAST();
}
static std::string toFileUri(const std::string& path) {
std::filesystem::path p(path);
return "file:///" + p.generic_string();
}
static std::string fromFileUri(const std::string& uri) {
const std::string prefix = "file:///";
if (uri.rfind(prefix, 0) != 0) return uri;
std::string path = uri.substr(prefix.size());
std::replace(path.begin(), path.end(), '/', '\\');
return path;
}
static int byteOffsetForLineCol(const std::string& text, int lineZero, int colZero) {
if (lineZero < 0) lineZero = 0;
if (colZero < 0) colZero = 0;
int line = 0;
int pos = 0;
while (pos < (int)text.size() && line < lineZero) {
if (text[pos] == '\n') ++line;
++pos;
}
int col = 0;
while (pos < (int)text.size() && text[pos] != '\n' && col < colZero) {
++pos;
++col;
}
return pos;
}
static int wordStartAt(const std::string& text, int pos) {
pos = std::max(0, std::min(pos, (int)text.size()));
while (pos > 0) {
char c = text[pos - 1];
if (std::isalnum((unsigned char)c) || c == '_') {
--pos;
} else {
break;
}
}
return pos;
}
static bool isWordChar(char c) {
return std::isalnum((unsigned char)c) || c == '_';
}
static std::string wordAt(const std::string& text, int pos) {
if (text.empty()) return "";
pos = std::max(0, std::min(pos, (int)text.size()));
if (pos == (int)text.size()) pos = (int)text.size() - 1;
if (pos < 0) return "";
if (!isWordChar(text[pos]) && pos > 0 && isWordChar(text[pos - 1])) {
--pos;
}
if (!isWordChar(text[pos])) return "";
int start = pos;
while (start > 0 && isWordChar(text[start - 1])) --start;
int end = pos;
while (end < (int)text.size() && isWordChar(text[end])) ++end;
return text.substr(start, end - start);
}
static std::string wordAtLineCol(const std::string& text, int lineZero, int colZero) {
int pos = byteOffsetForLineCol(text, lineZero, colZero);
return wordAt(text, pos);
}
static std::string wordPrefixAt(const std::string& text, int pos) {
int start = wordStartAt(text, pos);
return text.substr(start, pos - start);
}
void jumpTo(BufferState* buf, int lineZero, int colZero) {
if (!buf) return;
int pos = byteOffsetForLineCol(buf->editBuf, lineZero, colZero);
buf->widget.setCursor(pos);
buf->cursorLine = lineZero + 1;
buf->cursorCol = colZero + 1;
}
void 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();
}
std::string 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;
}
}
void createBuffer(const std::string& path, const std::string& content,
const std::string& language,
BufferManager::BufferMode mode = BufferManager::BufferMode::Structured) {
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->mode.setLanguage(language);
state->generatedMode.setLanguage(language);
state->editor.setContent(content, language);
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, mode);
state->bufferMode = mode;
activeBuffer = state.get();
bufferStates[path] = std::move(state);
active()->orchestratorDirty = true;
recordUndoSnapshot();
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
if (lsp && path.rfind("(untitled", 0) != 0) {
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
}
symbolsPending = true;
symbolsLastChange = ImGui::GetTime();
if (lsp) lsp->clearDocumentSymbols();
}
bool jumpToDefinitionLocation(const LSPClient::DefinitionLocation& loc) {
if (loc.uri.empty()) return false;
std::string path = fromFileUri(loc.uri);
if (path.empty()) return false;
if (buffers.hasBuffer(path)) switchToBuffer(path);
else doOpen(path);
jumpTo(active(), loc.range.start.line, loc.range.start.character);
return true;
}
bool goToDefinitionFallback(int lineZero, int colZero) {
if (!isStructured()) return false;
Module* ast = activeAST();
if (!ast || !active()) return false;
std::string word = wordAtLineCol(active()->editBuf, lineZero, colZero);
if (word.empty()) return false;
ASTNode* scopeNode = findNodeAtPosition(ast, lineZero, colZero);
if (!scopeNode) scopeNode = ast;
ContextAPI ctx;
ctx.setRoot(ast);
auto symbols = ctx.getInScopeSymbols(scopeNode->id);
for (const auto& sym : symbols) {
if (sym.name != word) continue;
ASTNode* target = findNodeById(ast, sym.nodeId);
if (target && target->hasSpan()) {
jumpTo(active(), target->spanStartLine, target->spanStartCol);
return true;
}
}
return false;
}
bool goToDefinitionAt(int lineZero, int colZero) {
if (!active()) return false;
if (lsp && lspTransport && lspTransport->isOpen() &&
active()->path.rfind("(untitled", 0) != 0) {
definitionPreviewValid = false;
definitionPending = true;
definitionLastRequest = ImGui::GetTime();
definitionRequestLine = lineZero;
definitionRequestCol = colZero;
definitionRequestPath = active()->path;
lsp->clearDefinitionLocations();
lsp->requestDefinition(toFileUri(active()->path), lineZero, colZero);
return true;
}
return goToDefinitionFallback(lineZero, colZero);
}
bool previewDefinitionAt(int lineZero, int colZero, std::string& outLabel) {
if (!isStructured() || !active()) return false;
Module* ast = activeAST();
if (!ast) return false;
std::string word = wordAtLineCol(active()->editBuf, lineZero, colZero);
if (word.empty()) return false;
ASTNode* scopeNode = findNodeAtPosition(ast, lineZero, colZero);
if (!scopeNode) scopeNode = ast;
ContextAPI ctx;
ctx.setRoot(ast);
auto symbols = ctx.getInScopeSymbols(scopeNode->id);
for (const auto& sym : symbols) {
if (sym.name != word) continue;
ASTNode* target = findNodeById(ast, sym.nodeId);
if (target && target->hasSpan()) {
outLabel = word + " -> line " + std::to_string(target->spanStartLine + 1);
return true;
}
}
return false;
}
void switchToBuffer(const std::string& path) {
if (!buffers.hasBuffer(path)) return;
buffers.switchToBuffer(path);
activeBuffer = bufferStates[path].get();
if (active()) active()->orchestratorDirty = true;
symbolsPending = true;
symbolsLastChange = ImGui::GetTime();
if (lsp) lsp->clearDocumentSymbols();
}
std::filesystem::path 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";
}
std::filesystem::path recentFilePath() const {
return configDir() / "recent.json";
}
void 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);
}
}
void 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);
}
void 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;
}
bool 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;
}
}
bool 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;
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;
}
}
std::filesystem::path sessionFilePath() const {
return configDir() / "session.json";
}
std::filesystem::path settingsFilePath() const {
return configDir() / "settings.json";
}
bool saveSession(const std::string& imguiIni) {
SessionData session;
session.workspaceRoot = workspaceRoot;
session.activePath = active() ? active()->path : "";
session.layoutPreset = LayoutManager::presetName(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;
}
}
bool 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;
}
}
void applySession(const SessionData& session) {
closeAllBuffers();
if (!session.workspaceRoot.empty()) {
workspaceRoot = session.workspaceRoot;
fileTreeDirty = true;
projectSearch.setRoot(workspaceRoot);
}
layoutPreset = LayoutManager::presetFromName(session.layoutPreset);
for (const auto& buf : session.buffers) {
doOpen(buf.path, bufferModeFromString(buf.mode));
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);
}
}
void applyTabSizeToBuffers(int size) {
for (auto& kv : bufferStates) {
kv.second->mode.setTabSize(size);
kv.second->generatedMode.setTabSize(size);
}
}
void applySettingsToState() {
showMinimap = settings.getShowMinimap();
showLineNumbers = settings.getShowLineNumbers();
layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset());
keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile()));
registerCommands();
applyTabSizeToBuffers(settings.getTabSize());
}
void loadSettingsFromDisk() {
settings.loadFromFile(settingsFilePath().string());
applySettingsToState();
}
void saveSettingsToDisk() {
settings.setShowMinimap(showMinimap);
settings.setShowLineNumbers(showLineNumbers);
settings.setLayoutPreset(LayoutManager::presetName(layoutPreset));
settings.setKeybindingProfile(KeybindingManager::profileName(keys.getProfile()));
std::filesystem::create_directories(settingsFilePath().parent_path());
settings.saveToFile(settingsFilePath().string());
}
void init() {
outputLog = "Whetstone Editor ready.\n";
workspaceRoot = std::filesystem::current_path().string();
projectSearch.setRoot(workspaceRoot);
fileTreeDirty = true;
loadRecentFiles();
loadSettingsFromDisk();
registerCommands();
initAgentServer();
refreshBuildSystem();
}
void initAgentServer() {
auto transport = std::make_unique<MockWebSocketTransport>();
agentTransport = transport.get();
agentServer = std::make_unique<WebSocketAgentServer>(std::move(transport));
agentServer->setRequestHandler([this](const json& request,
const std::string& sessionId) {
return processAgentRequest(request, sessionId);
});
agentServer->setSessionEventCallback([this](const AgentSession& s,
const std::string& event) {
logAgentEvent("Agent " + s.sessionId + " " + event);
if (event == "connected") {
agentMutationPermissions[s.sessionId] = false;
} else if (event == "disconnected") {
agentMutationPermissions.erase(s.sessionId);
}
});
agentServer->setRequestLogCallback([this](const std::string& sid,
const json& req,
const json& res) {
std::string method = req.value("method", "");
bool ok = !res.contains("error");
logAgentEvent("RPC " + sid + " " + method + (ok ? " ok" : " error"));
});
if (agentServer->start(agentPort)) {
logAgentEvent("Agent server started on port " + std::to_string(agentPort));
} else {
logAgentEvent("Agent server failed to start on port " + std::to_string(agentPort));
}
}
void shutdownAgentServer() {
if (agentServer) agentServer->stop();
}
void logAgentEvent(const std::string& msg) {
agentLog.push_back(msg);
}
void refreshBuildSystem() {
buildType = BuildSystem::detect(workspaceRoot);
}
int runBuildCommand(const BuildCommand& cmd) {
if (workspaceRoot.empty()) {
terminal.append("[build] No workspace root set.\n");
return -1;
}
showTerminalPanel = true;
lastBuildCommand = cmd.command;
int code = terminal.runAndCapture(workspaceRoot, cmd.command, lastBuildOutput);
buildErrors = BuildSystem::parseErrors(lastBuildOutput);
outputLog += "[build] " + cmd.label + " => exit " + std::to_string(code) + "\n";
return code;
}
json buildDiagnosticsJson() const {
json out;
json lspArr = json::array();
auto diags = lsp ? lsp->getDiagnostics() : std::vector<LSPClient::Diagnostic>{};
for (const auto& d : diags) {
lspArr.push_back({
{"uri", d.uri},
{"message", d.message},
{"severity", d.severity},
{"range", {
{"start", {{"line", d.range.start.line}, {"character", d.range.start.character}}},
{"end", {{"line", d.range.end.line}, {"character", d.range.end.character}}}
}}
});
}
json whetArr = json::array();
for (const auto& d : whetstoneDiagnostics) {
whetArr.push_back({
{"uri", d.uri},
{"message", d.message},
{"severity", d.severity},
{"line", d.line},
{"character", d.character}
});
}
out["lsp"] = lspArr;
out["whetstone"] = whetArr;
return out;
}
json processAgentRequest(const json& request, const std::string& sessionId) {
json response;
response["jsonrpc"] = "2.0";
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
std::string method = request.value("method", "");
if (method == "getAST") {
if (!active() || !isStructured()) {
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
return response;
}
Module* ast = activeAST();
if (!ast) {
response["error"] = {{"code", -32001}, {"message", "AST unavailable"}};
return response;
}
response["result"] = {
{"ast", toJson(ast)},
{"annotationCount", countAnnotationNodes(ast)},
{"diagnostics", buildDiagnosticsJson()}
};
return response;
}
if (method == "applyMutation") {
auto permIt = agentMutationPermissions.find(sessionId);
if (permIt == agentMutationPermissions.end() || !permIt->second) {
response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}};
return response;
}
if (!active() || !isStructured()) {
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
return response;
}
auto params = request.contains("params") ? request["params"] : json::object();
std::string type = params.value("type", "");
Module* ast = mutationAST();
if (!ast) {
response["error"] = {{"code", -32001}, {"message", "AST unavailable"}};
return response;
}
ASTMutationAPI mut;
mut.setRoot(ast);
ASTMutationAPI::MutationResult res;
if (type == "setProperty") {
res = mut.setProperty(params.value("nodeId", ""),
params.value("property", ""),
params.value("value", ""));
} else if (type == "updateNode") {
std::map<std::string, std::string> props;
if (params.contains("properties")) {
for (auto& [k, v] : params["properties"].items()) {
props[k] = v.get<std::string>();
}
}
res = mut.updateNode(params.value("nodeId", ""), props);
} else if (type == "deleteNode") {
res = mut.deleteNode(params.value("nodeId", ""));
} else if (type == "insertNode") {
ASTNode* node = nullptr;
if (params.contains("node")) {
node = fromJson(params["node"]);
}
res = mut.insertNode(params.value("parentId", ""),
params.value("role", ""), node);
if (!res.success && node) {
deleteTree(node);
}
} else {
response["error"] = {{"code", -32602}, {"message", "Unknown mutation type"}};
return response;
}
if (!res.success) {
response["error"] = {{"code", -32010}, {"message", res.error}};
return response;
}
applyOrchestratorToActive();
response["result"] = {
{"success", true},
{"warning", res.warning}
};
return response;
}
response["error"] = {{"code", -32601}, {"message", "Method not found"}};
return response;
}
void syncOrchestratorFromActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = active()->sync.getAST();
if (!ast) return;
active()->orchestrator.setAST(cloneModule(ast));
active()->orchestratorDirty = false;
}
void applyOrchestratorToActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = active()->orchestrator.getAST();
if (!ast) return;
active()->sync.setAST(cloneModule(ast));
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
refreshActiveTextFromAST();
active()->orchestratorDirty = false;
recordUndoSnapshot();
}
Module* mutationAST() {
if (!active()) return nullptr;
if (!isStructured()) return nullptr;
if (active()->orchestratorDirty || !active()->orchestrator.getAST()) {
syncOrchestratorFromActive();
}
return active()->orchestrator.getAST();
}
void recordUndoSnapshot() {
if (!active()) return;
Module* ast = isStructured() ? active()->sync.getAST() : nullptr;
active()->orchestrator.recordSnapshot(active()->editBuf, ast);
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void applySnapshotToActive(const std::string& text, std::unique_ptr<Module> ast) {
if (!active()) return;
active()->editBuf = text;
active()->editor.setContent(active()->editBuf, active()->language);
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
if (ast) {
active()->orchestrator.setAST(cloneModule(ast.get()));
active()->sync.setAST(std::move(ast));
} else {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->orchestrator.setAST(cloneModule(active()->sync.getAST()));
}
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
active()->orchestratorDirty = false;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
}
void registerCommand(const std::string& id,
const std::string& label,
const std::string& shortcut,
std::function<void()> fn) {
commandPalette.registerCommand(id, label, shortcut);
commandHandlers[id] = std::move(fn);
}
void registerCommands() {
commandPalette = CommandPalette();
commandHandlers.clear();
registerCommand("file.new", "File: New File",
keys.getBinding("file.new").toString(),
[this]() {
std::string lang = active() ? active()->language : "python";
createBuffer(makeUntitledName(), "", lang);
});
registerCommand("file.open", "File: Open...",
keys.getBinding("file.open").toString(),
[this]() {
std::string path = FileDialog::openFile(
{"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"},
std::string()});
if (!path.empty()) doOpen(path);
});
registerCommand("file.save", "File: Save",
keys.getBinding("file.save").toString(),
[this]() { doSave(); });
registerCommand("project.open", "Project: Open...",
"",
[this]() {
std::string path = FileDialog::openFile(
{"Open Project", {"*.whetstone"}, std::string()});
if (!path.empty()) {
if (!loadProject(path)) {
outputLog += "Failed to open project: " + path + "\n";
}
}
});
registerCommand("project.save", "Project: Save...",
"",
[this]() {
std::string path = FileDialog::saveFile(
{"Save Project", {"*.whetstone"}, std::string()});
if (!path.empty()) {
if (!saveProject(path)) {
outputLog += "Failed to save project: " + path + "\n";
}
}
});
registerCommand("edit.undo", "Edit: Undo",
keys.getBinding("edit.undo").toString(),
[this]() { doUndo(); });
registerCommand("edit.redo", "Edit: Redo",
keys.getBinding("edit.redo").toString(),
[this]() { doRedo(); });
registerCommand("search.find", "Search: Find/Replace",
keys.getBinding("search.find").toString(),
[this]() { showFind = !showFind; });
registerCommand("search.findInFiles", "Search: Find in Files",
keys.getBinding("search.findInFiles").toString(),
[this]() { showProjectSearch = !showProjectSearch; });
registerCommand("nav.goToLine", "Navigate: Go to Line",
keys.getBinding("nav.goToLine").toString(),
[this]() {
showGoToLine = true;
goToLineBuf[0] = '\0';
goToLineError = false;
});
registerCommand("view.whitespace", "View: Toggle Whitespace", "",
[this]() { showWhitespace = !showWhitespace; });
registerCommand("view.minimap", "View: Toggle Minimap", "",
[this]() { showMinimap = !showMinimap; });
registerCommand("view.annotations", "View: Toggle Annotations", "",
[this]() { showAnnotations = !showAnnotations; });
registerCommand("view.outline", "View: Toggle Outline", "",
[this]() { showOutline = !showOutline; });
registerCommand("view.toggleTerminal", "View: Toggle Terminal",
keys.getBinding("view.toggleTerminal").toString(),
[this]() { showTerminalPanel = !showTerminalPanel; });
registerCommand("view.lsp", "View: LSP Servers...", "",
[this]() { showLspSettings = !showLspSettings; });
registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "",
[this]() {
if (!active()) return;
active()->bufferMode = active()->bufferMode == BufferManager::BufferMode::Text ?
BufferManager::BufferMode::Structured : BufferManager::BufferMode::Text;
buffers.setBufferMode(active()->path, active()->bufferMode);
if (active()->bufferMode == BufferManager::BufferMode::Text) {
suggestions.clear();
whetstoneDiagnostics.clear();
analysisPending = false;
} else {
onTextChanged();
}
});
registerCommand("refactor.rename", "Refactor: Rename Variable", "",
[this]() {
refactorAction = 1;
showRefactorPopup = true;
refactorNameA[0] = '\0';
refactorNameB[0] = '\0';
});
registerCommand("refactor.extract", "Refactor: Extract Function", "",
[this]() {
refactorAction = 2;
showRefactorPopup = true;
refactorNameA[0] = '\0';
refactorNameB[0] = '\0';
});
registerCommand("refactor.inline", "Refactor: Inline Variable", "",
[this]() {
refactorAction = 3;
showRefactorPopup = true;
refactorNameA[0] = '\0';
refactorNameB[0] = '\0';
});
registerCommand("build.run", "Build: Run", keys.getBinding("build.run").toString(),
[this]() { runActiveFile(false); });
registerCommand("build.build", "Build: Build", keys.getBinding("build.build").toString(),
[this]() { runActiveFile(true); });
}
void executeCommand(const std::string& id) {
auto it = commandHandlers.find(id);
if (it == commandHandlers.end()) return;
it->second();
commandPalette.markUsed(id);
}
void setLanguage(const std::string& lang) {
if (!active()) return;
std::string prevLang = active()->language;
active()->language = lang;
active()->mode.setLanguage(lang);
if (active()->generatedLanguage == prevLang) {
active()->generatedLanguage = lang;
active()->generatedMode.setLanguage(lang);
active()->generatedHighlightsDirty = true;
}
active()->editor.setContent(active()->editBuf, lang);
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
active()->sync.setText(active()->editBuf, lang);
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
active()->orchestratorDirty = true;
active()->highlightsDirty = true;
}
std::string buildRunCommand(const std::string& path,
const std::string& language,
bool buildOnly) const {
std::filesystem::path p(path);
std::string ext = p.extension().string();
std::string quoted = "\"" + path + "\"";
if (language == "python" || ext == ".py") {
return buildOnly ? "python -m py_compile " + quoted : "python " + quoted;
}
if (language == "cpp" || ext == ".cpp" || ext == ".cc" || ext == ".cxx") {
std::string out = (p.parent_path() / p.stem()).string();
#ifdef _WIN32
out += ".exe";
#endif
std::string compile = "g++ " + quoted + " -std=c++20 -o \"" + out + "\"";
if (buildOnly) return compile;
return compile + " && \"" + out + "\"";
}
if (language == "rust" || ext == ".rs") {
return buildOnly ? "cargo build" : "cargo run";
}
if (language == "go" || ext == ".go") {
return buildOnly ? "go build " + quoted : "go run " + quoted;
}
if (language == "javascript" || ext == ".js") {
return buildOnly ? "node --check " + quoted : "node " + quoted;
}
if (language == "typescript" || ext == ".ts") {
return buildOnly ? "tsc " + quoted : "node " + quoted;
}
return "";
}
bool runActiveFile(bool buildOnly) {
if (!active()) return false;
if (active()->path.rfind("(untitled", 0) == 0) {
terminal.append("[terminal] Save the file before running.\n");
showTerminalPanel = true;
return false;
}
if (active()->modified) {
doSave();
}
std::string cmd = buildRunCommand(active()->path, active()->language, buildOnly);
if (cmd.empty()) {
terminal.append("[terminal] No runner for language: " + active()->language + "\n");
showTerminalPanel = true;
return false;
}
std::string cwd = workspaceRoot.empty()
? std::filesystem::path(active()->path).parent_path().string()
: workspaceRoot;
showTerminalPanel = true;
runInProgress = true;
hasRunResult = false;
lastRunCommand = cmd;
int code = terminal.runAndAppend(cwd, cmd);
terminal.append("[exit code: " + std::to_string(code) + "]\n");
runInProgress = false;
hasRunResult = true;
lastRunExitCode = code;
return code == 0;
}
// Called after editBuf changes (from ImGui input)
void onTextChanged() {
if (!active()) return;
active()->editor.setContent(active()->editBuf, active()->language);
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
active()->orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
symbolsPending = true;
symbolsLastChange = ImGui::GetTime();
if (lsp) lsp->clearDocumentSymbols();
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
recordUndoSnapshot();
}
void doUndo() {
if (!active()) return;
std::string text;
std::unique_ptr<Module> ast;
if (active()->orchestrator.undoSnapshot(text, ast)) {
applySnapshotToActive(text, std::move(ast));
}
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void doRedo() {
if (!active()) return;
std::string text;
std::unique_ptr<Module> ast;
if (active()->orchestrator.redoSnapshot(text, ast)) {
applySnapshotToActive(text, std::move(ast));
}
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void doFind() {
if (!active()) return;
if (strlen(findBuf) == 0) return;
int pos = active()->editor.find(findBuf, lastFindPos);
if (pos >= 0) {
lastFindPos = pos + 1;
int line = 1, col = 1;
for (int i = 0; i < pos && i < (int)active()->editBuf.size(); ++i) {
if (active()->editBuf[i] == '\n') { ++line; col = 1; } else { ++col; }
}
active()->cursorLine = line;
active()->cursorCol = col;
outputLog += "Found \"" + std::string(findBuf) + "\" at line " +
std::to_string(line) + ", col " + std::to_string(col) + "\n";
} else {
lastFindPos = 0;
outputLog += "\"" + std::string(findBuf) + "\" not found.\n";
}
}
void doReplaceAll() {
if (!active()) return;
if (strlen(findBuf) == 0) return;
int count = active()->editor.replaceAll(findBuf, replaceBuf);
if (count > 0) {
active()->editBuf = active()->editor.getContent();
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
}
outputLog += "Replaced " + std::to_string(count) + " occurrence(s).\n";
}
void 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;
outputLog += "Saved: " + active()->path + "\n";
if (lsp) lsp->didSave(toFileUri(active()->path), active()->editBuf);
} else {
outputLog += "Error saving: " + active()->path + "\n";
}
}
void doOpen(const std::string& path,
BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured) {
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";
createBuffer(path, content, language, modeOverride);
welcome.addRecentFile(path, language, bufferModeToString(modeOverride));
saveRecentFiles();
outputLog += "Opened: " + path + "\n";
} else {
outputLog += "Error opening: " + path + "\n";
}
}
void 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->generatedHighlightsDirty = true;
buf->modified = false;
outputLog += "Reloaded: " + path + "\n";
}
void handleFileChanges() {
auto changed = watcher.poll();
for (const auto& path : changed) {
auto it = bufferStates.find(path);
if (it == bufferStates.end()) continue;
if (it->second->modified) {
outputLog += "File changed on disk (dirty): " + path + "\n";
} else {
reloadBuffer(path);
}
}
}
void updateHighlights() {
if (!active()) return;
if (!active()->highlightsDirty) return;
active()->highlights = SyntaxHighlighter::highlight(active()->editBuf, active()->language);
active()->highlightsDirty = false;
}
void 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()->generatedHighlightsDirty) {
active()->generatedHighlights =
SyntaxHighlighter::highlight(active()->generatedBuf, active()->generatedLanguage);
active()->generatedHighlightsDirty = false;
}
}
void projectToLanguage(const std::string& targetLanguage) {
if (!active()) return;
Module* ast = activeAST();
if (!ast) {
outputLog += "Project to " + targetLanguage + ": no AST available.\n";
return;
}
CrossLanguageProjector projector;
auto projected = projector.project(ast, targetLanguage);
if (!projected) {
outputLog += "Project to " + targetLanguage + ": failed to project AST.\n";
return;
}
const int srcAnnoCount = countAnnotationNodes(ast);
const int projAnnoCount = countAnnotationNodes(projected.get());
const bool preserved = projector.annotationsPreserved(ast, projected.get());
std::string generated = generateForLanguage(projected.get(), targetLanguage);
std::string baseName = "(untitled-projection:" + targetLanguage + ")";
std::string projName = baseName;
int suffix = 1;
while (buffers.hasBuffer(projName)) {
projName = baseName + "-" + std::to_string(suffix++);
}
createBuffer(projName, generated, targetLanguage);
if (active()) {
active()->readOnly = true;
active()->sync.setText(generated, targetLanguage);
active()->sync.setAST(std::move(projected));
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
active()->editBuf = generated;
active()->editor.setContent(active()->editBuf, targetLanguage);
active()->mode.setLanguage(targetLanguage);
active()->highlightsDirty = true;
active()->generatedLanguage = targetLanguage;
active()->generatedMode.setLanguage(targetLanguage);
active()->generatedHighlightsDirty = true;
active()->modified = false;
}
outputLog += "Projected to " + targetLanguage + " in " + projName +
" (annotations " + std::to_string(srcAnnoCount) + " -> " +
std::to_string(projAnnoCount) + ", types preserved: " +
(preserved ? "yes" : "no") + ").\n";
}
void refreshActiveTextFromAST() {
if (!active()) return;
if (!isStructured()) return;
active()->editBuf = active()->sync.getText();
active()->editor.setContent(active()->editBuf, active()->language);
active()->highlightsDirty = true;
active()->modified = true;
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
analysisPending = true;
analysisLastChange = ImGui::GetTime();
}
void openDiff(const std::string& beforeText,
const std::string& afterText,
bool preview,
int action,
const std::vector<std::string>& transformIds,
const std::vector<BatchMutationAPI::Mutation>& batchMutations = {}) {
diff.active = true;
diff.preview = preview;
diff.action = action;
diff.batch = !batchMutations.empty();
diff.beforeText = beforeText;
diff.afterText = afterText;
diff.transformIds = transformIds;
diff.batchMutations = batchMutations;
LineDiff lineDiff = buildLineDiff(beforeText, afterText);
diff.beforeLines = std::move(lineDiff.beforeLines);
diff.afterLines = std::move(lineDiff.afterLines);
}
void 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; }
}
}
void refreshFileTree() {
if (!fileTreeDirty) return;
fileTreeRoot = fileTree.build(workspaceRoot);
fileTreeDirty = false;
}
};
struct NullLSPTransport : public LSPTransport {
void send(const std::string& msg) override { (void)msg; }
bool receive(std::string& out) override { (void)out; return false; }
bool isOpen() const override { return false; }
void close() override {}
};