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>
This commit is contained in:
@@ -56,7 +56,11 @@ struct AgentPermissionPolicy {
|
||||
method == "indexWorkspace" ||
|
||||
method == "getImportGraph" ||
|
||||
method == "getProjectDiagnostics" ||
|
||||
method == "searchProject") {
|
||||
method == "searchProject" ||
|
||||
method == "loadAnnotatedAST" ||
|
||||
method == "listAnnotatedFiles" ||
|
||||
method == "getSemanticAnnotations" ||
|
||||
method == "getUnannotatedNodes") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,7 +78,10 @@ struct AgentPermissionPolicy {
|
||||
method == "saveBuffer" ||
|
||||
method == "saveAllBuffers" ||
|
||||
method == "undo" ||
|
||||
method == "redo") {
|
||||
method == "redo" ||
|
||||
method == "saveAnnotatedAST" ||
|
||||
method == "setSemanticAnnotation" ||
|
||||
method == "removeSemanticAnnotation") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ inline json handleAgentRequest(EditorState& state, const json& request,
|
||||
{"nodeId", d.nodeId}});
|
||||
json violArr = json::array();
|
||||
for (const auto& v : pr.violations)
|
||||
violArr.push_back({{"type", v.type}, {"message", v.message},
|
||||
violArr.push_back({{"severity", v.severity}, {"category", v.category}, {"message", v.message},
|
||||
{"nodeId", v.nodeId}});
|
||||
json suggArr = json::array();
|
||||
for (const auto& s : pr.suggestions)
|
||||
@@ -420,8 +420,8 @@ inline json handleAgentRequest(EditorState& state, const json& request,
|
||||
{"success", pr.success}, {"generatedCode", pr.generatedCode},
|
||||
{"parseDiagnostics", diagArr}, {"validationDiagnostics", valDiagArr},
|
||||
{"violations", violArr}, {"suggestions", suggArr},
|
||||
{"foldCount", pr.foldResult.transformCount},
|
||||
{"dceCount", pr.dceResult.transformCount}
|
||||
{"foldCount", pr.foldResult.nodesModified},
|
||||
{"dceCount", pr.dceResult.nodesModified}
|
||||
};
|
||||
if (pr.ast) result["ast"] = toJson(pr.ast.get());
|
||||
return agentRpcResult(id, result);
|
||||
|
||||
@@ -20,11 +20,11 @@ inline BufferManager::BufferMode EditorState::defaultBufferMode() const {
|
||||
|
||||
inline void EditorState::createBuffer(const std::string& path, const std::string& content,
|
||||
const std::string& language,
|
||||
BufferManager::BufferMode mode = BufferManager::BufferMode::Text,
|
||||
size_t fileSizeBytes = 0,
|
||||
bool largeFileMode = false,
|
||||
bool disableSyntaxHighlight = false,
|
||||
bool deferAstSync = false) {
|
||||
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)) {
|
||||
@@ -346,8 +346,8 @@ inline void EditorState::doSave() {
|
||||
}
|
||||
|
||||
inline void EditorState::doOpen(const std::string& path,
|
||||
BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Text,
|
||||
bool deferAstSync = false) {
|
||||
BufferManager::BufferMode modeOverride,
|
||||
bool deferAstSync) {
|
||||
std::error_code sizeErr;
|
||||
size_t fileSizeBytes = 0;
|
||||
if (!path.empty()) {
|
||||
@@ -544,4 +544,3 @@ inline void EditorState::insertTextAtCursor(const std::string& text) {
|
||||
// --- Projection and lifecycle helpers (extracted) ---
|
||||
#include "BufferOpsProjection.h"
|
||||
#include "BufferOpsLifecycle.h"
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ inline json EditorState::buildSessionMetadata(bool autoRecording) const {
|
||||
json meta;
|
||||
meta["editorVersion"] = welcome.getVersion();
|
||||
meta["os"] = osLabel();
|
||||
meta["language"] = active() ? active()->language : std::string("unknown");
|
||||
meta["language"] = activeBuffer ? activeBuffer->language : std::string("unknown");
|
||||
meta["projectType"] = detectProjectType();
|
||||
meta["autoRecording"] = autoRecording;
|
||||
return meta;
|
||||
|
||||
@@ -74,7 +74,7 @@ inline void EditorState::openDiff(const std::string& beforeText,
|
||||
bool preview,
|
||||
int action,
|
||||
const std::vector<std::string>& transformIds,
|
||||
const std::vector<BatchMutationAPI::Mutation>& batchMutations = {}) {
|
||||
const std::vector<BatchMutationAPI::Mutation>& batchMutations) {
|
||||
diff.active = true;
|
||||
diff.preview = preview;
|
||||
diff.action = action;
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
return line;
|
||||
}
|
||||
|
||||
static bool isBracketChar(char c) {
|
||||
return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}';
|
||||
}
|
||||
|
||||
static int lineFromMouseY(float mouseY, float baseY, float lineHeight, int lineCount) {
|
||||
int line = (int)((mouseY - baseY) / lineHeight);
|
||||
line = std::max(0, std::min(line, lineCount - 1));
|
||||
@@ -311,4 +315,3 @@
|
||||
collectFoldNodes(ts_node_child(node, i), language, out);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,8 +144,7 @@
|
||||
}
|
||||
if (hoverSuggestion) {
|
||||
std::string tip = suggestion.label + " (" +
|
||||
std::to_string(suggestion.confidence) + ")
|
||||
" +
|
||||
std::to_string(suggestion.confidence) + ")\n" +
|
||||
suggestion.reason;
|
||||
renderRichTooltip("suggestion_marker_" + std::to_string(ln),
|
||||
tip,
|
||||
@@ -314,8 +313,7 @@
|
||||
else if (c == ' ') c = '>';
|
||||
}
|
||||
}
|
||||
if (!chunk.empty() && chunk[chunk.size() - 1] == '
|
||||
') {
|
||||
if (!chunk.empty() && chunk[chunk.size() - 1] == '\n') {
|
||||
chunk.pop_back();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,9 @@ public:
|
||||
}
|
||||
default: { // star (outline)
|
||||
ImVec2 pts[10];
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
float angle = (float)(IM_PI * 0.5 + i * (IM_PI / 5.0));
|
||||
float angle = (float)(kPi * 0.5f + i * (kPi / 5.0f));
|
||||
float radius = (i % 2 == 0) ? size : size * 0.5f;
|
||||
pts[i] = ImVec2(center.x + std::cos(angle) * radius,
|
||||
center.y - std::sin(angle) * radius);
|
||||
|
||||
@@ -35,10 +35,10 @@ public:
|
||||
void registerCommand(const std::string& id,
|
||||
const std::string& label,
|
||||
const std::string& shortcut,
|
||||
const std::string& category,
|
||||
int contextMask,
|
||||
const std::string& inputHint,
|
||||
bool inlineInput) {
|
||||
const std::string& category = "",
|
||||
int contextMask = CommandContext_Any,
|
||||
const std::string& inputHint = "",
|
||||
bool inlineInput = false) {
|
||||
auto& entry = commands_[id];
|
||||
entry.id = id;
|
||||
entry.label = label;
|
||||
@@ -56,8 +56,8 @@ public:
|
||||
}
|
||||
|
||||
std::vector<CommandMatch> search(const std::string& query,
|
||||
int contextMask,
|
||||
bool strictContext) const {
|
||||
int contextMask = CommandContext_Any,
|
||||
bool strictContext = false) const {
|
||||
std::vector<CommandMatch> results;
|
||||
for (const auto& [_, cmd] : commands_) {
|
||||
const bool contextOk = matchesContext(cmd, contextMask);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Annotation.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -13,6 +14,54 @@
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// --- Extract semantic annotation summary from a node's annotations ---
|
||||
// Returns a compact JSON object with semantic fields, or null if none exist.
|
||||
inline json extractSemanticSummary(const ASTNode* node) {
|
||||
if (!node) return json();
|
||||
auto annos = node->getChildren("annotations");
|
||||
if (annos.empty()) return json();
|
||||
json sem;
|
||||
for (const auto* a : annos) {
|
||||
if (a->conceptType == "IntentAnnotation") {
|
||||
auto* ia = static_cast<const IntentAnnotation*>(a);
|
||||
json obj;
|
||||
if (!ia->summary.empty()) obj["summary"] = ia->summary;
|
||||
if (!ia->category.empty()) obj["category"] = ia->category;
|
||||
if (!obj.empty()) sem["intent"] = obj;
|
||||
}
|
||||
else if (a->conceptType == "ComplexityAnnotation") {
|
||||
auto* ca = static_cast<const ComplexityAnnotation*>(a);
|
||||
json obj;
|
||||
if (!ca->timeComplexity.empty()) obj["time"] = ca->timeComplexity;
|
||||
if (ca->cognitiveComplexity > 0) obj["cognitive"] = ca->cognitiveComplexity;
|
||||
if (ca->linesOfLogic > 0) obj["lines"] = ca->linesOfLogic;
|
||||
if (!obj.empty()) sem["complexity"] = obj;
|
||||
}
|
||||
else if (a->conceptType == "RiskAnnotation") {
|
||||
auto* ra = static_cast<const RiskAnnotation*>(a);
|
||||
json obj;
|
||||
if (!ra->level.empty()) obj["level"] = ra->level;
|
||||
if (!ra->reason.empty()) obj["reason"] = ra->reason;
|
||||
if (ra->dependentCount > 0) obj["dependents"] = ra->dependentCount;
|
||||
if (!obj.empty()) sem["risk"] = obj;
|
||||
}
|
||||
else if (a->conceptType == "ContractAnnotation") {
|
||||
auto* ca = static_cast<const ContractAnnotation*>(a);
|
||||
json obj;
|
||||
if (!ca->preconditions.empty()) obj["pre"] = ca->preconditions;
|
||||
if (!ca->postconditions.empty()) obj["post"] = ca->postconditions;
|
||||
if (!ca->returnShape.empty()) obj["returns"] = ca->returnShape;
|
||||
if (!ca->sideEffects.empty()) obj["sideEffects"] = ca->sideEffects;
|
||||
if (!obj.empty()) sem["contract"] = obj;
|
||||
}
|
||||
else if (a->conceptType == "SemanticTagAnnotation") {
|
||||
auto* ta = static_cast<const SemanticTagAnnotation*>(a);
|
||||
if (!ta->tags.empty()) sem["tags"] = ta->tags;
|
||||
}
|
||||
}
|
||||
return sem.empty() ? json() : sem;
|
||||
}
|
||||
|
||||
// --- Extract a human-readable name from any AST node ---
|
||||
inline std::string getNodeName(const ASTNode* node) {
|
||||
if (!node) return "";
|
||||
@@ -70,6 +119,10 @@ inline json toJsonCompact(const ASTNode* node) {
|
||||
if (!name.empty()) j["name"] = name;
|
||||
if (node->hasSpan()) j["line"] = node->spanStartLine;
|
||||
|
||||
// Include semantic summary if annotations exist
|
||||
json sem = extractSemanticSummary(node);
|
||||
if (!sem.empty()) j["semantic"] = sem;
|
||||
|
||||
auto kids = node->allChildren();
|
||||
if (!kids.empty()) {
|
||||
j["childCount"] = (int)kids.size();
|
||||
@@ -105,6 +158,8 @@ inline json toJsonCompactSummary(const ASTNode* root) {
|
||||
std::string cname = getNodeName(child);
|
||||
if (!cname.empty()) cj["name"] = cname;
|
||||
if (child->hasSpan()) cj["line"] = child->spanStartLine;
|
||||
json csem = extractSemanticSummary(child);
|
||||
if (!csem.empty()) cj["semantic"] = csem;
|
||||
auto grandkids = child->allChildren();
|
||||
if (!grandkids.empty())
|
||||
cj["childCount"] = (int)grandkids.size();
|
||||
|
||||
@@ -313,7 +313,7 @@ inline bool EditorState::moveToMatchIndex(int index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void EditorState::doFindNext(bool backwards = false) {
|
||||
inline void EditorState::doFindNext(bool backwards) {
|
||||
if (!active()) return;
|
||||
if (strlen(search.findBuf) == 0) return;
|
||||
refreshSearchMatches();
|
||||
@@ -505,4 +505,3 @@ inline std::string EditorState::uiEventLabel(UIEventType type) {
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,9 @@ inline void EditorState::registerCommands() {
|
||||
int col = 0;
|
||||
if (parseLineColInput(arg, line, col)) {
|
||||
if (active()) {
|
||||
int totalLines = countLines(active()->editBuf);
|
||||
int totalLines = 1 + (int)std::count(active()->editBuf.begin(),
|
||||
active()->editBuf.end(),
|
||||
'\n');
|
||||
if (totalLines > 0) line = std::max(1, std::min(line, totalLines));
|
||||
col = std::max(1, col);
|
||||
jumpTo(active(), line - 1, col - 1);
|
||||
@@ -207,7 +209,7 @@ inline void EditorState::registerCommands() {
|
||||
[this]() { runActiveFile(true); });
|
||||
}
|
||||
|
||||
inline void EditorState::executeCommand(const std::string& id, const std::string& arg = {}) {
|
||||
inline void EditorState::executeCommand(const std::string& id, const std::string& arg) {
|
||||
auto it = commandHandlers.find(id);
|
||||
if (it == commandHandlers.end()) return;
|
||||
it->second(arg);
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ElispCommandBuilder — produces valid Elisp command strings
|
||||
@@ -146,6 +148,7 @@ public:
|
||||
if (!initFilePath.empty()) {
|
||||
cmd += " --load \"" + initFilePath + "\"";
|
||||
}
|
||||
#ifdef _WIN32
|
||||
cmd += " 2>&1";
|
||||
FILE* pipe = openPipe(cmd.c_str(), "r");
|
||||
if (!pipe) {
|
||||
@@ -157,16 +160,31 @@ public:
|
||||
outLog += buf;
|
||||
}
|
||||
int result = closePipe(pipe);
|
||||
if (result == 0) {
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
if (isDaemonAlive()) {
|
||||
if (result == 0 || isDaemonAlive()) {
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
lastError_ = outLog.empty() ? "Failed to start Emacs daemon" : outLog;
|
||||
return false;
|
||||
#else
|
||||
// Launch daemon in the background to avoid blocking editor startup.
|
||||
cmd += " >/tmp/whetstone-emacs-daemon.log 2>&1 &";
|
||||
int result = runCommand(cmd);
|
||||
if (result != 0) {
|
||||
lastError_ = "Failed to start Emacs daemon";
|
||||
return false;
|
||||
}
|
||||
outLog = "Starting Emacs daemon in background.";
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
if (isDaemonAlive()) {
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
lastError_ = "Emacs daemon did not become ready in time";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if daemon is running
|
||||
@@ -426,4 +444,3 @@ private:
|
||||
std::string lastInitPath_;
|
||||
mutable std::string lastRunCommand_;
|
||||
};
|
||||
|
||||
|
||||
@@ -1573,5 +1573,322 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
{{"results", results}, {"count", (int)results.size()}});
|
||||
}
|
||||
|
||||
// --- saveAnnotatedAST ---
|
||||
if (method == "saveAnnotatedAST") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string path = params.value("path", "");
|
||||
if (path.empty() && state.activeBuffer)
|
||||
path = state.activeBuffer->path;
|
||||
auto it = state.bufferStates.find(path);
|
||||
if (it == state.bufferStates.end())
|
||||
return headlessRpcError(id, -32602, "No buffer: " + path);
|
||||
Module* ast = it->second->sync.getAST();
|
||||
auto res = saveSidecarAST(state.workspaceRoot, path, ast);
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true},
|
||||
{"sidecarPath", res.sidecarPath},
|
||||
{"annotationCount", res.annotationCount}});
|
||||
}
|
||||
|
||||
// --- loadAnnotatedAST ---
|
||||
if (method == "loadAnnotatedAST") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string path = params.value("path", "");
|
||||
if (path.empty() && state.activeBuffer)
|
||||
path = state.activeBuffer->path;
|
||||
auto it = state.bufferStates.find(path);
|
||||
if (it == state.bufferStates.end())
|
||||
return headlessRpcError(id, -32602, "No buffer: " + path);
|
||||
Module* ast = it->second->sync.getAST();
|
||||
auto res = loadSidecarAST(state.workspaceRoot, path, ast);
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true},
|
||||
{"mergedCount", res.mergedCount},
|
||||
{"staleCount", res.staleCount}});
|
||||
}
|
||||
|
||||
// --- listAnnotatedFiles ---
|
||||
if (method == "listAnnotatedFiles") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto files = listSidecarFiles(state.workspaceRoot);
|
||||
return headlessRpcResult(id,
|
||||
{{"files", files}, {"count", (int)files.size()}});
|
||||
}
|
||||
|
||||
// --- setSemanticAnnotation ---
|
||||
if (method == "setSemanticAnnotation") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
std::string type = params.value("type", "");
|
||||
json fields = params.contains("fields") ? params["fields"]
|
||||
: json::object();
|
||||
if (nodeId.empty() || type.empty())
|
||||
return headlessRpcError(id, -32602, "nodeId and type required");
|
||||
if (!state.activeBuffer)
|
||||
return headlessRpcError(id, -32602, "No active buffer");
|
||||
Module* ast = state.activeAST();
|
||||
if (!ast)
|
||||
return headlessRpcError(id, -32602, "No AST available");
|
||||
ASTNode* target = findNodeById(ast, nodeId);
|
||||
if (!target)
|
||||
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
||||
|
||||
// Map type string to conceptType
|
||||
std::string conceptType;
|
||||
if (type == "intent") conceptType = "IntentAnnotation";
|
||||
else if (type == "complexity") conceptType = "ComplexityAnnotation";
|
||||
else if (type == "risk") conceptType = "RiskAnnotation";
|
||||
else if (type == "contract") conceptType = "ContractAnnotation";
|
||||
else if (type == "tags") conceptType = "SemanticTagAnnotation";
|
||||
else return headlessRpcError(id, -32602, "Unknown annotation type: " + type);
|
||||
|
||||
// Remove existing annotation of same type (update semantics)
|
||||
auto annos = target->getChildren("annotations");
|
||||
for (auto* a : annos) {
|
||||
if (a->conceptType == conceptType) {
|
||||
target->removeChild(a);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new annotation node
|
||||
json nodeJson = {{"concept", conceptType}, {"properties", fields}};
|
||||
ASTNode* newAnno = fromJson(nodeJson);
|
||||
if (!newAnno)
|
||||
return headlessRpcError(id, -32010, "Failed to create annotation");
|
||||
target->addChild("annotations", newAnno);
|
||||
|
||||
return headlessRpcResult(id, {{"success", true}, {"type", type}});
|
||||
}
|
||||
|
||||
// --- getSemanticAnnotations ---
|
||||
if (method == "getSemanticAnnotations") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!state.activeBuffer)
|
||||
return headlessRpcError(id, -32602, "No active buffer");
|
||||
Module* ast = state.activeAST();
|
||||
if (!ast)
|
||||
return headlessRpcError(id, -32602, "No AST available");
|
||||
|
||||
if (!nodeId.empty()) {
|
||||
// Return annotations for a specific node
|
||||
ASTNode* target = findNodeById(ast, nodeId);
|
||||
if (!target)
|
||||
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
||||
json annos = json::array();
|
||||
for (auto* a : target->getChildren("annotations")) {
|
||||
if (!isSemanticAnnotation(a->conceptType)) continue;
|
||||
json entry;
|
||||
if (a->conceptType == "IntentAnnotation") {
|
||||
auto* ia = static_cast<IntentAnnotation*>(a);
|
||||
entry = {{"type", "intent"}, {"summary", ia->summary},
|
||||
{"category", ia->category}};
|
||||
} else if (a->conceptType == "ComplexityAnnotation") {
|
||||
auto* ca = static_cast<ComplexityAnnotation*>(a);
|
||||
entry = {{"type", "complexity"},
|
||||
{"timeComplexity", ca->timeComplexity},
|
||||
{"cognitiveComplexity", ca->cognitiveComplexity},
|
||||
{"linesOfLogic", ca->linesOfLogic}};
|
||||
} else if (a->conceptType == "RiskAnnotation") {
|
||||
auto* ra = static_cast<RiskAnnotation*>(a);
|
||||
entry = {{"type", "risk"}, {"level", ra->level},
|
||||
{"reason", ra->reason},
|
||||
{"dependentCount", ra->dependentCount}};
|
||||
} else if (a->conceptType == "ContractAnnotation") {
|
||||
auto* ca = static_cast<ContractAnnotation*>(a);
|
||||
entry = {{"type", "contract"},
|
||||
{"preconditions", ca->preconditions},
|
||||
{"postconditions", ca->postconditions},
|
||||
{"returnShape", ca->returnShape},
|
||||
{"sideEffects", ca->sideEffects}};
|
||||
} else if (a->conceptType == "SemanticTagAnnotation") {
|
||||
auto* ta = static_cast<SemanticTagAnnotation*>(a);
|
||||
entry = {{"type", "tags"}, {"tags", ta->tags}};
|
||||
}
|
||||
if (!entry.empty()) annos.push_back(entry);
|
||||
}
|
||||
return headlessRpcResult(id,
|
||||
{{"nodeId", nodeId}, {"annotations", annos}});
|
||||
} else {
|
||||
// Return all annotated nodes
|
||||
json nodes = json::array();
|
||||
for (auto* child : ast->allChildren()) {
|
||||
auto annos = child->getChildren("annotations");
|
||||
json annoList = json::array();
|
||||
for (auto* a : annos) {
|
||||
if (!isSemanticAnnotation(a->conceptType)) continue;
|
||||
json entry;
|
||||
if (a->conceptType == "IntentAnnotation") {
|
||||
auto* ia = static_cast<IntentAnnotation*>(a);
|
||||
entry = {{"type", "intent"}, {"summary", ia->summary}};
|
||||
} else if (a->conceptType == "ComplexityAnnotation") {
|
||||
entry = {{"type", "complexity"}};
|
||||
} else if (a->conceptType == "RiskAnnotation") {
|
||||
auto* ra = static_cast<RiskAnnotation*>(a);
|
||||
entry = {{"type", "risk"}, {"level", ra->level}};
|
||||
} else if (a->conceptType == "ContractAnnotation") {
|
||||
entry = {{"type", "contract"}};
|
||||
} else if (a->conceptType == "SemanticTagAnnotation") {
|
||||
auto* ta = static_cast<SemanticTagAnnotation*>(a);
|
||||
entry = {{"type", "tags"}, {"tags", ta->tags}};
|
||||
}
|
||||
if (!entry.empty()) annoList.push_back(entry);
|
||||
}
|
||||
if (!annoList.empty()) {
|
||||
nodes.push_back({
|
||||
{"nodeId", child->id},
|
||||
{"name", getNodeName(child)},
|
||||
{"kind", child->conceptType},
|
||||
{"annotations", annoList}
|
||||
});
|
||||
}
|
||||
}
|
||||
return headlessRpcResult(id, {{"nodes", nodes}});
|
||||
}
|
||||
}
|
||||
|
||||
// --- removeSemanticAnnotation ---
|
||||
if (method == "removeSemanticAnnotation") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
std::string type = params.value("type", "");
|
||||
if (nodeId.empty() || type.empty())
|
||||
return headlessRpcError(id, -32602, "nodeId and type required");
|
||||
if (!state.activeBuffer)
|
||||
return headlessRpcError(id, -32602, "No active buffer");
|
||||
Module* ast = state.activeAST();
|
||||
if (!ast)
|
||||
return headlessRpcError(id, -32602, "No AST available");
|
||||
ASTNode* target = findNodeById(ast, nodeId);
|
||||
if (!target)
|
||||
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
||||
|
||||
std::string conceptType;
|
||||
if (type == "intent") conceptType = "IntentAnnotation";
|
||||
else if (type == "complexity") conceptType = "ComplexityAnnotation";
|
||||
else if (type == "risk") conceptType = "RiskAnnotation";
|
||||
else if (type == "contract") conceptType = "ContractAnnotation";
|
||||
else if (type == "tags") conceptType = "SemanticTagAnnotation";
|
||||
else return headlessRpcError(id, -32602, "Unknown type: " + type);
|
||||
|
||||
bool removed = false;
|
||||
for (auto* a : target->getChildren("annotations")) {
|
||||
if (a->conceptType == conceptType) {
|
||||
target->removeChild(a);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return headlessRpcResult(id, {{"removed", removed}, {"type", type}});
|
||||
}
|
||||
|
||||
// --- getUnannotatedNodes ---
|
||||
if (method == "getUnannotatedNodes") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string filterType = params.value("type", "");
|
||||
bool includeHints = params.value("includeHints", false);
|
||||
if (!state.activeBuffer)
|
||||
return headlessRpcError(id, -32602, "No active buffer");
|
||||
Module* ast = state.activeAST();
|
||||
if (!ast)
|
||||
return headlessRpcError(id, -32602, "No AST available");
|
||||
|
||||
// Map filter type to conceptType
|
||||
std::string filterConcept;
|
||||
if (filterType == "intent") filterConcept = "IntentAnnotation";
|
||||
else if (filterType == "complexity") filterConcept = "ComplexityAnnotation";
|
||||
else if (filterType == "risk") filterConcept = "RiskAnnotation";
|
||||
else if (filterType == "contract") filterConcept = "ContractAnnotation";
|
||||
else if (filterType == "tags") filterConcept = "SemanticTagAnnotation";
|
||||
|
||||
json nodes = json::array();
|
||||
for (auto* child : ast->allChildren()) {
|
||||
// Only report Function and Variable nodes
|
||||
if (child->conceptType != "Function" &&
|
||||
child->conceptType != "Variable")
|
||||
continue;
|
||||
|
||||
auto annos = child->getChildren("annotations");
|
||||
bool hasTarget = false;
|
||||
if (filterConcept.empty()) {
|
||||
// Check for any semantic annotation
|
||||
for (auto* a : annos) {
|
||||
if (isSemanticAnnotation(a->conceptType)) {
|
||||
hasTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* a : annos) {
|
||||
if (a->conceptType == filterConcept) {
|
||||
hasTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTarget) {
|
||||
json entry = {
|
||||
{"nodeId", child->id},
|
||||
{"name", getNodeName(child)},
|
||||
{"kind", child->conceptType}
|
||||
};
|
||||
// Add static analysis hints if requested
|
||||
if (includeHints) {
|
||||
json hints;
|
||||
// Body statement count
|
||||
auto body = child->getChildren("body");
|
||||
hints["bodyStatements"] = (int)body.size();
|
||||
// Side effect heuristic: check for io/network calls
|
||||
bool hasSideEffects = false;
|
||||
for (auto* stmt : body) {
|
||||
if (stmt->conceptType == "FunctionCall" ||
|
||||
stmt->conceptType == "MethodCall") {
|
||||
std::string callee = getNodeName(stmt);
|
||||
if (callee.find("print") != std::string::npos ||
|
||||
callee.find("write") != std::string::npos ||
|
||||
callee.find("send") != std::string::npos ||
|
||||
callee.find("read") != std::string::npos ||
|
||||
callee.find("open") != std::string::npos)
|
||||
hasSideEffects = true;
|
||||
}
|
||||
}
|
||||
hints["sideEffectHint"] = hasSideEffects
|
||||
? "possible" : "none";
|
||||
entry["hints"] = hints;
|
||||
}
|
||||
nodes.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return headlessRpcResult(id,
|
||||
{{"nodes", nodes}, {"count", (int)nodes.size()}});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "PrimitivesRegistry.h"
|
||||
#include "SemanticTags.h"
|
||||
#include "ProjectState.h"
|
||||
#include "SidecarPersistence.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
// Extracted from EditorState.h (Sprint 8).
|
||||
// Included from EditorState.h after the EditorState struct definition.
|
||||
|
||||
inline void EditorState::ensureImportForSymbol(const std::string& library, const std::string& symbol) {
|
||||
if (!active() || library.empty()) return;
|
||||
inline void EditorState::ensureImportForSymbol(const std::string& libraryName, const std::string& symbol) {
|
||||
if (!active() || libraryName.empty()) return;
|
||||
if (settings.getBlockVulnerableImports()) {
|
||||
PackageEcosystem eco = ecosystemForLanguage(active()->language);
|
||||
std::string osvEco = osvEcosystem(eco);
|
||||
if (!osvEco.empty()) {
|
||||
std::string key = vulnKey(osvEco, library);
|
||||
std::string key = vulnKey(osvEco, libraryName);
|
||||
if (library.dependencyPanel.vulnIgnore.find(key) ==
|
||||
library.dependencyPanel.vulnIgnore.end()) {
|
||||
std::string version = dependencyVersionFor(osvEco, library);
|
||||
auto vulns = library.vulnDb.query(osvEco, library, version);
|
||||
std::string version = dependencyVersionFor(osvEco, libraryName);
|
||||
auto vulns = library.vulnDb.query(osvEco, libraryName, version);
|
||||
if (!vulns.empty()) {
|
||||
notify(NotificationLevel::Warning,
|
||||
"Blocked vulnerable import: " + library);
|
||||
"Blocked vulnerable import: " + libraryName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ inline void EditorState::ensureImportForSymbol(const std::string& library, const
|
||||
if (paren != std::string::npos) clean = clean.substr(0, paren);
|
||||
ImportEditResult result = ensureImport(active()->editBuf,
|
||||
active()->language,
|
||||
library,
|
||||
libraryName,
|
||||
clean);
|
||||
if (result.changed) {
|
||||
active()->editBuf = result.text;
|
||||
@@ -324,9 +324,9 @@ inline PackageEcosystem EditorState::ecosystemForLanguage(const std::string& lan
|
||||
return PackageEcosystem::Cpp;
|
||||
}
|
||||
|
||||
inline std::vector<ImportLocation> EditorState::collectImportLocations(const std::string& text,
|
||||
inline std::vector<EditorState::ImportLocation> EditorState::collectImportLocations(const std::string& text,
|
||||
const std::string& language) {
|
||||
std::vector<ImportLocation> out;
|
||||
std::vector<EditorState::ImportLocation> out;
|
||||
auto lines = importSplitLines(text);
|
||||
for (int i = 0; i < (int)lines.size(); ++i) {
|
||||
std::string t = trimStr(lines[i]);
|
||||
@@ -422,4 +422,3 @@ inline std::string EditorState::agentActorLabel(const std::string& sessionId) co
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
@@ -493,6 +493,39 @@ private:
|
||||
// Step 211: Register prompts
|
||||
// ---------------------------------------------------------------
|
||||
void registerWhetstonePrompts() {
|
||||
// --- Step 270: Semantic annotation prompts ---
|
||||
prompts_.push_back({"annotate_intent",
|
||||
"Annotate unannotated functions with intent summaries and categories. "
|
||||
"Uses whetstone_get_unannotated_nodes and whetstone_set_semantic_annotation.",
|
||||
{}
|
||||
});
|
||||
|
||||
prompts_.push_back({"annotate_complexity",
|
||||
"Annotate functions with complexity estimates (time complexity, "
|
||||
"cognitive complexity, lines of logic).",
|
||||
{}
|
||||
});
|
||||
|
||||
prompts_.push_back({"annotate_risk",
|
||||
"Assess modification risk for each function based on callers, "
|
||||
"complexity, and side effects. Uses call hierarchy for dependent counts.",
|
||||
{}
|
||||
});
|
||||
|
||||
prompts_.push_back({"annotate_contracts",
|
||||
"Document preconditions, postconditions, return shapes, and "
|
||||
"side effects for each function.",
|
||||
{}
|
||||
});
|
||||
|
||||
prompts_.push_back({"annotate_full",
|
||||
"Run a complete annotation pass: intent, complexity, risk, "
|
||||
"and contracts for all unannotated functions. Save the annotated "
|
||||
"AST sidecar when done.",
|
||||
{}
|
||||
});
|
||||
|
||||
// --- Original prompts ---
|
||||
prompts_.push_back({"annotate_module",
|
||||
"Analyze the current module and suggest memory annotations for all "
|
||||
"unannotated functions with confidence scores.",
|
||||
@@ -520,6 +553,80 @@ private:
|
||||
|
||||
std::vector<MCPPromptMessage> generatePromptMessages(const std::string& name,
|
||||
const json& args) {
|
||||
// --- Step 270: Semantic annotation prompt messages ---
|
||||
if (name == "annotate_intent") {
|
||||
return {{
|
||||
"user",
|
||||
{{"type", "text"}, {"text",
|
||||
"For each unannotated function, add an intent annotation:\n"
|
||||
"1. Call whetstone_get_unannotated_nodes to find targets\n"
|
||||
"2. Use whetstone_get_ast (compact:true) to understand each function\n"
|
||||
"3. Call whetstone_set_semantic_annotation with type='intent', providing:\n"
|
||||
" - summary: 1-sentence description of what and why\n"
|
||||
" - category: one of 'validation', 'transformation', 'io',\n"
|
||||
" 'coordination', 'computation', 'initialization'\n"
|
||||
"4. Verify with whetstone_get_semantic_annotations"
|
||||
}}
|
||||
}};
|
||||
}
|
||||
if (name == "annotate_complexity") {
|
||||
return {{
|
||||
"user",
|
||||
{{"type", "text"}, {"text",
|
||||
"For each function, estimate complexity:\n"
|
||||
"1. Call whetstone_get_unannotated_nodes with type='complexity'\n"
|
||||
"2. Use whetstone_get_ast (compact:true) to read each function\n"
|
||||
"3. Call whetstone_set_semantic_annotation with type='complexity':\n"
|
||||
" - timeComplexity: 'O(1)', 'O(n)', 'O(n^2)', etc.\n"
|
||||
" - cognitiveComplexity: 1-10 scale\n"
|
||||
" - linesOfLogic: count of logic statements"
|
||||
}}
|
||||
}};
|
||||
}
|
||||
if (name == "annotate_risk") {
|
||||
return {{
|
||||
"user",
|
||||
{{"type", "text"}, {"text",
|
||||
"For each function, assess modification risk:\n"
|
||||
"1. Call whetstone_get_unannotated_nodes with type='risk'\n"
|
||||
"2. Use whetstone_get_call_hierarchy to count callers/dependents\n"
|
||||
"3. Consider complexity and side effects\n"
|
||||
"4. Call whetstone_set_semantic_annotation with type='risk':\n"
|
||||
" - level: 'low', 'medium', 'high', 'critical'\n"
|
||||
" - reason: why this risk level\n"
|
||||
" - dependentCount: number of callers/consumers"
|
||||
}}
|
||||
}};
|
||||
}
|
||||
if (name == "annotate_contracts") {
|
||||
return {{
|
||||
"user",
|
||||
{{"type", "text"}, {"text",
|
||||
"For each function, document data contracts:\n"
|
||||
"1. Call whetstone_get_unannotated_nodes with type='contract'\n"
|
||||
"2. Use whetstone_get_ast (compact:true) to read each function\n"
|
||||
"3. Call whetstone_set_semantic_annotation with type='contract':\n"
|
||||
" - preconditions: input requirements\n"
|
||||
" - postconditions: output guarantees\n"
|
||||
" - returnShape: human-readable type/shape\n"
|
||||
" - sideEffects: 'none', 'io', 'mutation', 'network'"
|
||||
}}
|
||||
}};
|
||||
}
|
||||
if (name == "annotate_full") {
|
||||
return {{
|
||||
"user",
|
||||
{{"type", "text"}, {"text",
|
||||
"Run a complete semantic annotation pass:\n"
|
||||
"1. Call whetstone_get_unannotated_nodes to find all targets\n"
|
||||
"2. For each function, add intent, complexity, risk, and contract\n"
|
||||
" annotations using whetstone_set_semantic_annotation\n"
|
||||
"3. Use whetstone_get_call_hierarchy for caller counts\n"
|
||||
"4. Save the annotated AST with whetstone_save_annotated_ast\n"
|
||||
"5. Verify with whetstone_get_semantic_annotations"
|
||||
}}
|
||||
}};
|
||||
}
|
||||
if (name == "annotate_module") {
|
||||
std::string scope = args.value("scope", "all");
|
||||
return {{
|
||||
@@ -934,6 +1041,126 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Step 267: Sidecar persistence tools
|
||||
// ---------------------------------------------------------------
|
||||
void registerSidecarTools() {
|
||||
// whetstone_save_annotated_ast
|
||||
tools_.push_back({"whetstone_save_annotated_ast",
|
||||
"Save the current buffer's AST (with all semantic annotations) "
|
||||
"to a .whetstone/ sidecar file. Annotations persist alongside "
|
||||
"the codebase without polluting source code.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"path", {{"type", "string"},
|
||||
{"description", "Buffer path to save annotations for"}}}
|
||||
}}, {"required", {"path"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_save_annotated_ast"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("saveAnnotatedAST", args);
|
||||
};
|
||||
|
||||
// whetstone_load_annotated_ast
|
||||
tools_.push_back({"whetstone_load_annotated_ast",
|
||||
"Load semantic annotations from a .whetstone/ sidecar file "
|
||||
"and merge them into the current buffer's AST. Matches "
|
||||
"annotations to live nodes by node ID.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"path", {{"type", "string"},
|
||||
{"description", "Buffer path to load annotations for"}}}
|
||||
}}, {"required", {"path"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_load_annotated_ast"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("loadAnnotatedAST", args);
|
||||
};
|
||||
|
||||
// whetstone_list_annotated_files
|
||||
tools_.push_back({"whetstone_list_annotated_files",
|
||||
"List all files that have saved .whetstone/ sidecar annotation "
|
||||
"files in the workspace.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_list_annotated_files"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("listAnnotatedFiles", args);
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Step 269: Semantic annotation management tools
|
||||
// ---------------------------------------------------------------
|
||||
void registerSemanticAnnotationTools() {
|
||||
// whetstone_set_semantic_annotation
|
||||
tools_.push_back({"whetstone_set_semantic_annotation",
|
||||
"Set or update a semantic annotation on an AST node. If an "
|
||||
"annotation of the same type already exists, it is replaced. "
|
||||
"Types: intent, complexity, risk, contract, tags.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"nodeId", {{"type", "string"},
|
||||
{"description", "Target node ID"}}},
|
||||
{"type", {{"type", "string"},
|
||||
{"enum", {"intent", "complexity", "risk", "contract", "tags"}},
|
||||
{"description", "Annotation type"}}},
|
||||
{"fields", {{"type", "object"},
|
||||
{"description", "Annotation-specific fields"}}}
|
||||
}}, {"required", {"nodeId", "type", "fields"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_set_semantic_annotation"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("setSemanticAnnotation", args);
|
||||
};
|
||||
|
||||
// whetstone_get_semantic_annotations
|
||||
tools_.push_back({"whetstone_get_semantic_annotations",
|
||||
"Get semantic annotations. If nodeId provided, returns "
|
||||
"annotations for that node. Otherwise returns all annotated "
|
||||
"nodes with their annotations.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"nodeId", {{"type", "string"},
|
||||
{"description",
|
||||
"Node ID (optional, omit for all annotated nodes)"}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_semantic_annotations"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getSemanticAnnotations", args);
|
||||
};
|
||||
|
||||
// whetstone_remove_semantic_annotation
|
||||
tools_.push_back({"whetstone_remove_semantic_annotation",
|
||||
"Remove a semantic annotation of a specific type from a node.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"nodeId", {{"type", "string"},
|
||||
{"description", "Target node ID"}}},
|
||||
{"type", {{"type", "string"},
|
||||
{"enum", {"intent", "complexity", "risk", "contract", "tags"}},
|
||||
{"description", "Annotation type to remove"}}}
|
||||
}}, {"required", {"nodeId", "type"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_remove_semantic_annotation"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("removeSemanticAnnotation", args);
|
||||
};
|
||||
|
||||
// whetstone_get_unannotated_nodes
|
||||
tools_.push_back({"whetstone_get_unannotated_nodes",
|
||||
"Get Function/Variable nodes that lack semantic annotations. "
|
||||
"Optionally filter by annotation type to find nodes missing "
|
||||
"a specific annotation.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"type", {{"type", "string"},
|
||||
{"enum", {"intent", "complexity", "risk", "contract", "tags"}},
|
||||
{"description",
|
||||
"Filter: nodes missing this annotation type (optional)"}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_unannotated_nodes"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getUnannotatedNodes", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
registerASTTools();
|
||||
registerAnnotationTools();
|
||||
@@ -942,5 +1169,7 @@ private:
|
||||
registerBatchTools();
|
||||
registerProjectTools();
|
||||
registerSaveUndoTools();
|
||||
registerSidecarTools();
|
||||
registerSemanticAnnotationTools();
|
||||
}
|
||||
};
|
||||
|
||||
213
editor/src/SidecarPersistence.h
Normal file
213
editor/src/SidecarPersistence.h
Normal file
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
// Step 267: Sidecar AST Persistence
|
||||
//
|
||||
// Save/load annotated ASTs as .whetstone/<path>.ast.json sidecar files.
|
||||
// Annotations live alongside the codebase without polluting source code.
|
||||
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Annotation.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// --- Sidecar path resolution ---
|
||||
inline std::string sidecarPath(const std::string& workspaceRoot,
|
||||
const std::string& filePath) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path base = fs::path(workspaceRoot) / ".whetstone";
|
||||
return (base / (filePath + ".ast.json")).string();
|
||||
}
|
||||
|
||||
// --- Count semantic annotations in an AST ---
|
||||
inline int countSemanticAnnotations(const ASTNode* node) {
|
||||
if (!node) return 0;
|
||||
int count = 0;
|
||||
auto annos = node->getChildren("annotations");
|
||||
for (const auto* a : annos) {
|
||||
if (a->conceptType == "IntentAnnotation" ||
|
||||
a->conceptType == "ComplexityAnnotation" ||
|
||||
a->conceptType == "RiskAnnotation" ||
|
||||
a->conceptType == "ContractAnnotation" ||
|
||||
a->conceptType == "SemanticTagAnnotation")
|
||||
++count;
|
||||
}
|
||||
for (const auto* child : node->allChildren()) {
|
||||
count += countSemanticAnnotations(child);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// --- Save annotated AST to sidecar file ---
|
||||
struct SidecarSaveResult {
|
||||
bool success = false;
|
||||
std::string sidecarPath;
|
||||
int annotationCount = 0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
inline SidecarSaveResult saveSidecarAST(const std::string& workspaceRoot,
|
||||
const std::string& filePath,
|
||||
Module* ast) {
|
||||
SidecarSaveResult result;
|
||||
if (!ast) {
|
||||
result.error = "No AST to save";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.sidecarPath = sidecarPath(workspaceRoot, filePath);
|
||||
namespace fs = std::filesystem;
|
||||
fs::create_directories(fs::path(result.sidecarPath).parent_path());
|
||||
|
||||
json astJson = toJson(ast);
|
||||
std::ofstream out(result.sidecarPath);
|
||||
if (!out.is_open()) {
|
||||
result.error = "Cannot write to " + result.sidecarPath;
|
||||
return result;
|
||||
}
|
||||
out << astJson.dump(2);
|
||||
out.close();
|
||||
|
||||
result.annotationCount = countSemanticAnnotations(ast);
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Check if a node type is a semantic annotation ---
|
||||
inline bool isSemanticAnnotation(const std::string& conceptType) {
|
||||
return conceptType == "IntentAnnotation" ||
|
||||
conceptType == "ComplexityAnnotation" ||
|
||||
conceptType == "RiskAnnotation" ||
|
||||
conceptType == "ContractAnnotation" ||
|
||||
conceptType == "SemanticTagAnnotation";
|
||||
}
|
||||
|
||||
// --- Find a matching node in the live AST ---
|
||||
// Tries ID match first, then falls back to concept+name match
|
||||
// (node IDs are ephemeral and change across re-parses).
|
||||
inline ASTNode* findMatchingNode(ASTNode* liveRoot,
|
||||
const ASTNode* sidecarNode) {
|
||||
if (!liveRoot || !sidecarNode) return nullptr;
|
||||
// Try exact ID match first
|
||||
ASTNode* byId = findNodeById(liveRoot, sidecarNode->id);
|
||||
if (byId) return byId;
|
||||
// Fallback: match by concept type + name
|
||||
std::string name = getNodeName(sidecarNode);
|
||||
if (name.empty()) return nullptr;
|
||||
for (auto* child : liveRoot->allChildren()) {
|
||||
if (child->conceptType == sidecarNode->conceptType &&
|
||||
getNodeName(child) == name)
|
||||
return child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Merge annotations from sidecar AST into live AST ---
|
||||
// Matches nodes by ID or concept+name, copies semantic annotations.
|
||||
struct SidecarMergeResult {
|
||||
bool success = false;
|
||||
int mergedCount = 0;
|
||||
int staleCount = 0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
inline void mergeAnnotationsRecursive(ASTNode* sidecarNode,
|
||||
ASTNode* liveRoot,
|
||||
int& mergedCount,
|
||||
int& staleCount) {
|
||||
if (!sidecarNode) return;
|
||||
|
||||
// Check if this sidecar node has semantic annotations to merge
|
||||
auto annos = sidecarNode->getChildren("annotations");
|
||||
for (auto* anno : annos) {
|
||||
if (!isSemanticAnnotation(anno->conceptType)) continue;
|
||||
|
||||
// Find matching node in live AST
|
||||
ASTNode* liveNode = findMatchingNode(liveRoot, sidecarNode);
|
||||
if (liveNode) {
|
||||
// Clone the annotation via JSON roundtrip
|
||||
json annoJson = toJson(anno);
|
||||
ASTNode* cloned = fromJson(annoJson);
|
||||
if (cloned) {
|
||||
liveNode->addChild("annotations", cloned);
|
||||
++mergedCount;
|
||||
}
|
||||
} else {
|
||||
++staleCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into children
|
||||
for (auto* child : sidecarNode->allChildren()) {
|
||||
// Skip annotation children themselves
|
||||
if (isSemanticAnnotation(child->conceptType)) continue;
|
||||
mergeAnnotationsRecursive(child, liveRoot, mergedCount, staleCount);
|
||||
}
|
||||
}
|
||||
|
||||
inline SidecarMergeResult loadSidecarAST(const std::string& workspaceRoot,
|
||||
const std::string& filePath,
|
||||
Module* liveAST) {
|
||||
SidecarMergeResult result;
|
||||
if (!liveAST) {
|
||||
result.error = "No live AST to merge into";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string path = sidecarPath(workspaceRoot, filePath);
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) {
|
||||
result.error = "No sidecar file: " + path;
|
||||
return result;
|
||||
}
|
||||
|
||||
json astJson;
|
||||
try {
|
||||
in >> astJson;
|
||||
} catch (const std::exception& e) {
|
||||
result.error = std::string("Failed to parse sidecar: ") + e.what();
|
||||
return result;
|
||||
}
|
||||
in.close();
|
||||
|
||||
ASTNode* sidecarAST = fromJson(astJson);
|
||||
if (!sidecarAST) {
|
||||
result.error = "Failed to deserialize sidecar AST";
|
||||
return result;
|
||||
}
|
||||
|
||||
mergeAnnotationsRecursive(sidecarAST, liveAST,
|
||||
result.mergedCount, result.staleCount);
|
||||
|
||||
// Clean up sidecar AST
|
||||
delete sidecarAST;
|
||||
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- List available sidecar files ---
|
||||
inline std::vector<std::string> listSidecarFiles(
|
||||
const std::string& workspaceRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
std::vector<std::string> files;
|
||||
fs::path dir = fs::path(workspaceRoot) / ".whetstone";
|
||||
if (!fs::exists(dir)) return files;
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||
if (entry.is_regular_file()) {
|
||||
std::string name = entry.path().filename().string();
|
||||
// Remove .ast.json suffix to get original path
|
||||
const std::string suffix = ".ast.json";
|
||||
if (name.size() > suffix.size() &&
|
||||
name.substr(name.size() - suffix.size()) == suffix) {
|
||||
files.push_back(name.substr(0, name.size() - suffix.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
@@ -141,3 +141,44 @@ class ConstExprAnnotation : public Annotation {
|
||||
public:
|
||||
ConstExprAnnotation() { conceptType = "ConstExprAnnotation"; }
|
||||
};
|
||||
|
||||
// Semantic annotations for AI guidance (Sprint 10, Step 266)
|
||||
|
||||
class IntentAnnotation : public Annotation {
|
||||
public:
|
||||
std::string summary; // 1-sentence description of what/why
|
||||
std::string category; // "validation", "transformation", "io",
|
||||
// "coordination", "computation", "initialization"
|
||||
IntentAnnotation() { conceptType = "IntentAnnotation"; }
|
||||
};
|
||||
|
||||
class ComplexityAnnotation : public Annotation {
|
||||
public:
|
||||
std::string timeComplexity; // "O(1)", "O(n)", "O(n^2)", etc.
|
||||
int cognitiveComplexity = 0; // 1-10 scale
|
||||
int linesOfLogic = 0;
|
||||
ComplexityAnnotation() { conceptType = "ComplexityAnnotation"; }
|
||||
};
|
||||
|
||||
class RiskAnnotation : public Annotation {
|
||||
public:
|
||||
std::string level; // "low", "medium", "high", "critical"
|
||||
std::string reason;
|
||||
int dependentCount = 0; // how many callers/consumers
|
||||
RiskAnnotation() { conceptType = "RiskAnnotation"; }
|
||||
};
|
||||
|
||||
class ContractAnnotation : public Annotation {
|
||||
public:
|
||||
std::string preconditions;
|
||||
std::string postconditions;
|
||||
std::string returnShape; // human-readable type/shape description
|
||||
std::string sideEffects; // "none", "io", "mutation", "network"
|
||||
ContractAnnotation() { conceptType = "ContractAnnotation"; }
|
||||
};
|
||||
|
||||
class SemanticTagAnnotation : public Annotation {
|
||||
public:
|
||||
std::vector<std::string> tags; // e.g. ["@serialize", "@validation"]
|
||||
SemanticTagAnnotation() { conceptType = "SemanticTagAnnotation"; }
|
||||
};
|
||||
|
||||
@@ -134,6 +134,35 @@ inline json propertiesToJson(const ASTNode* node) {
|
||||
if (!n->semanticHint.empty()) props["semanticHint"] = n->semanticHint;
|
||||
if (!n->position.empty()) props["position"] = n->position;
|
||||
}
|
||||
// Semantic annotations (Sprint 10)
|
||||
else if (ct == "IntentAnnotation") {
|
||||
auto* n = static_cast<const IntentAnnotation*>(node);
|
||||
if (!n->summary.empty()) props["summary"] = n->summary;
|
||||
if (!n->category.empty()) props["category"] = n->category;
|
||||
}
|
||||
else if (ct == "ComplexityAnnotation") {
|
||||
auto* n = static_cast<const ComplexityAnnotation*>(node);
|
||||
if (!n->timeComplexity.empty()) props["timeComplexity"] = n->timeComplexity;
|
||||
props["cognitiveComplexity"] = n->cognitiveComplexity;
|
||||
props["linesOfLogic"] = n->linesOfLogic;
|
||||
}
|
||||
else if (ct == "RiskAnnotation") {
|
||||
auto* n = static_cast<const RiskAnnotation*>(node);
|
||||
if (!n->level.empty()) props["level"] = n->level;
|
||||
if (!n->reason.empty()) props["reason"] = n->reason;
|
||||
props["dependentCount"] = n->dependentCount;
|
||||
}
|
||||
else if (ct == "ContractAnnotation") {
|
||||
auto* n = static_cast<const ContractAnnotation*>(node);
|
||||
if (!n->preconditions.empty()) props["preconditions"] = n->preconditions;
|
||||
if (!n->postconditions.empty()) props["postconditions"] = n->postconditions;
|
||||
if (!n->returnShape.empty()) props["returnShape"] = n->returnShape;
|
||||
if (!n->sideEffects.empty()) props["sideEffects"] = n->sideEffects;
|
||||
}
|
||||
else if (ct == "SemanticTagAnnotation") {
|
||||
auto* n = static_cast<const SemanticTagAnnotation*>(node);
|
||||
if (!n->tags.empty()) props["tags"] = n->tags;
|
||||
}
|
||||
// NullLiteral, ListLiteral, IndexAccess, Block, Assignment, IfStatement,
|
||||
// WhileLoop, Return, ExpressionStatement, ListType, SetType, MapType,
|
||||
// TupleType, ArrayType, OptionalType — no extra properties
|
||||
@@ -206,6 +235,11 @@ inline ASTNode* createNode(const std::string& conceptName) {
|
||||
if (conceptName == "DerefStrategy") return new DerefStrategy();
|
||||
if (conceptName == "OptimizationLock") return new OptimizationLock();
|
||||
if (conceptName == "LangSpecific") return new LangSpecific();
|
||||
if (conceptName == "IntentAnnotation") return new IntentAnnotation();
|
||||
if (conceptName == "ComplexityAnnotation") return new ComplexityAnnotation();
|
||||
if (conceptName == "RiskAnnotation") return new RiskAnnotation();
|
||||
if (conceptName == "ContractAnnotation") return new ContractAnnotation();
|
||||
if (conceptName == "SemanticTagAnnotation") return new SemanticTagAnnotation();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -328,13 +362,53 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) {
|
||||
if (props.contains("semanticHint")) n->semanticHint = props["semanticHint"].get<std::string>();
|
||||
if (props.contains("position")) n->position = props["position"].get<std::string>();
|
||||
}
|
||||
// Semantic annotations (Sprint 10)
|
||||
else if (ct == "IntentAnnotation") {
|
||||
auto* n = static_cast<IntentAnnotation*>(node);
|
||||
if (props.contains("summary")) n->summary = props["summary"].get<std::string>();
|
||||
if (props.contains("category")) n->category = props["category"].get<std::string>();
|
||||
}
|
||||
else if (ct == "ComplexityAnnotation") {
|
||||
auto* n = static_cast<ComplexityAnnotation*>(node);
|
||||
if (props.contains("timeComplexity")) n->timeComplexity = props["timeComplexity"].get<std::string>();
|
||||
if (props.contains("cognitiveComplexity")) n->cognitiveComplexity = props["cognitiveComplexity"].get<int>();
|
||||
if (props.contains("linesOfLogic")) n->linesOfLogic = props["linesOfLogic"].get<int>();
|
||||
}
|
||||
else if (ct == "RiskAnnotation") {
|
||||
auto* n = static_cast<RiskAnnotation*>(node);
|
||||
if (props.contains("level")) n->level = props["level"].get<std::string>();
|
||||
if (props.contains("reason")) n->reason = props["reason"].get<std::string>();
|
||||
if (props.contains("dependentCount")) n->dependentCount = props["dependentCount"].get<int>();
|
||||
}
|
||||
else if (ct == "ContractAnnotation") {
|
||||
auto* n = static_cast<ContractAnnotation*>(node);
|
||||
if (props.contains("preconditions")) n->preconditions = props["preconditions"].get<std::string>();
|
||||
if (props.contains("postconditions")) n->postconditions = props["postconditions"].get<std::string>();
|
||||
if (props.contains("returnShape")) n->returnShape = props["returnShape"].get<std::string>();
|
||||
if (props.contains("sideEffects")) n->sideEffects = props["sideEffects"].get<std::string>();
|
||||
}
|
||||
else if (ct == "SemanticTagAnnotation") {
|
||||
auto* n = static_cast<SemanticTagAnnotation*>(node);
|
||||
if (props.contains("tags") && props["tags"].is_array()) {
|
||||
n->tags.clear();
|
||||
for (const auto& tag : props["tags"]) {
|
||||
if (tag.is_string()) n->tags.push_back(tag.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string generateNodeId() {
|
||||
static int counter = 0;
|
||||
return "node_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
inline ASTNode* fromJson(const json& j) {
|
||||
ASTNode* node = createNode(j["concept"].get<std::string>());
|
||||
if (!node) return nullptr;
|
||||
|
||||
node->id = j["id"].get<std::string>();
|
||||
node->id = j.contains("id") ? j["id"].get<std::string>()
|
||||
: generateNodeId();
|
||||
if (j.contains("span")) {
|
||||
const auto& span = j["span"];
|
||||
if (span.contains("start") && span.contains("end")) {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
#include "imgui_impl_sdl2.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "imgui_internal.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_opengl.h>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "EditorState.h"
|
||||
@@ -72,15 +74,110 @@ static std::string emacsChordForEvent(SDL_Keycode sym, Uint16 mods) {
|
||||
return prefix + base;
|
||||
}
|
||||
|
||||
static ImGuiID splitDockNode(ImGuiID& mainId, DockDirection direction, float ratio) {
|
||||
ImGuiDir dir = ImGuiDir_None;
|
||||
switch (direction) {
|
||||
case DockDirection::Left: dir = ImGuiDir_Left; break;
|
||||
case DockDirection::Right: dir = ImGuiDir_Right; break;
|
||||
case DockDirection::Bottom: dir = ImGuiDir_Down; break;
|
||||
case DockDirection::Top: dir = ImGuiDir_Up; break;
|
||||
case DockDirection::Center: return mainId;
|
||||
}
|
||||
return ImGui::DockBuilderSplitNode(mainId, dir, ratio, nullptr, &mainId);
|
||||
}
|
||||
|
||||
static std::unordered_map<std::string, ImGuiID> buildDockNodes(
|
||||
ImGuiID dockId,
|
||||
const LayoutConfig& cfg,
|
||||
const ImVec2& size) {
|
||||
std::unordered_map<std::string, ImGuiID> nodes;
|
||||
ImGui::DockBuilderRemoveNode(dockId);
|
||||
ImGui::DockBuilderAddNode(dockId, ImGuiDockNodeFlags_DockSpace);
|
||||
ImGui::DockBuilderSetNodeSize(dockId, size);
|
||||
ImGuiID mainId = dockId;
|
||||
for (const auto& panel : cfg.panels) {
|
||||
if (panel.direction == DockDirection::Center) continue;
|
||||
nodes[panel.id] = splitDockNode(mainId, panel.direction, panel.sizeRatio);
|
||||
}
|
||||
for (const auto& panel : cfg.panels) {
|
||||
if (panel.direction != DockDirection::Center) continue;
|
||||
nodes[panel.id] = mainId;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
static void dockPanelGroup(
|
||||
const std::unordered_map<std::string, ImGuiID>& nodes,
|
||||
const std::string& panelId,
|
||||
std::initializer_list<const char*> windows) {
|
||||
auto it = nodes.find(panelId);
|
||||
if (it == nodes.end()) return;
|
||||
for (const char* window : windows) {
|
||||
ImGui::DockBuilderDockWindow(window, it->second);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyDockLayout(ImGuiID dockId, const LayoutConfig& cfg, const ImVec2& size) {
|
||||
auto nodes = buildDockNodes(dockId, cfg, size);
|
||||
dockPanelGroup(nodes, "Explorer", {"Explorer"});
|
||||
dockPanelGroup(nodes, "Editor", {"Editor"});
|
||||
dockPanelGroup(nodes, "Panel", {"Panel"});
|
||||
dockPanelGroup(nodes, "ToolWindow",
|
||||
{"Outline", "Dependencies", "Libraries", "Compose",
|
||||
"Emacs Packages", "Emacs Bridge", "Memory Strategies"});
|
||||
dockPanelGroup(nodes, "Minibuffer", {"##Minibuffer"});
|
||||
ImGui::DockBuilderFinish(dockId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
int main(int, char**) {
|
||||
auto logStage = [](const char* msg) {
|
||||
std::fprintf(stderr, "[whetstone-startup] %s\n", msg);
|
||||
std::fflush(stderr);
|
||||
};
|
||||
|
||||
logStage("begin");
|
||||
{
|
||||
const char* envDriver = std::getenv("SDL_VIDEODRIVER");
|
||||
if (envDriver && std::string(envDriver) == "offscreen") {
|
||||
std::fprintf(stderr,
|
||||
"[whetstone-startup] overriding SDL_VIDEODRIVER=offscreen\n");
|
||||
SDL_setenv("SDL_VIDEODRIVER", "", 1);
|
||||
}
|
||||
bool hasX11 = false;
|
||||
bool hasWayland = false;
|
||||
int numDrivers = SDL_GetNumVideoDrivers();
|
||||
std::fprintf(stderr, "[whetstone-startup] available SDL drivers:");
|
||||
for (int i = 0; i < numDrivers; ++i) {
|
||||
const char* d = SDL_GetVideoDriver(i);
|
||||
if (!d) continue;
|
||||
std::fprintf(stderr, " %s", d);
|
||||
if (std::string(d) == "x11") hasX11 = true;
|
||||
if (std::string(d) == "wayland") hasWayland = true;
|
||||
}
|
||||
std::fprintf(stderr, "\n");
|
||||
std::fflush(stderr);
|
||||
const char* currentEnv = std::getenv("SDL_VIDEODRIVER");
|
||||
if (!currentEnv || std::string(currentEnv).empty()) {
|
||||
if (hasX11) SDL_setenv("SDL_VIDEODRIVER", "x11", 1);
|
||||
else if (hasWayland) SDL_setenv("SDL_VIDEODRIVER", "wayland", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// SDL init
|
||||
logStage("SDL_Init");
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||
printf("SDL Error: %s\n", SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
std::fprintf(stderr, "[whetstone-startup] SDL video driver: %s\n",
|
||||
driver ? driver : "(null)");
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
const char* glsl_version = "#version 130";
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||
@@ -92,13 +189,30 @@ int main(int, char**) {
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
|
||||
auto winFlags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE |
|
||||
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MAXIMIZED);
|
||||
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
logStage("SDL_CreateWindow");
|
||||
SDL_Window* window = SDL_CreateWindow("Whetstone Editor",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
1440, 900, winFlags);
|
||||
if (!window) {
|
||||
printf("SDL_CreateWindow Error: %s\n", SDL_GetError());
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
logStage("SDL_GL_CreateContext");
|
||||
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
|
||||
if (!gl_context) {
|
||||
printf("SDL_GL_CreateContext Error: %s\n", SDL_GetError());
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
logStage("SDL_GL_MakeCurrent");
|
||||
SDL_GL_MakeCurrent(window, gl_context);
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
SDL_ShowWindow(window);
|
||||
SDL_RaiseWindow(window);
|
||||
logStage("window shown");
|
||||
|
||||
// ImGui init
|
||||
IMGUI_CHECKVERSION();
|
||||
@@ -109,15 +223,11 @@ int main(int, char**) {
|
||||
|
||||
// Load fonts
|
||||
const float baseFontSize = 15.0f;
|
||||
ImFont* monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", baseFontSize);
|
||||
if (!monoFont) {
|
||||
monoFont = io.Fonts->AddFontDefault();
|
||||
}
|
||||
ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", baseFontSize);
|
||||
if (!uiFont) {
|
||||
uiFont = io.Fonts->AddFontDefault();
|
||||
}
|
||||
// Bootstrap with default fonts; platform-specific/custom fonts are loaded in reloadFonts().
|
||||
ImFont* monoFont = io.Fonts->AddFontDefault();
|
||||
ImFont* uiFont = io.Fonts->AddFontDefault();
|
||||
|
||||
logStage("imgui initialized");
|
||||
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
|
||||
ImGui_ImplOpenGL3_Init(glsl_version);
|
||||
|
||||
@@ -126,7 +236,9 @@ int main(int, char**) {
|
||||
state.monoFont = monoFont;
|
||||
state.uiFont = uiFont;
|
||||
state.baseFontSize = baseFontSize;
|
||||
logStage("state.init begin");
|
||||
state.init();
|
||||
logStage("state.init end");
|
||||
|
||||
auto loadFontFromPath = [&](const std::string& preferred,
|
||||
const std::vector<std::string>& fallbacks,
|
||||
@@ -194,6 +306,8 @@ int main(int, char**) {
|
||||
io.FontGlobalScale = state.settings.getFontSize() / baseFontSize;
|
||||
SessionData session;
|
||||
bool hasSession = state.loadSession(session);
|
||||
bool applyInitialLayout = !hasSession;
|
||||
bool dockLayoutApplied = false;
|
||||
if (hasSession) {
|
||||
if (!session.imguiIni.empty()) {
|
||||
ImGui::LoadIniSettingsFromMemory(session.imguiIni.c_str(),
|
||||
@@ -207,6 +321,7 @@ int main(int, char**) {
|
||||
state.lsp = std::make_shared<LSPClient>(state.lspTransport);
|
||||
|
||||
bool done = false;
|
||||
bool firstFrame = true;
|
||||
while (!done) {
|
||||
// --- Event handling ---
|
||||
SDL_Event event;
|
||||
@@ -373,6 +488,14 @@ int main(int, char**) {
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGuiID dockId = ImGui::GetID("WhetstoneDS");
|
||||
ImGui::DockSpace(dockId, ImVec2(0, 0), ImGuiDockNodeFlags_None);
|
||||
if ((applyInitialLayout && !dockLayoutApplied) || state.ui.requestLayoutReset) {
|
||||
LayoutManager layout;
|
||||
layout.setPreset(state.ui.layoutPreset);
|
||||
applyDockLayout(dockId, layout.getConfig(), viewport->WorkSize);
|
||||
dockLayoutApplied = true;
|
||||
state.ui.requestBottomCollapse = true;
|
||||
state.ui.requestLayoutReset = false;
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
// --- Render all panels ---
|
||||
@@ -435,6 +558,10 @@ int main(int, char**) {
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
SDL_GL_SwapWindow(window);
|
||||
if (firstFrame) {
|
||||
logStage("first frame swapped");
|
||||
firstFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
||||
@@ -56,10 +56,16 @@ static void renderBottomPanel(EditorState& state) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
if (state.ui.requestBottomCollapse) {
|
||||
ImGui::SetNextWindowCollapsed(true, ImGuiCond_Always);
|
||||
}
|
||||
// ---------------------------------------------------------------
|
||||
// Bottom panel — Output / AST / Highlighted Preview / Terminal
|
||||
// ---------------------------------------------------------------
|
||||
ImGui::Begin("Panel");
|
||||
if (state.ui.requestBottomCollapse) {
|
||||
state.ui.requestBottomCollapse = false;
|
||||
}
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Bottom;
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ static void renderCommandPalette(EditorState& state) {
|
||||
bool selected = (displayIndex == state.commandSelected);
|
||||
std::string rowId = "##cmd_recent_" + std::to_string(displayIndex);
|
||||
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||
ImGuiSelectableFlags_SpanAvailWidth,
|
||||
0,
|
||||
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||
state.commandSelected = displayIndex;
|
||||
accept = true;
|
||||
@@ -375,7 +375,7 @@ static void renderCommandPalette(EditorState& state) {
|
||||
bool selected = (displayIndex == state.commandSelected);
|
||||
std::string rowId = "##cmd_" + std::to_string(displayIndex);
|
||||
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||
ImGuiSelectableFlags_SpanAvailWidth,
|
||||
0,
|
||||
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||
state.commandSelected = displayIndex;
|
||||
accept = true;
|
||||
@@ -502,7 +502,7 @@ static void renderCommandPalette(EditorState& state) {
|
||||
bool selected = (i == state.commandSelected);
|
||||
std::string rowId = "##file_" + std::to_string(i);
|
||||
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||
ImGuiSelectableFlags_SpanAvailWidth,
|
||||
0,
|
||||
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||
state.commandSelected = i;
|
||||
accept = true;
|
||||
|
||||
@@ -29,6 +29,18 @@ static void renderExplorerPanel(EditorState& state) {
|
||||
state.refreshFileTree();
|
||||
if (state.fileTreeRoot.path.empty()) {
|
||||
ImGui::TextDisabled("(no workspace)");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Open a folder to browse project files.");
|
||||
if (ImGui::Button("Open Folder")) {
|
||||
auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot});
|
||||
if (!path.empty()) {
|
||||
state.workspaceRoot = path;
|
||||
state.fileTreeDirty = true;
|
||||
state.search.projectSearch.setRoot(state.workspaceRoot);
|
||||
state.lastDialogPath = path;
|
||||
state.refreshBuildSystem();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RenderFileTree(state.fileTreeRoot, state);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,10 @@ static void renderMenuBar(EditorState& state) {
|
||||
state.ui.layoutPreset = LayoutPreset::Emacs;
|
||||
if (ImGui::MenuItem("JetBrains", nullptr, state.ui.layoutPreset == LayoutPreset::JetBrains))
|
||||
state.ui.layoutPreset = LayoutPreset::JetBrains;
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Reset Layout")) {
|
||||
state.ui.requestLayoutReset = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
|
||||
@@ -93,7 +93,7 @@ static void renderStatusBar(EditorState& state) {
|
||||
|
||||
// Left cluster: mode, language, encoding, line ending
|
||||
std::string modeLabel = state.active()
|
||||
? (state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text" : "Structured")
|
||||
? (state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text Mode" : "Structured Mode")
|
||||
: "-";
|
||||
if (ImGui::Button(modeLabel.c_str())) {
|
||||
toggleBufferMode(state);
|
||||
|
||||
@@ -23,6 +23,8 @@ struct UIFlags {
|
||||
bool showAnnotateWizard = false;
|
||||
bool showProjectWizard = false;
|
||||
bool showAgentWizard = false;
|
||||
bool requestLayoutReset = false;
|
||||
bool requestBottomCollapse = false;
|
||||
int bottomTab = 0; // 0=Output,1=AST,2=Highlighted
|
||||
LayoutPreset layoutPreset = LayoutPreset::VSCode;
|
||||
FocusRegion focusTarget = FocusRegion::None;
|
||||
|
||||
Reference in New Issue
Block a user