1
0
mirror of synced 2025-01-11 05:42:15 +01:00

feat: Added clickable links to error messages in the pattern editor (#1988)

Errors printed in the console can be clicked to have the cursor jump to
the source code line where the error is at.

The mouse cursor changes its shape to indicate which parts of the error
message can be clicked on the console. When the cursor jumps, the text
editor takes the focus away from the console and it scrolls the window
to make the line with the error is visible if it isn't. This code uses
the function created for the go-to PR but adds code to switch focus to
the target. When the codes are merged please keep both the part that
jumps the cursor and the part that sets the focus.

---------

Co-authored-by: Nik <werwolv98@gmail.com>
This commit is contained in:
paxcut 2024-12-06 14:45:36 -07:00 committed by GitHub
parent d02e170dac
commit cf09029847
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 202 additions and 25 deletions

View File

@ -9,6 +9,7 @@
#include <map>
#include <regex>
#include "imgui.h"
#include "imgui_internal.h"
class TextEditor
{
@ -132,11 +133,72 @@ public:
using Identifiers = std::unordered_map<std::string, Identifier>;
using Keywords = std::unordered_set<std::string> ;
using ErrorMarkers = std::map<Coordinates, std::pair<uint32_t ,std::string>>;
using ErrorHoverBoxes = std::map<Coordinates, std::pair<ImVec2,ImVec2>>;
using Breakpoints = std::unordered_set<uint32_t>;
using Palette = std::array<ImU32, (uint32_t)PaletteIndex::Max>;
using Char = uint8_t ;
class ActionableBox {
ImRect mBox;
public:
ActionableBox()=default;
explicit ActionableBox(const ImRect &box) : mBox(box) {}
std::function<void()> mCallback;
virtual bool trigger() {
return ImGui::IsMouseHoveringRect(mBox.Min,mBox.Max);
}
void setCallback(const std::function<void()> &callback) { mCallback = callback; }
};
class CursorChangeBox : public ActionableBox {
public:
CursorChangeBox()=default;
explicit CursorChangeBox(const ImRect &box) : ActionableBox(box) {
setCallback([]() {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
});
}
};
class ErrorGotoBox : public ActionableBox {
Coordinates mPos;
public:
ErrorGotoBox()=default;
ErrorGotoBox(const ImRect &box, const Coordinates &pos, TextEditor *editor) : ActionableBox(box), mPos(pos) {
setCallback( [this,editor]() {
editor->JumpToCoords(mPos);
});
}
bool trigger() override {
return ActionableBox::trigger() && ImGui::IsMouseClicked(0);
}
};
using ErrorGotoBoxes = std::map<Coordinates, ErrorGotoBox>;
using CursorBoxes = std::map<Coordinates, CursorChangeBox>;
class ErrorHoverBox : public ActionableBox {
Coordinates mPos;
std::string mErrorText;
public:
ErrorHoverBox()=default;
ErrorHoverBox(const ImRect &box, const Coordinates &pos,const char *errorText) : ActionableBox(box), mPos(pos), mErrorText(errorText) {
setCallback([this]() {
ImGui::BeginTooltip();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
ImGui::Text("Error at line %d:", mPos.mLine);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.2f, 1.0f));
ImGui::TextUnformatted(mErrorText.c_str());
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
);
}
};
using ErrorHoverBoxes = std::map<Coordinates, ErrorHoverBox>;
struct Glyph
{
Char mChar;
@ -188,6 +250,23 @@ public:
static const LanguageDefinition& AngelScript();
static const LanguageDefinition& Lua();
};
void ClearErrorMarkers() {
mErrorMarkers.clear();
mErrorHoverBoxes.clear();
}
void ClearGotoBoxes() {
mErrorGotoBoxes.clear();
}
void ClearCursorBoxes() {
mCursorBoxes.clear();
}
void ClearActionables() {
ClearErrorMarkers();
ClearGotoBoxes();
ClearCursorBoxes();
}
struct Selection {
Coordinates mStart;
@ -210,6 +289,8 @@ public:
void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
void SetText(const std::string& aText);
void JumpToLine(int line);
void JumpToCoords(const Coordinates &coords);
std::string GetText() const;
bool isEmpty() const {
auto text = GetText();
@ -221,12 +302,32 @@ public:
std::string GetSelectedText() const;
std::string GetCurrentLineText()const;
std::string GetLineText(int line)const;
void SetSourceCodeEditor(TextEditor *editor) { mSourceCodeEditor = editor; }
TextEditor *GetSourceCodeEditor() {
if(mSourceCodeEditor!=nullptr)
return mSourceCodeEditor;
return this;
}
class FindReplaceHandler;
public:
void AddClickableText(std::string text) {
mClickableText.push_back(text);
}
void ClearClickableText() {
mClickableText.clear();
}
FindReplaceHandler *GetFindReplaceHandler() { return &mFindReplaceHandler; }
int GetTotalLines() const { return (int)mLines.size(); }
bool IsOverwrite() const { return mOverwrite; }
void setFocusAtCoords(const Coordinates &coords) {
mFocusAtCoords = coords;
mUpdateFocus = true;
}
void SetOverwrite(bool aValue) { mOverwrite = aValue; }
void SetReadOnly(bool aValue);
@ -244,8 +345,10 @@ public:
Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
void SetCursorPosition(const Coordinates& aPosition);
bool RaiseContextMenu() { return mRaiseContextMenu; }
void ClearRaiseContextMenu() { mRaiseContextMenu = false; }
inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;}
inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; }
@ -491,11 +594,14 @@ private:
Breakpoints mBreakpoints;
ErrorMarkers mErrorMarkers;
ErrorHoverBoxes mErrorHoverBoxes;
ErrorGotoBoxes mErrorGotoBoxes;
CursorBoxes mCursorBoxes;
ImVec2 mCharAdvance;
Coordinates mInteractiveStart, mInteractiveEnd;
std::string mLineBuffer;
uint64_t mStartTime;
std::vector<std::string> mDefines;
TextEditor *mSourceCodeEditor=nullptr;
float m_linesAdded = 0;
float m_savedScrollY = 0;
float m_pixelsAdded = 0;
@ -504,6 +610,10 @@ private:
bool mShowCursor;
bool mShowLineNumbers;
bool mRaiseContextMenu = false;
Coordinates mFocusAtCoords;
bool mUpdateFocus = false;
std::vector<std::string> mClickableText;
static const int sCursorBlinkInterval;
static const int sCursorBlinkOnTime;

View File

@ -982,6 +982,48 @@ void TextEditor::Render() {
}
}
// Render goto buttons
auto lineText = GetLineText(lineNo);
Coordinates gotoKey = Coordinates(lineNo + 1, 0);
std::string errorLineColumn;
bool found = false;
for (auto text : mClickableText) {
if (lineText.find(text) == 0) {
errorLineColumn = lineText.substr(text.size());
if (!errorLineColumn.empty()) {
found = true;
break;
}
}
}
if (found) {
int currLine = 0, currColumn = 0;
if (auto idx = errorLineColumn.find(":"); idx != std::string::npos) {
auto errorLine = errorLineColumn.substr(0, idx);
if (!errorLine.empty())
currLine = std::stoi(errorLine) - 1;
auto errorColumn = errorLineColumn.substr(idx + 1);
if (!errorColumn.empty())
currColumn = std::stoi(errorColumn) - 1;
}
TextEditor::Coordinates errorPos = {currLine, currColumn};
ImVec2 errorStart = ImVec2(lineStartScreenPos.x, lineStartScreenPos.y);
ImVec2 errorEnd = ImVec2( lineStartScreenPos.x + TextDistanceToLineStart(Coordinates(lineNo, GetLineCharacterCount(lineNo))), lineStartScreenPos.y + mCharAdvance.y);
ErrorGotoBox box = ErrorGotoBox(ImRect({errorStart, errorEnd}), errorPos, GetSourceCodeEditor());
mErrorGotoBoxes[gotoKey] = box;
CursorChangeBox cursorBox = CursorChangeBox(ImRect({errorStart, errorEnd}));
mCursorBoxes[gotoKey] = cursorBox;
}
if (mCursorBoxes.find(gotoKey) != mCursorBoxes.end()) {
auto box = mCursorBoxes[gotoKey];
if (box.trigger()) box.mCallback();
}
if (mErrorGotoBoxes.find(gotoKey) != mErrorGotoBoxes.end()) {
auto box = mErrorGotoBoxes[gotoKey];
if (box.trigger()) box.mCallback();
}
// Render colorized text
auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]);
ImVec2 bufferOffset;
@ -1004,17 +1046,35 @@ void TextEditor::Render() {
mLineBuffer.clear();
}
if (underwaved) {
auto textStart = TextDistanceToLineStart(Coordinates(lineNo, i)) + mTextStart;
auto textStart = TextDistanceToLineStart(Coordinates(lineNo, i));
auto begin = ImVec2(lineStartScreenPos.x + textStart, lineStartScreenPos.y);
auto errorLength = errorIt->second.first;
auto errorMessage = errorIt->second.second;
if (errorLength == 0)
errorLength = line.size() - i - 1;
auto end = Underwaves(begin, errorLength, mPalette[(int32_t) PaletteIndex::ErrorMarker]);
mErrorHoverBoxes[Coordinates(lineNo+1,i+1)]=std::make_pair(begin,end);
Coordinates key = Coordinates(lineNo+1,i+1);
ErrorHoverBox box = ErrorHoverBox(ImRect({begin, end}), key, errorMessage.c_str());
mErrorHoverBoxes[key] = box;
}
Coordinates key = Coordinates(lineNo + 1, i + 1);
if (mErrorHoverBoxes.find(key) != mErrorHoverBoxes.end()) {
auto box = mErrorHoverBoxes[key];
if (box.trigger()) box.mCallback();
}
prevColor = color;
if (mUpdateFocus && mFocusAtCoords == Coordinates(lineNo, i)) {
mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = mFocusAtCoords;
mSelectionMode = SelectionMode::Normal;
SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode);
ResetCursorBlinkTime();
EnsureCursorVisible();
ImGui::SetKeyboardFocusHere(-1);
mUpdateFocus = false;
}
if (glyph.mChar == '\t') {
auto oldX = bufferOffset.x;
bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize);
@ -1070,22 +1130,6 @@ void TextEditor::Render() {
mScrollToCursor = false;
}
for (auto [key,value] : mErrorMarkers) {
auto start = mErrorHoverBoxes[key].first;
auto end = mErrorHoverBoxes[key].second;
if (ImGui::IsMouseHoveringRect(start, end)) {
ImGui::BeginTooltip();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
ImGui::Text("Error at line %d:", key.mLine);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.2f, 1.0f));
ImGui::Text("%s", value.second.c_str());
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGuiPopupFlags_ popup_flags = ImGuiPopupFlags_None;
ImGuiContext& g = *GImGui;
auto oldTopMargin = mTopMargin;
@ -1553,6 +1597,19 @@ void TextEditor::DeleteSelection() {
Colorize(mState.mSelectionStart.mLine, 1);
}
void TextEditor::JumpToLine(int line) {
auto newPos = Coordinates(line, 0);
JumpToCoords(newPos);
setFocusAtCoords(newPos);
}
void TextEditor::JumpToCoords(const Coordinates &aNewPos) {
SetSelection(aNewPos, aNewPos);
SetCursorPosition(aNewPos);
EnsureCursorVisible();
setFocusAtCoords(aNewPos);
}
void TextEditor::MoveUp(int aAmount, bool aSelect) {
ResetCursorBlinkTime();
auto oldPos = mState.mCursorPosition;
@ -2478,10 +2535,12 @@ std::string TextEditor::GetSelectedText() const {
}
std::string TextEditor::GetCurrentLineText() const {
auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine);
return GetText(
Coordinates(mState.mCursorPosition.mLine, 0),
Coordinates(mState.mCursorPosition.mLine, lineLength));
return GetLineText(mState.mCursorPosition.mLine);
}
std::string TextEditor::GetLineText(int line) const {
auto lineLength = GetLineCharacterCount(line);
return GetText(Coordinates(line, 0),Coordinates(line, lineLength));
}
void TextEditor::ProcessInputs() {

View File

@ -214,6 +214,13 @@ namespace hex::plugin::builtin {
m_consoleEditor.SetReadOnly(true);
m_consoleEditor.SetShowCursor(false);
m_consoleEditor.SetShowLineNumbers(false);
m_consoleEditor.SetSourceCodeEditor(&m_textEditor);
std::string sourcecode = pl::api::Source::DefaultSource;
std::string error = "E: ";
std::string end = ":";
std::string arrow = " --> in ";
m_consoleEditor.AddClickableText(error + sourcecode + end);
m_consoleEditor.AddClickableText(error + arrow + sourcecode + end);
this->registerEvents();
this->registerMenuItems();
@ -1753,8 +1760,9 @@ namespace hex::plugin::builtin {
m_runningEvaluators += 1;
m_executionDone.get(provider) = false;
m_textEditor.ClearActionables();
m_textEditor.SetErrorMarkers({});
m_consoleEditor.ClearActionables();
m_console.get(provider).clear();
m_consoleNeedsUpdate = true;