2026-02-09 21:09:24 -07:00
|
|
|
#pragma once
|
|
|
|
|
// Included inside CodeEditorWidget (rendering + folds).
|
|
|
|
|
public:
|
2026-02-10 04:55:49 -07:00
|
|
|
static int annotationShapeIndex(const std::string& label) {
|
|
|
|
|
size_t h = 0;
|
|
|
|
|
for (unsigned char c : label) h = h * 131 + c;
|
|
|
|
|
return (int)(h % 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void drawAnnotationShape(ImDrawList* drawList,
|
|
|
|
|
const ImVec2& center,
|
|
|
|
|
float size,
|
|
|
|
|
ImU32 color,
|
|
|
|
|
int shape) {
|
|
|
|
|
switch (shape) {
|
|
|
|
|
case 0: { // circle
|
|
|
|
|
drawList->AddCircleFilled(center, size, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 1: { // triangle
|
|
|
|
|
ImVec2 top(center.x, center.y - size);
|
|
|
|
|
ImVec2 left(center.x - size, center.y + size);
|
|
|
|
|
ImVec2 right(center.x + size, center.y + size);
|
|
|
|
|
drawList->AddTriangleFilled(top, left, right, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 2: { // square
|
|
|
|
|
ImVec2 a(center.x - size, center.y - size);
|
|
|
|
|
ImVec2 b(center.x + size, center.y + size);
|
|
|
|
|
drawList->AddRectFilled(a, b, color, 0.0f);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 3: { // diamond
|
|
|
|
|
ImVec2 top(center.x, center.y - size);
|
|
|
|
|
ImVec2 right(center.x + size, center.y);
|
|
|
|
|
ImVec2 bottom(center.x, center.y + size);
|
|
|
|
|
ImVec2 left(center.x - size, center.y);
|
|
|
|
|
drawList->AddQuadFilled(top, right, bottom, left, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default: { // star (outline)
|
|
|
|
|
ImVec2 pts[10];
|
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
|
|
|
constexpr float kPi = 3.14159265358979323846f;
|
2026-02-10 04:55:49 -07:00
|
|
|
for (int i = 0; i < 10; ++i) {
|
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
|
|
|
float angle = (float)(kPi * 0.5f + i * (kPi / 5.0f));
|
2026-02-10 04:55:49 -07:00
|
|
|
float radius = (i % 2 == 0) ? size : size * 0.5f;
|
|
|
|
|
pts[i] = ImVec2(center.x + std::cos(angle) * radius,
|
|
|
|
|
center.y - std::sin(angle) * radius);
|
|
|
|
|
}
|
|
|
|
|
drawList->AddPolyline(pts, 10, color, true, 1.5f);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 21:09:24 -07:00
|
|
|
CodeEditorResult render(const char* id,
|
|
|
|
|
std::string& text,
|
|
|
|
|
const std::vector<HighlightSpan>& spans,
|
|
|
|
|
const CodeEditorOptions& options,
|
|
|
|
|
const ImVec2& size,
|
|
|
|
|
ImFont* font) {
|
|
|
|
|
CodeEditorResult result;
|
|
|
|
|
if (!font) font = ImGui::GetFont();
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
2026-02-09 22:31:47 -07:00
|
|
|
syncPrimaryToMulti();
|
2026-02-09 22:18:11 -07:00
|
|
|
const double nowSeconds = options.nowSeconds > 0.0 ? options.nowSeconds : ImGui::GetTime();
|
2026-02-09 21:09:24 -07:00
|
|
|
// Build line start offsets
|
|
|
|
|
std::vector<int> lineStarts;
|
|
|
|
|
lineStarts.reserve(128);
|
|
|
|
|
lineStarts.push_back(0);
|
|
|
|
|
for (int i = 0; i < (int)text.size(); ++i) {
|
|
|
|
|
if (text[i] == '\n') lineStarts.push_back(i + 1);
|
|
|
|
|
}
|
|
|
|
|
const int lineCount = (int)lineStarts.size();
|
|
|
|
|
|
2026-02-09 22:35:05 -07:00
|
|
|
auto isOpenBracket = [](char c) {
|
|
|
|
|
return c == '(' || c == '[' || c == '{';
|
|
|
|
|
};
|
|
|
|
|
auto isCloseBracket = [](char c) {
|
|
|
|
|
return c == ')' || c == ']' || c == '}';
|
|
|
|
|
};
|
|
|
|
|
auto matchingOpen = [](char c) {
|
|
|
|
|
if (c == ')') return '(';
|
|
|
|
|
if (c == ']') return '[';
|
|
|
|
|
if (c == '}') return '{';
|
|
|
|
|
return '\0';
|
|
|
|
|
};
|
|
|
|
|
auto isBracketChar = [&](char c) {
|
|
|
|
|
return isOpenBracket(c) || isCloseBracket(c);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::vector<int> bracketDepth(text.size(), -1);
|
|
|
|
|
std::vector<int> bracketMatch(text.size(), -1);
|
|
|
|
|
std::vector<int> stack;
|
|
|
|
|
std::vector<char> stackChars;
|
|
|
|
|
stack.reserve(128);
|
|
|
|
|
stackChars.reserve(128);
|
|
|
|
|
for (int i = 0; i < (int)text.size(); ++i) {
|
|
|
|
|
char c = text[i];
|
|
|
|
|
if (isOpenBracket(c)) {
|
|
|
|
|
stack.push_back(i);
|
|
|
|
|
stackChars.push_back(c);
|
|
|
|
|
bracketDepth[i] = (int)stack.size() - 1;
|
|
|
|
|
} else if (isCloseBracket(c)) {
|
|
|
|
|
char open = matchingOpen(c);
|
|
|
|
|
if (!stackChars.empty() && stackChars.back() == open) {
|
|
|
|
|
int openPos = stack.back();
|
|
|
|
|
stack.pop_back();
|
|
|
|
|
stackChars.pop_back();
|
|
|
|
|
int depth = (int)stack.size();
|
|
|
|
|
bracketDepth[i] = depth;
|
|
|
|
|
bracketMatch[i] = openPos;
|
|
|
|
|
bracketMatch[openPos] = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::array<ImU32, 6> rainbow = {
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_1", IM_COL32(242, 120, 75, 255)),
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_2", IM_COL32(241, 196, 15, 255)),
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_3", IM_COL32(46, 204, 113, 255)),
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_4", IM_COL32(52, 152, 219, 255)),
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_5", IM_COL32(155, 89, 182, 255)),
|
|
|
|
|
ThemeEngine::instance().editorColor("bracket_6", IM_COL32(231, 76, 60, 255))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int focusBracketPos = -1;
|
|
|
|
|
int focusBracketMatch = -1;
|
|
|
|
|
if (cursor_ > 0 && cursor_ - 1 < (int)text.size() && isBracketChar(text[cursor_ - 1])) {
|
|
|
|
|
focusBracketPos = cursor_ - 1;
|
|
|
|
|
} else if (cursor_ < (int)text.size() && isBracketChar(text[cursor_])) {
|
|
|
|
|
focusBracketPos = cursor_;
|
|
|
|
|
}
|
|
|
|
|
if (focusBracketPos >= 0 && focusBracketPos < (int)bracketMatch.size()) {
|
|
|
|
|
focusBracketMatch = bracketMatch[focusBracketPos];
|
|
|
|
|
}
|
|
|
|
|
int scopeStartLine = -1;
|
|
|
|
|
int scopeEndLine = -1;
|
|
|
|
|
if (focusBracketMatch >= 0) {
|
|
|
|
|
int start = std::min(focusBracketPos, focusBracketMatch);
|
|
|
|
|
int end = std::max(focusBracketPos, focusBracketMatch);
|
|
|
|
|
scopeStartLine = lineFromPos(start, lineStarts);
|
|
|
|
|
scopeEndLine = lineFromPos(end, lineStarts);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 21:09:24 -07:00
|
|
|
// Clamp cursor
|
|
|
|
|
if (cursor_ < 0) cursor_ = 0;
|
|
|
|
|
if (cursor_ > (int)text.size()) cursor_ = (int)text.size();
|
|
|
|
|
|
|
|
|
|
// Build per-byte category map
|
|
|
|
|
std::vector<TokenCategory> charCats(text.size(), TokenCategory::Plain);
|
|
|
|
|
for (const auto& span : spans) {
|
|
|
|
|
uint32_t end = std::min<uint32_t>(span.end, (uint32_t)charCats.size());
|
|
|
|
|
for (uint32_t i = span.start; i < end; ++i) {
|
|
|
|
|
charCats[i] = span.category;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.enableFolding && options.mode) {
|
|
|
|
|
updateFolds(text, options.mode->getLanguage());
|
|
|
|
|
} else {
|
|
|
|
|
folds_.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Measure
|
2026-02-09 21:58:13 -07:00
|
|
|
const float lineHeight =
|
|
|
|
|
ImGui::GetTextLineHeightWithSpacing() * std::max(0.5f, options.lineHeightScale);
|
|
|
|
|
const float baseAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x;
|
|
|
|
|
const float charAdvance = baseAdvance + std::max(0.0f, options.letterSpacing);
|
2026-02-09 21:09:24 -07:00
|
|
|
const float gutterWidth = options.showLineNumbers ?
|
|
|
|
|
calcGutterWidth(lineCount, font, charAdvance) : 12.0f;
|
|
|
|
|
const float minimapWidth = options.showMinimap ? 80.0f : 0.0f;
|
|
|
|
|
|
|
|
|
|
// Estimate content size for scrollbars
|
|
|
|
|
int maxLineLen = 0;
|
|
|
|
|
for (int ln = 0; ln < lineCount; ++ln) {
|
|
|
|
|
int start = lineStarts[ln];
|
|
|
|
|
int end = (ln + 1 < lineCount) ? lineStarts[ln + 1] - 1 : (int)text.size();
|
|
|
|
|
int len = std::max(0, end - start);
|
|
|
|
|
maxLineLen = std::max(maxLineLen, len);
|
|
|
|
|
}
|
|
|
|
|
ImVec2 contentSize(gutterWidth + maxLineLen * charAdvance + minimapWidth + 4.0f,
|
|
|
|
|
lineCount * lineHeight + 4.0f);
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild(id, size, false, ImGuiWindowFlags_HorizontalScrollbar);
|
|
|
|
|
if (options.syncScrollX && !options.scrollMaster) {
|
|
|
|
|
ImGui::SetScrollX(*options.syncScrollX);
|
|
|
|
|
}
|
|
|
|
|
if (options.syncScrollY && !options.scrollMaster) {
|
|
|
|
|
ImGui::SetScrollY(*options.syncScrollY);
|
|
|
|
|
}
|
|
|
|
|
std::string annoPopupId = std::string(id) + "_AnnoPopup";
|
|
|
|
|
ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
|
|
|
|
// Dummy to set scroll extents
|
|
|
|
|
ImGui::Dummy(contentSize);
|
|
|
|
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
const float scrollX = ImGui::GetScrollX();
|
|
|
|
|
const float scrollY = ImGui::GetScrollY();
|
|
|
|
|
if (options.syncScrollX && options.scrollMaster) {
|
|
|
|
|
*options.syncScrollX = scrollX;
|
|
|
|
|
}
|
|
|
|
|
if (options.syncScrollY && options.scrollMaster) {
|
|
|
|
|
*options.syncScrollY = scrollY;
|
|
|
|
|
}
|
|
|
|
|
ImVec2 gutterBase(origin.x, origin.y - scrollY);
|
|
|
|
|
ImVec2 textBase(origin.x + gutterWidth - scrollX, origin.y - scrollY);
|
|
|
|
|
const float windowWidth = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x;
|
|
|
|
|
const float minimapBaseX = origin.x + std::max(0.0f, windowWidth - minimapWidth);
|
|
|
|
|
const float minimapBaseY = origin.y - scrollY;
|
|
|
|
|
|
|
|
|
|
// Focus and input
|
|
|
|
|
const bool hovered = ImGui::IsWindowHovered();
|
|
|
|
|
const bool focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
|
|
|
|
|
|
|
|
// Mouse handling
|
|
|
|
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
ImGui::SetKeyboardFocusHere(-1);
|
|
|
|
|
const ImVec2 mouse = ImGui::GetMousePos();
|
2026-02-17 08:47:26 -07:00
|
|
|
bool finalizeSelection = true;
|
2026-02-09 21:09:24 -07:00
|
|
|
if (options.showMinimap && mouse.x >= minimapBaseX) {
|
|
|
|
|
float miniHeight = lineCount * lineHeight;
|
|
|
|
|
if (miniHeight > 0.0f) {
|
|
|
|
|
float localY = mouse.y - minimapBaseY;
|
|
|
|
|
float t = std::max(0.0f, std::min(1.0f, localY / miniHeight));
|
|
|
|
|
float targetScroll = t * lineCount * lineHeight;
|
|
|
|
|
ImGui::SetScrollY(targetScroll);
|
|
|
|
|
}
|
|
|
|
|
selecting_ = false;
|
2026-02-17 08:47:26 -07:00
|
|
|
finalizeSelection = false;
|
2026-02-09 21:09:24 -07:00
|
|
|
}
|
|
|
|
|
bool ctrlClick = io.KeyCtrl || io.KeySuper;
|
2026-02-09 22:31:47 -07:00
|
|
|
bool altClick = io.KeyAlt;
|
|
|
|
|
bool columnSelect = io.KeyAlt && io.KeyShift;
|
2026-02-09 21:09:24 -07:00
|
|
|
int clickCount = ImGui::GetIO().MouseClickedCount[0];
|
2026-02-17 08:47:26 -07:00
|
|
|
if (!finalizeSelection) {
|
|
|
|
|
// Minimap click updates viewport only; keep selection state unchanged.
|
|
|
|
|
} else if (mouse.x < origin.x + gutterWidth || clickCount >= 3) {
|
2026-02-09 21:09:24 -07:00
|
|
|
int line = lineFromMouseY(mouse.y, gutterBase.y, lineHeight, lineCount);
|
|
|
|
|
result.lineClicked = true;
|
|
|
|
|
result.clickedLine = line;
|
|
|
|
|
const FoldRegion* fold = findFoldAtLine(line);
|
|
|
|
|
if (fold && mouse.x < origin.x + 12.0f) {
|
|
|
|
|
toggleFoldAtLine(line);
|
|
|
|
|
}
|
|
|
|
|
int lineStart = lineStarts[line];
|
|
|
|
|
int lineEnd = (line + 1 < lineCount) ? lineStarts[line + 1] : (int)text.size();
|
|
|
|
|
cursor_ = lineStart;
|
|
|
|
|
selStart_ = lineStart;
|
|
|
|
|
selEnd_ = lineEnd;
|
2026-02-09 22:31:47 -07:00
|
|
|
} else if (columnSelect) {
|
|
|
|
|
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
|
|
|
|
|
int line = lineFromPos(pos, lineStarts);
|
|
|
|
|
int col = pos - lineStarts[line];
|
|
|
|
|
columnSelectActive_ = true;
|
|
|
|
|
columnAnchorLine_ = line;
|
|
|
|
|
columnAnchorCol_ = col;
|
|
|
|
|
updateColumnSelection(text, lineStarts, columnAnchorLine_, columnAnchorCol_, line, col);
|
|
|
|
|
selecting_ = true;
|
2026-02-17 08:47:26 -07:00
|
|
|
finalizeSelection = false;
|
2026-02-09 22:31:47 -07:00
|
|
|
} else if (altClick) {
|
|
|
|
|
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
|
|
|
|
|
addCursorAt(pos);
|
|
|
|
|
selecting_ = false;
|
2026-02-17 08:47:26 -07:00
|
|
|
finalizeSelection = false;
|
2026-02-09 21:09:24 -07:00
|
|
|
} else {
|
2026-02-09 22:31:47 -07:00
|
|
|
clearExtraCursors();
|
2026-02-09 21:09:24 -07:00
|
|
|
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
|
|
|
|
|
result.lineClicked = true;
|
|
|
|
|
result.clickedLine = lineFromPos(pos, lineStarts);
|
|
|
|
|
if (ctrlClick) {
|
|
|
|
|
result.ctrlClick = true;
|
|
|
|
|
result.ctrlClickPos = pos;
|
|
|
|
|
result.ctrlClickLine = result.clickedLine;
|
|
|
|
|
result.ctrlClickCol = pos - lineStarts[result.ctrlClickLine];
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
selectWordAt(text, pos);
|
|
|
|
|
} else if (ImGui::GetIO().KeyShift) {
|
|
|
|
|
if (!hasSelection()) selStart_ = cursor_;
|
|
|
|
|
cursor_ = pos;
|
|
|
|
|
selEnd_ = cursor_;
|
|
|
|
|
} else {
|
|
|
|
|
cursor_ = pos;
|
|
|
|
|
selStart_ = cursor_;
|
|
|
|
|
selEnd_ = cursor_;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-17 08:47:26 -07:00
|
|
|
if (finalizeSelection) selecting_ = true;
|
2026-02-09 21:09:24 -07:00
|
|
|
}
|
2026-02-17 08:47:26 -07:00
|
|
|
|
2026-02-09 21:09:24 -07:00
|
|
|
if (hovered && selecting_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
|
|
|
|
const ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
if (mouse.x >= origin.x + gutterWidth) {
|
|
|
|
|
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
|
2026-02-09 22:31:47 -07:00
|
|
|
if (columnSelectActive_ && (io.KeyAlt && io.KeyShift)) {
|
|
|
|
|
int line = lineFromPos(pos, lineStarts);
|
|
|
|
|
int col = pos - lineStarts[line];
|
|
|
|
|
updateColumnSelection(text, lineStarts, columnAnchorLine_, columnAnchorCol_, line, col);
|
|
|
|
|
} else {
|
|
|
|
|
selEnd_ = pos;
|
|
|
|
|
}
|
2026-02-09 21:09:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
selecting_ = false;
|
2026-02-09 22:31:47 -07:00
|
|
|
columnSelectActive_ = false;
|
2026-02-09 21:09:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hovered) {
|
|
|
|
|
const ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
if (mouse.x >= origin.x + gutterWidth &&
|
|
|
|
|
(!options.showMinimap || mouse.x < minimapBaseX)) {
|
|
|
|
|
int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight);
|
|
|
|
|
int line = lineFromPos(pos, lineStarts);
|
|
|
|
|
result.hoverValid = true;
|
|
|
|
|
result.hoverPos = pos;
|
|
|
|
|
result.hoverLine = line;
|
|
|
|
|
result.hoverCol = pos - lineStarts[line];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard input
|
|
|
|
|
if (focused && !options.readOnly) {
|
|
|
|
|
handleKeyboard(text, result.changed, lineStarts, options.mode);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 05:09:14 -07:00
|
|
|
// Render visible lines (virtualized with buffer)
|
|
|
|
|
const int virtualBuffer = 50;
|
|
|
|
|
const int viewFirst = std::max(0, (int)(scrollY / lineHeight));
|
|
|
|
|
const int viewLines = (int)(ImGui::GetContentRegionAvail().y / lineHeight) + 2;
|
|
|
|
|
const int firstLine = std::max(0, viewFirst - virtualBuffer);
|
|
|
|
|
const int lastLine = std::min(lineCount - 1, viewFirst + viewLines + virtualBuffer);
|
2026-02-09 21:09:24 -07:00
|
|
|
|
|
|
|
|
const float textAreaWidth = std::max(0.0f, windowWidth - gutterWidth - minimapWidth);
|
|
|
|
|
const int currentLine = lineFromPos(cursor_, lineStarts);
|
|
|
|
|
|
2026-02-10 11:22:44 -07:00
|
|
|
renderVisibleLines(text,
|
|
|
|
|
lineStarts,
|
|
|
|
|
lineCount,
|
|
|
|
|
firstLine,
|
|
|
|
|
lastLine,
|
|
|
|
|
currentLine,
|
|
|
|
|
lineHeight,
|
|
|
|
|
charAdvance,
|
|
|
|
|
gutterWidth,
|
|
|
|
|
textAreaWidth,
|
|
|
|
|
origin,
|
|
|
|
|
textBase,
|
|
|
|
|
options,
|
|
|
|
|
charCats,
|
|
|
|
|
bracketDepth,
|
|
|
|
|
rainbow,
|
|
|
|
|
focusBracketPos,
|
|
|
|
|
focusBracketMatch,
|
|
|
|
|
scopeStartLine,
|
|
|
|
|
scopeEndLine,
|
|
|
|
|
nowSeconds,
|
|
|
|
|
hovered,
|
|
|
|
|
drawList,
|
|
|
|
|
result,
|
|
|
|
|
font);
|
2026-02-09 21:09:24 -07:00
|
|
|
if (ImGui::BeginPopup(annoPopupId.c_str())) {
|
2026-02-17 08:47:26 -07:00
|
|
|
ImGui::TextUnformatted("Annotation Editor (STUB: UI editor not wired yet)");
|
2026-02-09 21:09:24 -07:00
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextUnformatted(annotationPopupMessage_.c_str());
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cursor
|
|
|
|
|
if (focused) {
|
2026-02-09 22:18:11 -07:00
|
|
|
float blinkAlpha = AnimationUtils::blinkAlpha(nowSeconds,
|
|
|
|
|
options.cursorBlinkRate,
|
|
|
|
|
options.reduceMotion);
|
|
|
|
|
if (blinkAlpha > 0.02f) {
|
2026-02-09 22:31:47 -07:00
|
|
|
for (size_t i = 0; i < cursors_.size(); ++i) {
|
|
|
|
|
const auto& c = cursors_[i];
|
|
|
|
|
int curLine = lineFromPos(c.cursor, lineStarts);
|
|
|
|
|
int lineStart = lineStarts[curLine];
|
|
|
|
|
int col = c.cursor - lineStart;
|
|
|
|
|
float x = textBase.x + col * charAdvance;
|
|
|
|
|
float y = textBase.y + curLine * lineHeight;
|
|
|
|
|
ImU32 base = ThemeEngine::instance().editorColor("caret",
|
|
|
|
|
IM_COL32(240, 240, 240, 255));
|
|
|
|
|
ImVec4 color = ImGui::ColorConvertU32ToFloat4(base);
|
|
|
|
|
color.w *= (i == 0) ? blinkAlpha : (blinkAlpha * 0.6f);
|
|
|
|
|
ImU32 col32 = ImGui::ColorConvertFloat4ToU32(color);
|
|
|
|
|
drawList->AddLine(ImVec2(x, y), ImVec2(x, y + lineHeight), col32, 1.0f);
|
|
|
|
|
}
|
2026-02-09 21:09:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Minimap
|
|
|
|
|
if (options.showMinimap && minimapWidth > 0.0f) {
|
|
|
|
|
float miniHeight = lineCount * lineHeight;
|
|
|
|
|
ImVec2 miniA(minimapBaseX, minimapBaseY);
|
|
|
|
|
ImVec2 miniB(minimapBaseX + minimapWidth, minimapBaseY + miniHeight);
|
|
|
|
|
drawList->AddRectFilled(miniA, miniB,
|
|
|
|
|
ThemeEngine::instance().editorColor("minimap_bg",
|
|
|
|
|
IM_COL32(18, 18, 18, 255)));
|
|
|
|
|
for (int ln = 0; ln < lineCount; ++ln) {
|
|
|
|
|
float y = minimapBaseY + ln * lineHeight;
|
|
|
|
|
ImU32 color = ThemeEngine::instance().editorColor("minimap_line",
|
|
|
|
|
IM_COL32(80, 80, 80, 255));
|
|
|
|
|
if (ln == currentLine) {
|
|
|
|
|
color = ThemeEngine::instance().editorColor("minimap_line_active",
|
|
|
|
|
IM_COL32(120, 120, 160, 255));
|
|
|
|
|
}
|
|
|
|
|
drawList->AddRectFilled(ImVec2(minimapBaseX + 2.0f, y),
|
|
|
|
|
ImVec2(minimapBaseX + minimapWidth - 2.0f, y + 2.0f),
|
|
|
|
|
color);
|
|
|
|
|
}
|
|
|
|
|
float viewStart = (lineCount > 0) ? (scrollY / (lineCount * lineHeight)) : 0.0f;
|
|
|
|
|
float viewEnd = viewStart + (ImGui::GetContentRegionAvail().y / (lineCount * lineHeight));
|
|
|
|
|
viewStart = std::max(0.0f, std::min(1.0f, viewStart));
|
|
|
|
|
viewEnd = std::max(0.0f, std::min(1.0f, viewEnd));
|
|
|
|
|
ImVec2 vA(minimapBaseX, minimapBaseY + viewStart * miniHeight);
|
|
|
|
|
ImVec2 vB(minimapBaseX + minimapWidth, minimapBaseY + viewEnd * miniHeight);
|
|
|
|
|
drawList->AddRect(vA, vB,
|
|
|
|
|
ThemeEngine::instance().editorColor("minimap_view",
|
|
|
|
|
IM_COL32(120, 160, 220, 180)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
2026-02-09 22:31:47 -07:00
|
|
|
syncMultiToPrimary();
|
2026-02-09 21:09:24 -07:00
|
|
|
result.cursorByte = cursor_;
|
|
|
|
|
result.lineCount = lineCount;
|
|
|
|
|
result.gutterWidth = gutterWidth;
|
|
|
|
|
result.currentLine = currentLine;
|
|
|
|
|
result.foldCount = (int)folds_.size();
|
|
|
|
|
result.anyFolded = std::any_of(folds_.begin(), folds_.end(),
|
|
|
|
|
[](const FoldRegion& f) { return f.folded; });
|
|
|
|
|
result.minimapEnabled = options.showMinimap;
|
|
|
|
|
result.minimapWidth = minimapWidth;
|
|
|
|
|
if (options.showMinimap && lineCount > 0) {
|
|
|
|
|
float miniHeight = lineCount * lineHeight;
|
|
|
|
|
result.minimapViewportStart = (scrollY / (lineCount * lineHeight)) * miniHeight;
|
|
|
|
|
result.minimapViewportEnd = result.minimapViewportStart +
|
|
|
|
|
(ImGui::GetContentRegionAvail().y / (lineCount * lineHeight)) * miniHeight;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:31:47 -07:00
|
|
|
void setCursor(int pos) {
|
|
|
|
|
cursor_ = pos;
|
|
|
|
|
syncPrimaryToMulti();
|
|
|
|
|
}
|
2026-02-09 21:09:24 -07:00
|
|
|
int getCursor() const { return cursor_; }
|
2026-02-09 22:24:57 -07:00
|
|
|
bool hasSelectionRange() const { return hasSelection(); }
|
|
|
|
|
void getSelectionRange(int& start, int& end) const {
|
|
|
|
|
if (!hasSelection()) {
|
|
|
|
|
start = end = -1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
start = std::min(selStart_, selEnd_);
|
|
|
|
|
end = std::max(selStart_, selEnd_);
|
|
|
|
|
}
|
|
|
|
|
void setSelectionRange(int start, int end) {
|
|
|
|
|
selStart_ = start;
|
|
|
|
|
selEnd_ = end;
|
|
|
|
|
}
|
2026-02-09 21:09:24 -07:00
|
|
|
const std::vector<FoldRegion>& getFoldRegions() const { return folds_; }
|
|
|
|
|
std::vector<int> getFoldedLines() const {
|
|
|
|
|
std::vector<int> lines;
|
|
|
|
|
for (const auto& f : folds_) {
|
|
|
|
|
if (f.folded) lines.push_back(f.startLine);
|
|
|
|
|
}
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setDesiredFoldedLines(const std::vector<int>& lines) {
|
|
|
|
|
desiredFoldedLines_ = lines;
|
|
|
|
|
applyDesiredFoldState_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void toggleFoldAtLine(int line) {
|
|
|
|
|
for (auto& f : folds_) {
|
|
|
|
|
if (f.startLine == line) {
|
|
|
|
|
f.folded = !f.folded;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
int cursor_ = 0;
|
|
|
|
|
int selStart_ = -1;
|
|
|
|
|
int selEnd_ = -1;
|
|
|
|
|
bool selecting_ = false;
|
2026-02-09 22:31:47 -07:00
|
|
|
std::vector<MultiCursor> cursors_;
|
|
|
|
|
bool columnSelectActive_ = false;
|
|
|
|
|
int columnAnchorLine_ = 0;
|
|
|
|
|
int columnAnchorCol_ = 0;
|
2026-02-09 21:09:24 -07:00
|
|
|
std::vector<FoldRegion> folds_;
|
|
|
|
|
std::string lastFoldText_;
|
|
|
|
|
std::string lastFoldLang_;
|
|
|
|
|
std::string annotationPopupMessage_;
|
|
|
|
|
std::vector<int> desiredFoldedLines_;
|
|
|
|
|
bool applyDesiredFoldState_ = false;
|
|
|
|
|
|
|
|
|
|
#include "CodeEditorRenderHelpers.h"
|
2026-02-10 11:22:44 -07:00
|
|
|
#include "CodeEditorRenderVisible.h"
|