814 lines
51 KiB
C++
814 lines
51 KiB
C++
#pragma once
|
|
|
|
#include "../EditorState.h"
|
|
#include "../EditorUtils.h"
|
|
#include "../CompletionUtils.h"
|
|
#include "../AnimationUtils.h"
|
|
#include "../RichTooltip.h"
|
|
#include <map>
|
|
|
|
static void renderEditorPanel(EditorState& state) {
|
|
// ---------------------------------------------------------------
|
|
// Editor (center) -- editable text area
|
|
// ---------------------------------------------------------------
|
|
ImGui::Begin("Editor");
|
|
ImGui::PushFont(state.uiFont);
|
|
ImGui::BeginChild("##breadcrumbs", ImVec2(0, 26.0f), false, ImGuiWindowFlags_NoScrollbar);
|
|
if (!state.active()) {
|
|
ImGui::TextDisabled("(no file)");
|
|
} else if (!state.isStructured()) {
|
|
ImGui::TextDisabled("Text mode");
|
|
} else {
|
|
Module* ast = state.activeAST();
|
|
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
|
int colZero = std::max(0, state.active()->cursorCol - 1);
|
|
ASTNode* node = ast ? findNodeAtPosition(ast, lineZero, colZero) : nullptr;
|
|
if (!node) {
|
|
ImGui::TextDisabled("(no scope)");
|
|
} else {
|
|
auto crumbs = buildBreadcrumbTrail(node);
|
|
for (size_t i = 0; i < crumbs.size(); ++i) {
|
|
if (i > 0) {
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted(">");
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::PushID((int)i);
|
|
const auto& item = crumbs[i];
|
|
if (item.isRole) {
|
|
ImGui::TextDisabled("%s", item.label.c_str());
|
|
} else if (item.node && item.node->hasSpan()) {
|
|
if (ImGui::SmallButton(item.label.c_str())) {
|
|
state.jumpTo(state.active(), item.node->spanStartLine, item.node->spanStartCol);
|
|
}
|
|
} else {
|
|
ImGui::TextUnformatted(item.label.c_str());
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::PushFont(state.monoFont);
|
|
|
|
// Tab bar for the file
|
|
static bool renameOpen = false;
|
|
static char renameBuf[260] = {};
|
|
static std::string renameTarget;
|
|
if (ImGui::BeginTabBar("EditorTabs",
|
|
ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs)) {
|
|
if (state.buffers.bufferCount() == 0) {
|
|
if (ImGui::BeginTabItem("Welcome")) {
|
|
RenderWelcome(state.welcome, state, state.lastDialogPath);
|
|
ImGui::EndTabItem();
|
|
}
|
|
} else {
|
|
std::map<std::string, std::pair<int, int>> diagCounts;
|
|
if (state.lsp) {
|
|
for (const auto& d : state.lsp->getDiagnostics()) {
|
|
std::string path = EditorState::fromFileUri(d.uri);
|
|
auto& counts = diagCounts[path];
|
|
if (d.severity == 1) counts.first++;
|
|
else if (d.severity == 2) counts.second++;
|
|
}
|
|
}
|
|
for (const auto& d : state.whetstoneDiagnostics) {
|
|
std::string path = EditorState::fromFileUri(d.uri);
|
|
auto& counts = diagCounts[path];
|
|
if (d.severity == 1) counts.first++;
|
|
else if (d.severity == 2) counts.second++;
|
|
}
|
|
auto openBuffers = state.buffers.getOpenBuffers();
|
|
for (const auto& path : openBuffers) {
|
|
auto* buf = state.bufferStates[path].get();
|
|
if (!buf) continue;
|
|
std::string tabLabel = path;
|
|
if (buf->modified) tabLabel += " *";
|
|
auto it = diagCounts.find(path);
|
|
if (it != diagCounts.end()) {
|
|
int err = it->second.first;
|
|
int warn = it->second.second;
|
|
if (err > 0) tabLabel += " [E" + std::to_string(err) + "]";
|
|
else if (warn > 0) tabLabel += " [W" + std::to_string(warn) + "]";
|
|
}
|
|
bool open = true;
|
|
if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) {
|
|
if (!state.active() || state.active()->path != path) {
|
|
state.switchToBuffer(path);
|
|
}
|
|
const double nowSeconds = ImGui::GetTime();
|
|
float tabAlpha = state.uiAnimations.tabFadeAlpha(path,
|
|
nowSeconds,
|
|
state.settings.getReduceMotion());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
|
|
ImGui::GetStyle().Alpha * tabAlpha);
|
|
// Editable text area fills available space
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
avail.y -= 4; // small margin
|
|
|
|
state.updateHighlights();
|
|
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
|
state.updateGenerated();
|
|
}
|
|
std::vector<int> errorLines;
|
|
std::vector<int> warningLines;
|
|
std::vector<DiagnosticRange> diagRanges;
|
|
std::vector<AnnotationMarker> annoMarkers;
|
|
std::vector<SuggestionMarker> suggestionMarkers;
|
|
std::vector<AnnotationConflictMarker> conflictMarkers;
|
|
std::string activeUri = EditorState::toFileUri(buf->path);
|
|
if (state.lsp) {
|
|
auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri);
|
|
for (const auto& d : lspDiags) {
|
|
if (d.severity == 1) errorLines.push_back(d.range.start.line);
|
|
else if (d.severity == 2) warningLines.push_back(d.range.start.line);
|
|
DiagnosticRange dr;
|
|
dr.startLine = d.range.start.line;
|
|
dr.startCol = d.range.start.character;
|
|
dr.endLine = d.range.end.line;
|
|
dr.endCol = d.range.end.character;
|
|
dr.severity = d.severity;
|
|
dr.message = d.message;
|
|
diagRanges.push_back(std::move(dr));
|
|
}
|
|
}
|
|
for (const auto& d : state.whetstoneDiagnostics) {
|
|
if (d.uri != activeUri) continue;
|
|
if (d.severity == 1) errorLines.push_back(d.line);
|
|
else if (d.severity == 2) warningLines.push_back(d.line);
|
|
DiagnosticRange dr;
|
|
dr.startLine = d.line;
|
|
dr.startCol = d.character;
|
|
dr.endLine = d.line;
|
|
dr.endCol = d.character + 1;
|
|
dr.severity = d.severity;
|
|
dr.message = d.message;
|
|
diagRanges.push_back(std::move(dr));
|
|
}
|
|
if (state.isStructured()) {
|
|
Module* ast = state.activeAST();
|
|
if (ast) {
|
|
collectAnnotationMarkers(ast, annoMarkers);
|
|
std::vector<AnnotationConflict> conflicts;
|
|
collectAnnotationConflicts(ast, conflicts);
|
|
for (const auto& c : conflicts) {
|
|
AnnotationConflictMarker m;
|
|
m.childLine = c.childLine;
|
|
m.parentLine = c.parentLine;
|
|
m.message = c.message;
|
|
m.childAnnoId = c.childAnnoId;
|
|
m.parentAnnoId = c.parentAnnoId;
|
|
m.parentStrategy = c.parentStrategy;
|
|
conflictMarkers.push_back(std::move(m));
|
|
}
|
|
for (const auto& s : state.suggestions) {
|
|
if (s.confidence <= 0.5) continue;
|
|
ASTNode* target = findNodeById(ast, s.nodeId);
|
|
if (!target || !target->hasSpan()) continue;
|
|
SuggestionMarker sm;
|
|
sm.line = target->spanStartLine;
|
|
sm.confidence = s.confidence;
|
|
sm.reason = s.reason;
|
|
sm.annotationType = s.annotationType;
|
|
sm.strategy = s.strategy;
|
|
sm.nodeId = s.nodeId;
|
|
sm.label = "@" + s.annotationType.substr(0, s.annotationType.find("Annotation")) +
|
|
"(" + s.strategy + ")";
|
|
suggestionMarkers.push_back(std::move(sm));
|
|
}
|
|
}
|
|
}
|
|
|
|
CodeEditorResult res;
|
|
if (buf->language == "org") {
|
|
renderOrgDocument(state.emacsState.orgDoc,
|
|
buf->editBuf,
|
|
state.ui.showWhitespace,
|
|
state.monoFont,
|
|
state.uiFont,
|
|
state.settings.getLineHeightScale(),
|
|
state.settings.getLetterSpacing(),
|
|
[&](const std::string& newText) {
|
|
buf->editBuf = newText;
|
|
state.onTextChanged();
|
|
},
|
|
[&](int, const std::string&, const std::string&) {
|
|
buf->modified = true;
|
|
},
|
|
[&](const std::string& lang, const std::string& code) {
|
|
return state.runOrgBlock(lang, code);
|
|
});
|
|
res.cursorByte = buf->widget.getCursor();
|
|
} else {
|
|
CodeEditorOptions opts;
|
|
opts.showWhitespace = state.ui.showWhitespace;
|
|
opts.readOnly = buf->readOnly;
|
|
opts.mode = &buf->mode;
|
|
opts.enableFolding = true;
|
|
opts.showMinimap = state.ui.showMinimap;
|
|
opts.showAnnotations = state.ui.showAnnotations;
|
|
opts.showLineNumbers = state.ui.showLineNumbers;
|
|
opts.lineHeightScale = state.settings.getLineHeightScale();
|
|
opts.letterSpacing = state.settings.getLetterSpacing();
|
|
opts.cursorBlinkRate = state.settings.getCursorBlinkRate();
|
|
opts.reduceMotion = state.settings.getReduceMotion();
|
|
opts.nowSeconds = nowSeconds;
|
|
if (state.ui.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1;
|
|
else if (state.ui.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2;
|
|
else opts.annotationLayout = 0;
|
|
opts.errorLines = &errorLines;
|
|
opts.warningLines = &warningLines;
|
|
opts.diagnostics = &diagRanges;
|
|
opts.annotations = &annoMarkers;
|
|
opts.suggestions = &suggestionMarkers;
|
|
opts.conflicts = &conflictMarkers;
|
|
opts.searchPulseLine = state.search.pulseLine;
|
|
opts.searchPulseStart = state.search.pulseStart;
|
|
|
|
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
|
opts.syncScrollX = &buf->splitScrollX;
|
|
opts.syncScrollY = &buf->splitScrollY;
|
|
opts.scrollMaster = true;
|
|
|
|
ImGui::BeginTable("##editorSplit", 2,
|
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp);
|
|
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f);
|
|
ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f);
|
|
ImGui::TableNextRow();
|
|
ImGui::TableSetColumnIndex(0);
|
|
res = buf->widget.render("##editor",
|
|
buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), state.monoFont);
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
ImGui::BeginGroup();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Generated");
|
|
ImGui::SameLine();
|
|
const char* targetLabels[] = {"Python", "C++", "Elisp"};
|
|
const char* targetValues[] = {"python", "cpp", "elisp"};
|
|
int targetIndex = 0;
|
|
for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) {
|
|
if (buf->generatedLanguage == targetValues[i]) {
|
|
targetIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
if (ImGui::Combo("##targetLang", &targetIndex,
|
|
targetLabels, IM_ARRAYSIZE(targetLabels))) {
|
|
buf->generatedLanguage = targetValues[targetIndex];
|
|
buf->generatedMode.setLanguage(buf->generatedLanguage);
|
|
buf->generatedHighlightsDirty = true;
|
|
state.updateGenerated();
|
|
}
|
|
ImVec2 genAvail = ImGui::GetContentRegionAvail();
|
|
CodeEditorOptions genOpts;
|
|
genOpts.readOnly = true;
|
|
genOpts.showWhitespace = state.ui.showWhitespace;
|
|
genOpts.mode = &buf->generatedMode;
|
|
genOpts.showLineNumbers = state.ui.showLineNumbers;
|
|
genOpts.showCurrentLine = false;
|
|
genOpts.lineHeightScale = state.settings.getLineHeightScale();
|
|
genOpts.letterSpacing = state.settings.getLetterSpacing();
|
|
genOpts.cursorBlinkRate = state.settings.getCursorBlinkRate();
|
|
genOpts.reduceMotion = state.settings.getReduceMotion();
|
|
genOpts.nowSeconds = nowSeconds;
|
|
genOpts.highlightLine = buf->generatedHighlightLine;
|
|
genOpts.syncScrollX = &buf->splitScrollX;
|
|
genOpts.syncScrollY = &buf->splitScrollY;
|
|
genOpts.scrollMaster = false;
|
|
buf->generatedWidget.render("##generated",
|
|
buf->generatedBuf, buf->generatedHighlights, genOpts,
|
|
ImVec2(0, genAvail.y), state.monoFont);
|
|
ImGui::EndGroup();
|
|
ImGui::EndTable();
|
|
} else {
|
|
res = buf->widget.render("##editor",
|
|
buf->editBuf, buf->highlights, opts, avail, state.monoFont);
|
|
}
|
|
}
|
|
|
|
if (buf->language != "org") {
|
|
state.updateCursorPos(res.cursorByte);
|
|
if (res.ctrlClick && state.active()) {
|
|
state.goToDefinitionAt(res.ctrlClickLine, res.ctrlClickCol);
|
|
}
|
|
if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) {
|
|
Module* ast = state.activeAST();
|
|
if (ast) {
|
|
int genLines = countLines(buf->generatedBuf);
|
|
int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1));
|
|
buf->generatedHighlightLine = targetLine;
|
|
} else {
|
|
buf->generatedHighlightLine = -1;
|
|
}
|
|
}
|
|
if (res.changed) {
|
|
state.onTextChanged();
|
|
state.completionPending = true;
|
|
state.completionLastChange = ImGui::GetTime();
|
|
state.completionVisible = false;
|
|
state.completionSelected = 0;
|
|
if (state.lsp) state.lsp->clearCompletionItems();
|
|
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
|
int cursor = state.active()->widget.getCursor();
|
|
if (cursor > 0 && state.active()->editBuf[cursor - 1] == '(') {
|
|
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
|
int colZero = std::max(0, state.active()->cursorCol - 1);
|
|
state.lsp->requestSignatureHelp(EditorState::toFileUri(state.active()->path),
|
|
lineZero, colZero);
|
|
} else if (cursor > 0 && state.active()->editBuf[cursor - 1] == ')') {
|
|
state.lsp->clearSignatureHelp();
|
|
}
|
|
}
|
|
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
|
state.analysisPending = true;
|
|
state.analysisLastChange = ImGui::GetTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (res.suggestionClicked) {
|
|
state.suggestionPopup.annotationType = res.clickedSuggestion.annotationType;
|
|
state.suggestionPopup.strategy = res.clickedSuggestion.strategy;
|
|
state.suggestionPopup.reason = res.clickedSuggestion.reason;
|
|
state.suggestionPopup.confidence = res.clickedSuggestion.confidence;
|
|
state.suggestionPopup.nodeId = res.clickedSuggestion.nodeId;
|
|
state.showSuggestionPopup = true;
|
|
ImGui::OpenPopup("SuggestionPopup");
|
|
}
|
|
|
|
double now = ImGui::GetTime();
|
|
if (state.completionPending && (now - state.completionLastChange) > 0.2) {
|
|
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
|
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
|
int colZero = std::max(0, state.active()->cursorCol - 1);
|
|
state.lsp->requestCompletion(EditorState::toFileUri(state.active()->path),
|
|
lineZero, colZero);
|
|
}
|
|
state.completionPending = false;
|
|
}
|
|
|
|
if (state.symbolsPending && (now - state.symbolsLastChange) > 0.3) {
|
|
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
|
state.lsp->requestDocumentSymbols(EditorState::toFileUri(state.active()->path));
|
|
}
|
|
state.symbolsPending = false;
|
|
}
|
|
|
|
if (state.analysisPending && (now - state.analysisLastChange) > 0.5) {
|
|
if (state.isStructured()) {
|
|
auto result = state.pipeline.run(state.active()->editBuf,
|
|
state.active()->language,
|
|
state.active()->language);
|
|
if (result.success) {
|
|
state.whetstoneDiagnostics =
|
|
collectWhetstoneDiagnostics(result.validationDiags,
|
|
result.violations,
|
|
EditorState::toFileUri(state.active()->path));
|
|
state.appendUnusedImportDiagnostics(state.whetstoneDiagnostics);
|
|
} else {
|
|
state.whetstoneDiagnostics.clear();
|
|
}
|
|
Module* ast = state.activeAST();
|
|
if (ast) {
|
|
MemoryStrategyInference inf;
|
|
state.suggestions = inf.inferAnnotations(ast);
|
|
} else {
|
|
state.suggestions.clear();
|
|
}
|
|
} else {
|
|
state.whetstoneDiagnostics.clear();
|
|
state.suggestions.clear();
|
|
}
|
|
state.analysisPending = false;
|
|
}
|
|
|
|
if (state.lsp && state.active()) {
|
|
auto items = state.lsp->getCompletionItems();
|
|
int cursor = state.active()->widget.getCursor();
|
|
std::string prefix = EditorState::wordPrefixAt(state.active()->editBuf, cursor);
|
|
std::string nodeId;
|
|
if (state.activeAST()) {
|
|
ASTNode* scopeNode = findNodeAtPosition(state.activeAST(),
|
|
std::max(0, state.active()->cursorLine - 1),
|
|
std::max(0, state.active()->cursorCol - 1));
|
|
if (scopeNode) nodeId = scopeNode->id;
|
|
}
|
|
std::vector<PrimitiveSymbol> primitives;
|
|
auto funcs = state.library.primitives.getAvailableFunctions(nodeId);
|
|
auto types = state.library.primitives.getAvailableTypes(nodeId);
|
|
auto consts = state.library.primitives.getAvailableConstants(nodeId);
|
|
primitives.insert(primitives.end(), funcs.begin(), funcs.end());
|
|
primitives.insert(primitives.end(), types.begin(), types.end());
|
|
primitives.insert(primitives.end(), consts.begin(), consts.end());
|
|
|
|
auto built = buildLibraryAwareCompletions(items, primitives, prefix);
|
|
std::vector<LSPClient::CompletionItem> filtered;
|
|
filtered.reserve(built.items.size());
|
|
for (const auto& item : built.items) {
|
|
const std::string& key = item.filterText.empty() ? item.label : item.filterText;
|
|
if (prefix.empty() || key.rfind(prefix, 0) == 0) filtered.push_back(item);
|
|
}
|
|
|
|
state.completionVisible = !filtered.empty();
|
|
if (state.completionVisible) {
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f,
|
|
ImGui::GetWindowPos().y + 60.0f));
|
|
ImGui::BeginChild("##completionPopup", ImVec2(300, 150), true);
|
|
int maxIndex = std::max(0, (int)filtered.size() - 1);
|
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
|
state.completionSelected = std::min(state.completionSelected + 1, maxIndex);
|
|
}
|
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
|
state.completionSelected = std::max(state.completionSelected - 1, 0);
|
|
}
|
|
bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Tab);
|
|
bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape);
|
|
|
|
if (dismiss) {
|
|
state.lsp->clearCompletionItems();
|
|
state.completionVisible = false;
|
|
}
|
|
|
|
auto deriveLibrary = [](const std::string& name) {
|
|
auto pos = name.find("::");
|
|
if (pos != std::string::npos) return name.substr(0, pos);
|
|
pos = name.find('.');
|
|
if (pos != std::string::npos) return name.substr(0, pos);
|
|
return std::string();
|
|
};
|
|
|
|
for (int i = 0; i < (int)filtered.size(); ++i) {
|
|
std::string itemLabel = filtered[i].label;
|
|
if (filtered[i].kind != 0) {
|
|
itemLabel = "[" + std::to_string(filtered[i].kind) + "] " + itemLabel;
|
|
}
|
|
bool selected = (i == state.completionSelected);
|
|
bool preferred = built.preferredNames.count(filtered[i].label) > 0;
|
|
std::string libHint = deriveLibrary(filtered[i].label);
|
|
if (!preferred && !libHint.empty()) itemLabel += " (auto-import)";
|
|
if (!preferred) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f));
|
|
}
|
|
if (ImGui::Selectable(itemLabel.c_str(), selected)) {
|
|
state.completionSelected = i;
|
|
accept = true;
|
|
}
|
|
if (!preferred) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
if (accept && !filtered.empty()) {
|
|
const auto& item = filtered[state.completionSelected];
|
|
bool preferred = built.preferredNames.count(item.label) > 0;
|
|
if (!preferred) {
|
|
std::string libHint = deriveLibrary(item.label);
|
|
if (!libHint.empty()) {
|
|
state.ensureImportForSymbol(libHint, item.label);
|
|
}
|
|
}
|
|
int start = EditorState::wordStartAt(state.active()->editBuf, cursor);
|
|
state.applyCompletion(state.active(), item.insertText, start, cursor);
|
|
state.lsp->clearCompletionItems();
|
|
state.completionVisible = false;
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
}
|
|
|
|
if (state.showSuggestionPopup) {
|
|
if (ImGui::BeginPopupModal("SuggestionPopup", &state.showSuggestionPopup, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("%s(%s)", state.suggestionPopup.annotationType.c_str(),
|
|
state.suggestionPopup.strategy.c_str());
|
|
ImGui::Separator();
|
|
ImGui::TextWrapped("%s", state.suggestionPopup.reason.c_str());
|
|
ImGui::Text("Confidence: %.2f", state.suggestionPopup.confidence);
|
|
if (ImGui::Button("Apply")) {
|
|
Module* ast = state.mutationAST();
|
|
if (ast) {
|
|
ASTNode* target = findNodeById(ast, state.suggestionPopup.nodeId);
|
|
if (target) {
|
|
auto* anno = createAnnotationNode(state.suggestionPopup.annotationType,
|
|
state.suggestionPopup.strategy);
|
|
if (anno) {
|
|
if (target->hasSpan()) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
}
|
|
state.mutator.setRoot(ast);
|
|
auto res2 = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res2.error.empty()) state.notify(NotificationLevel::Error, res2.error);
|
|
state.applyOrchestratorToActive();
|
|
}
|
|
}
|
|
}
|
|
state.showSuggestionPopup = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
state.showSuggestionPopup = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
if (state.lsp && state.active()) {
|
|
if (ImGui::IsWindowHovered()) {
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
if (mouse.x != state.hoverLastMouse.x || mouse.y != state.hoverLastMouse.y) {
|
|
state.hoverLastMouse = mouse;
|
|
state.hoverLastMove = ImGui::GetTime();
|
|
state.hoverPending = true;
|
|
state.lsp->clearHover();
|
|
}
|
|
if (state.hoverPending && (ImGui::GetTime() - state.hoverLastMove) > 0.5) {
|
|
if (state.active()->path.rfind("(untitled", 0) != 0) {
|
|
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
|
int colZero = std::max(0, state.active()->cursorCol - 1);
|
|
state.lsp->requestHover(EditorState::toFileUri(state.active()->path),
|
|
lineZero, colZero);
|
|
}
|
|
state.hoverPending = false;
|
|
}
|
|
std::string hover = state.lsp->getHoverContents();
|
|
static std::string lastHoverText;
|
|
bool hoverActive = !hover.empty();
|
|
if (hoverActive) lastHoverText = hover;
|
|
if (!lastHoverText.empty()) {
|
|
renderRichTooltip("lsp_hover",
|
|
lastHoverText,
|
|
hoverActive,
|
|
state.settings.getReduceMotion(),
|
|
state.monoFont);
|
|
}
|
|
}
|
|
|
|
if (state.definitionPending) {
|
|
auto defs = state.lsp->getDefinitionLocations();
|
|
if (!defs.empty()) {
|
|
state.definitionPending = false;
|
|
state.definitionPreview = defs.front();
|
|
state.definitionPreviewValid = true;
|
|
state.lsp->clearDefinitionLocations();
|
|
state.jumpToDefinitionLocation(state.definitionPreview);
|
|
} else if ((ImGui::GetTime() - state.definitionLastRequest) > 0.5) {
|
|
state.definitionPending = false;
|
|
state.goToDefinitionFallback(state.definitionRequestLine,
|
|
state.definitionRequestCol);
|
|
}
|
|
}
|
|
|
|
auto sig = state.lsp->getSignatureHelp();
|
|
if (!sig.label.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + 20.0f,
|
|
ImGui::GetWindowPos().y + ImGui::GetWindowHeight() - 80.0f));
|
|
ImGui::BeginChild("##signatureHelp", ImVec2(420, 60), true);
|
|
ImGui::TextUnformatted(sig.label.c_str());
|
|
ImGui::EndChild();
|
|
}
|
|
}
|
|
|
|
if (res.hoverValid && (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) {
|
|
std::string preview;
|
|
if (state.previewDefinitionAt(res.hoverLine, res.hoverCol, preview)) {
|
|
renderRichTooltip("definition_preview",
|
|
preview,
|
|
true,
|
|
state.settings.getReduceMotion(),
|
|
state.monoFont);
|
|
} else if (state.definitionPreviewValid) {
|
|
std::string path = EditorState::fromFileUri(state.definitionPreview.uri);
|
|
std::string label = path + ":" +
|
|
std::to_string(state.definitionPreview.range.start.line + 1);
|
|
renderRichTooltip("definition_preview_fallback",
|
|
label,
|
|
true,
|
|
state.settings.getReduceMotion(),
|
|
state.monoFont);
|
|
}
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextWindow("AnnotationContext", ImGuiPopupFlags_MouseButtonRight)) {
|
|
Module* ast = state.mutationAST();
|
|
int lineZero = state.active() ? std::max(0, state.active()->cursorLine - 1) : 0;
|
|
ASTNode* target = ast ? findAnnotationTarget(ast, lineZero) : nullptr;
|
|
if (!target) {
|
|
ImGui::TextDisabled("No annotatable symbol on this line.");
|
|
} else {
|
|
bool mutated = false;
|
|
ImGui::Text("Target: %s", nodeDisplayName(target).c_str());
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginMenu("Add Annotation")) {
|
|
if (ImGui::MenuItem("@Reclaim(Tracing)")) {
|
|
auto* anno = createAnnotationNode("ReclaimAnnotation", "Tracing");
|
|
if (anno) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("@Owner(Single)")) {
|
|
auto* anno = createAnnotationNode("OwnerAnnotation", "Single");
|
|
if (anno) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("@Deallocate(Explicit)")) {
|
|
auto* anno = createAnnotationNode("DeallocateAnnotation", "Explicit");
|
|
if (anno) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("@Lifetime(RAII)")) {
|
|
auto* anno = createAnnotationNode("LifetimeAnnotation", "RAII");
|
|
if (anno) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("@Allocate(Static)")) {
|
|
auto* anno = createAnnotationNode("AllocateAnnotation", "Static");
|
|
if (anno) {
|
|
anno->setSpan(target->spanStartLine, target->spanStartCol,
|
|
target->spanEndLine, target->spanEndCol);
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.insertNode(target->id, "annotations", anno);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
const auto& annos = target->getChildren("annotations");
|
|
if (ImGui::BeginMenu("Edit Annotation", !annos.empty())) {
|
|
for (auto* anno : annos) {
|
|
std::string label = annotationLabel(anno);
|
|
if (ImGui::BeginMenu(label.c_str())) {
|
|
if (anno->conceptType == "ReclaimAnnotation") {
|
|
if (ImGui::MenuItem("Tracing")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(anno->id, "strategy", "Tracing");
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
} else if (anno->conceptType == "OwnerAnnotation") {
|
|
if (ImGui::MenuItem("Single")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(anno->id, "strategy", "Single");
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
} else if (anno->conceptType == "DeallocateAnnotation") {
|
|
if (ImGui::MenuItem("Explicit")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(anno->id, "strategy", "Explicit");
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
} else if (anno->conceptType == "LifetimeAnnotation") {
|
|
if (ImGui::MenuItem("RAII")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(anno->id, "strategy", "RAII");
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
} else if (anno->conceptType == "AllocateAnnotation") {
|
|
if (ImGui::MenuItem("Static")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(anno->id, "strategy", "Static");
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Remove Annotation", !annos.empty())) {
|
|
for (auto* anno : annos) {
|
|
std::string label = annotationLabel(anno);
|
|
if (ImGui::MenuItem(label.c_str())) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.deleteNode(anno->id);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
std::vector<AnnotationConflict> conflicts;
|
|
collectAnnotationConflicts(ast, conflicts);
|
|
bool conflictOnLine = false;
|
|
AnnotationConflict lineConflict;
|
|
for (const auto& c : conflicts) {
|
|
if (c.childLine == lineZero || c.parentLine == lineZero) {
|
|
lineConflict = c;
|
|
conflictOnLine = true;
|
|
break;
|
|
}
|
|
}
|
|
if (ImGui::BeginMenu("Resolve Conflict", conflictOnLine)) {
|
|
if (conflictOnLine) {
|
|
if (ImGui::MenuItem("Remove child annotation")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.deleteNode(lineConflict.childAnnoId);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
if (ImGui::MenuItem("Match parent strategy")) {
|
|
state.mutator.setRoot(ast);
|
|
auto res = state.mutator.setProperty(lineConflict.childAnnoId,
|
|
"strategy",
|
|
lineConflict.parentStrategy);
|
|
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
|
|
if (res.error.empty()) mutated = true;
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (mutated) {
|
|
state.applyOrchestratorToActive();
|
|
}
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
if (path.rfind("(untitled", 0) == 0) {
|
|
renameOpen = true;
|
|
renameTarget = path;
|
|
std::snprintf(renameBuf, sizeof(renameBuf), "%s", path.c_str());
|
|
ImGui::OpenPopup("Rename Buffer");
|
|
}
|
|
}
|
|
if (!open) {
|
|
state.buffers.closeBuffer(path);
|
|
state.bufferStates.erase(path);
|
|
if (path.rfind("(untitled", 0) != 0) state.watcher.unwatch(path);
|
|
if (state.buffers.bufferCount() > 0) {
|
|
state.switchToBuffer(state.buffers.getOpenBuffers().front());
|
|
} else {
|
|
state.activeBuffer = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
|
|
if (renameOpen && ImGui::BeginPopupModal("Rename Buffer", nullptr,
|
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::TextUnformatted("Rename untitled buffer:");
|
|
ImGui::SetNextItemWidth(320.0f);
|
|
ImGui::InputText("##renameBuf", renameBuf, sizeof(renameBuf));
|
|
if (ImGui::Button("Rename")) {
|
|
std::string newName = renameBuf;
|
|
if (!newName.empty() && renameTarget.rfind("(untitled", 0) == 0) {
|
|
state.renameBufferPath(renameTarget, newName);
|
|
}
|
|
renameOpen = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
renameOpen = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::PopFont();
|
|
ImGui::End();
|
|
}
|