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:
parent
d02e170dac
commit
cf09029847
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user