From cf09029847c88b1987bebf95c89906e0e2bd08eb Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:45:36 -0700 Subject: [PATCH] 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 --- .../ColorTextEditor/include/TextEditor.h | 114 +++++++++++++++++- .../ColorTextEditor/source/TextEditor.cpp | 103 ++++++++++++---- .../content/views/view_pattern_editor.cpp | 10 +- 3 files changed, 202 insertions(+), 25 deletions(-) diff --git a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h index 3b8ebf7fb..ffedde538 100644 --- a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h +++ b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h @@ -9,6 +9,7 @@ #include #include #include "imgui.h" +#include "imgui_internal.h" class TextEditor { @@ -132,12 +133,73 @@ public: using Identifiers = std::unordered_map; using Keywords = std::unordered_set ; using ErrorMarkers = std::map>; - using ErrorHoverBoxes = std::map>; using Breakpoints = std::unordered_set; using Palette = std::array; using Char = uint8_t ; - struct Glyph + class ActionableBox { + + ImRect mBox; + public: + ActionableBox()=default; + explicit ActionableBox(const ImRect &box) : mBox(box) {} + std::function mCallback; + virtual bool trigger() { + return ImGui::IsMouseHoveringRect(mBox.Min,mBox.Max); + } + void setCallback(const std::function &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; + using CursorBoxes = std::map; + + 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; + + struct Glyph { Char mChar; PaletteIndex mColorIndex = PaletteIndex::Default; @@ -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 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 mClickableText; static const int sCursorBlinkInterval; static const int sCursorBlinkOnTime; diff --git a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp index 33cbe5236..3bcc66c22 100644 --- a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp +++ b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp @@ -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() { diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index 72d86a7db..cc31901f4 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -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;