Files
whetstone_DSL/editor/src/EditOps.h
Bill 976161dc4a 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

508 lines
18 KiB
C++

#pragma once
// --- EditOps.h ---
// Extracted from EditorState.h (Sprint 8, Step 237).
// Editing operations, undo/redo, find/replace, commands, navigation.
// Included from EditorState.h after the EditorState struct definition.
inline bool EditorState::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, defaultBufferMode());
jumpTo(active(), loc.range.start.line, loc.range.start.character);
return true;
}
inline bool EditorState::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;
}
inline bool EditorState::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);
}
inline bool EditorState::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;
}
inline void EditorState::applyTabSizeToBuffers(int size) {
for (auto& kv : bufferStates) {
kv.second->mode.setTabSize(size);
kv.second->generatedMode.setTabSize(size);
}
}
inline void EditorState::setActiveBufferMode(BufferManager::BufferMode mode) {
if (!active()) return;
if (active()->bufferMode == mode) return;
active()->bufferMode = mode;
buffers.setBufferMode(active()->path, active()->bufferMode);
if (active()->bufferMode == BufferManager::BufferMode::Text) {
suggestions.clear();
whetstoneDiagnostics.clear();
analysisPending = false;
} else {
onTextChanged();
}
}
inline void EditorState::queueLspDidChange() {
if (!lsp || !active()) return;
if (active()->path.rfind("(untitled", 0) == 0) return;
active()->lspVersion += 1;
lspChangePending = true;
lspChangeLast = ImGui::GetTime();
lspChangePath = active()->path;
}
inline void EditorState::syncOrchestratorFromActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = active()->sync.getAST();
if (!ast) return;
active()->orchestrator.setAST(cloneModule(ast));
active()->orchestratorDirty = false;
}
inline void EditorState::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();
}
inline Module* EditorState::mutationAST() {
if (!active()) return nullptr;
if (!isStructured()) return nullptr;
if (active()->orchestratorDirty || !active()->orchestrator.getAST()) {
syncOrchestratorFromActive();
}
return active()->orchestrator.getAST();
}
inline void EditorState::recordUndoSnapshot() {
if (!active()) return;
Module* ast = isStructured() ? active()->sync.getAST() : nullptr;
active()->orchestrator.recordSnapshot(active()->editBuf, ast);
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
inline void EditorState::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()->highlightRequestTime = ImGui::GetTime();
active()->generatedHighlightsDirty = true;
active()->modified = true;
queueLspDidChange();
}
// --- Command registration (extracted) ---
#include "EditOpsCommands.h"
inline void EditorState::setLanguage(const std::string& lang) {
if (!active()) return;
std::string prevLang = active()->language;
active()->language = lang;
active()->mode.setLanguage(lang);
if (activeAST()) {
library.primitives.setRoot(activeAST());
}
library.primitives.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;
if (lang == "elisp") {
emacsState.emacsFunctionIndexDirty = true;
}
if (lang == "org") {
active()->bufferMode = BufferManager::BufferMode::Text;
buffers.setBufferMode(active()->path, active()->bufferMode);
}
events.publishDebounced(UIEventType::ASTChanged,
active()->path,
{},
ImGui::GetTime(),
0.1);
}
inline void EditorState::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()->highlightRequestTime = ImGui::GetTime();
active()->generatedHighlightsDirty = true;
active()->modified = true;
queueLspDidChange();
events.publish(UIEventType::FileModified, active()->path, {}, ImGui::GetTime());
events.publishDebounced(UIEventType::ASTChanged,
active()->path,
{},
ImGui::GetTime(),
0.15);
recordUndoSnapshot();
}
inline void EditorState::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();
}
inline void EditorState::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();
}
inline SearchOptions EditorState::currentSearchOptions() const {
SearchOptions opts;
opts.matchCase = search.matchCase;
opts.wholeWord = search.wholeWord;
opts.useRegex = search.useRegex;
return opts;
}
inline void EditorState::addSearchHistory(const std::string& query) {
if (query.empty()) return;
auto it = std::find(search.findHistory.begin(), search.findHistory.end(), query);
if (it != search.findHistory.end()) {
search.findHistory.erase(it);
}
search.findHistory.insert(search.findHistory.begin(), query);
if (search.findHistory.size() > 10) {
search.findHistory.resize(10);
}
}
inline bool EditorState::selectionRange(int& outStart, int& outEnd) {
if (!active()) return false;
if (!active()->widget.hasSelectionRange()) return false;
active()->widget.getSelectionRange(outStart, outEnd);
return outStart >= 0 && outEnd > outStart;
}
inline void EditorState::refreshSearchMatches() {
search.matches.clear();
search.currentMatchIndex = -1;
if (!active()) return;
std::string query = search.findBuf;
if (query.empty()) return;
int rangeStart = 0;
int rangeEnd = (int)active()->editBuf.size();
if (search.findInSelection) {
int selStart = -1;
int selEnd = -1;
if (selectionRange(selStart, selEnd)) {
rangeStart = selStart;
rangeEnd = selEnd;
}
}
search.matches = SearchUtils::collectMatches(active()->editBuf,
query,
currentSearchOptions(),
rangeStart,
rangeEnd);
}
inline bool EditorState::moveToMatchIndex(int index) {
if (!active()) return false;
if (index < 0 || index >= (int)search.matches.size()) return false;
const auto& match = search.matches[index];
active()->widget.setSelectionRange(match.start, match.end);
active()->widget.setCursor(match.end);
updateCursorPos(match.start);
search.currentMatchIndex = index;
search.lastFindPos = match.end;
search.pulseLine = match.line;
search.pulseStart = ImGui::GetTime();
return true;
}
inline void EditorState::doFindNext(bool backwards) {
if (!active()) return;
if (strlen(search.findBuf) == 0) return;
refreshSearchMatches();
if (search.matches.empty()) {
search.lastFindPos = 0;
search.pulseLine = -1;
search.pulseStart = 0.0;
notify(NotificationLevel::Warning,
"\"" + std::string(search.findBuf) + "\" not found.");
return;
}
int cursorPos = active()->widget.getCursor();
int chosenIndex = -1;
if (backwards) {
for (int i = (int)search.matches.size() - 1; i >= 0; --i) {
if (search.matches[i].end < cursorPos) {
chosenIndex = i;
break;
}
}
if (chosenIndex < 0) chosenIndex = (int)search.matches.size() - 1;
} else {
for (int i = 0; i < (int)search.matches.size(); ++i) {
if (search.matches[i].start > cursorPos) {
chosenIndex = i;
break;
}
}
if (chosenIndex < 0) chosenIndex = 0;
}
if (moveToMatchIndex(chosenIndex)) {
addSearchHistory(search.findBuf);
const auto& m = search.matches[chosenIndex];
NotificationTarget target;
target.path = active()->path;
target.line = m.line;
target.col = m.col;
notify(NotificationLevel::Info,
"Found \"" + std::string(search.findBuf) + "\" at line " +
std::to_string(m.line + 1) + ", col " + std::to_string(m.col + 1),
target);
}
}
inline void EditorState::doReplaceCurrent() {
if (!active()) return;
if (strlen(search.findBuf) == 0) return;
refreshSearchMatches();
if (search.matches.empty()) {
notify(NotificationLevel::Warning,
"\"" + std::string(search.findBuf) + "\" not found.");
return;
}
int index = search.currentMatchIndex;
if (index < 0 || index >= (int)search.matches.size()) {
int cursorPos = active()->widget.getCursor();
for (int i = 0; i < (int)search.matches.size(); ++i) {
if (search.matches[i].start >= cursorPos) {
index = i;
break;
}
}
if (index < 0) index = 0;
}
const auto& match = search.matches[index];
std::string replacement = SearchUtils::applyReplacement(match.matchText,
search.findBuf,
search.replaceBuf,
currentSearchOptions());
active()->editBuf.replace(match.start, match.end - match.start, replacement);
onTextChanged();
active()->widget.setCursor(match.start + (int)replacement.size());
updateCursorPos(match.start + (int)replacement.size());
addSearchHistory(search.findBuf);
}
inline void EditorState::doFind() {
doFindNext(false);
}
inline void EditorState::doReplaceAll() {
if (!active()) return;
if (strlen(search.findBuf) == 0) return;
int rangeStart = 0;
int rangeEnd = (int)active()->editBuf.size();
if (search.findInSelection) {
int selStart = -1;
int selEnd = -1;
if (selectionRange(selStart, selEnd)) {
rangeStart = selStart;
rangeEnd = selEnd;
}
}
int count = 0;
std::string replaced = SearchUtils::replaceAll(active()->editBuf,
search.findBuf,
search.replaceBuf,
currentSearchOptions(),
rangeStart,
rangeEnd,
count);
if (count > 0) {
active()->editBuf = replaced;
onTextChanged();
}
addSearchHistory(search.findBuf);
notify(NotificationLevel::Info,
"Replaced " + std::to_string(count) + " occurrence(s).");
}
inline std::string EditorState::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;
}
inline int EditorState::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;
}
inline int EditorState::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;
}
inline std::string EditorState::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);
}
inline std::string EditorState::wordAtLineCol(const std::string& text, int lineZero, int colZero) {
int pos = byteOffsetForLineCol(text, lineZero, colZero);
return wordAt(text, pos);
}
inline std::string EditorState::wordPrefixAt(const std::string& text, int pos) {
int start = wordStartAt(text, pos);
return text.substr(start, pos - start);
}
inline std::string EditorState::uiEventLabel(UIEventType type) {
switch (type) {
case UIEventType::ASTChanged: return "ast_changed";
case UIEventType::BufferSwitched: return "buffer_switched";
case UIEventType::DiagnosticsUpdated: return "diagnostics_updated";
case UIEventType::ThemeChanged: return "theme_changed";
case UIEventType::SettingsChanged: return "settings_changed";
case UIEventType::FileModified: return "file_modified";
case UIEventType::AgentConnected: return "agent_connected";
case UIEventType::NotificationPosted: return "notification_posted";
}
return "unknown";
}