Fix: Very long lines in text editor could make it hang. (#2426)

Fixed by only processing text that's visible.

Also fixed the cursor jumping to breakpoint line when selected by
clicking the line number and added highlighting of the current editing
line. An optimization that caches the number of utf-8 chars in each line
was included as well.
Finally, an error that caused ImHex to crash if a pattern was saved as
itself was also fixed.
This commit is contained in:
paxcut
2025-08-30 20:01:05 -07:00
committed by GitHub
parent 76bb0e420d
commit c8caf6124e
7 changed files with 103 additions and 69 deletions

View File

@@ -2665,7 +2665,7 @@ namespace hex::plugin::builtin {
file.writeString(wolv::util::trim(m_textEditor.get(provider).getText()));
m_patternFileDirty.get(provider) = false;
auto loadedPath = m_changeTracker.get(provider).getPath();
if ((loadedPath.empty() && loadedPath != path) || (!loadedPath.empty() && !trackFile))
if ((loadedPath.empty() && loadedPath != path) || (!loadedPath.empty() && !trackFile) || loadedPath == path)
m_changeTracker.get(provider).stopTracking();
if (trackFile) {

View File

@@ -324,13 +324,16 @@ namespace hex::ui {
std::string m_colors;
std::string m_flags;
bool m_colorized = false;
i32 m_lineMaxColumn;
Line() : m_chars(), m_colors(), m_flags(), m_colorized(false) {}
Line() : m_chars(), m_colors(), m_flags(), m_colorized(false), m_lineMaxColumn(-1) {}
explicit Line(const char *line) { Line(std::string(line)); }
explicit Line(const std::string &line) : m_chars(line), m_colors(std::string(line.size(), 0x00)), m_flags(std::string(line.size(), 0x00)), m_colorized(false) {}
Line(const Line &line) : m_chars(line.m_chars), m_colors(line.m_colors), m_flags(line.m_flags), m_colorized(line.m_colorized) {}
explicit Line(const std::string &line) :
m_chars(line), m_colors(std::string(line.size(), 0x00)), m_flags(std::string(line.size(), 0x00)), m_colorized(false), m_lineMaxColumn(getMaxCharColumn()) {}
Line(const Line &line) : m_chars(line.m_chars), m_colors(line.m_colors), m_flags(line.m_flags), m_colorized(line.m_colorized), m_lineMaxColumn(line.m_lineMaxColumn) {}
i32 getCharacterColumn(i32 index) const;
i32 getMaxCharColumn() const;
LineIterator begin();
LineIterator end();
Line &operator=(const Line &line);
@@ -481,7 +484,7 @@ namespace hex::ui {
void setLongestLineLength(u64 line) { m_longestLineLength = line; }
u64 getLongestLineLength() const { return m_longestLineLength; }
void setTopMarginChanged(i32 newMargin);
void setFocusAtCoords(const Coordinates &coords);
void setFocusAtCoords(const Coordinates &coords, bool ensureVisible = false);
void clearErrorMarkers();
void clearActionables();
private:
@@ -533,10 +536,10 @@ namespace hex::ui {
void setOverwrite(bool value) { m_overwrite = value; }
bool isOverwrite() const { return m_overwrite; }
void setText(const std::string &text, bool undo = false);
std::string getText() const;
std::string getText();
std::vector<std::string> getTextLines() const;
std::string getSelectedText() const;
std::string getLineText(i32 line) const;
std::string getSelectedText();
std::string getLineText(i32 line);
inline void setTextChanged(bool value) { m_textChanged = value; }
inline bool isTextChanged() { return m_textChanged; }
inline void setReadOnly(bool value) { m_readOnly = value; }
@@ -545,7 +548,7 @@ namespace hex::ui {
inline void setHandleKeyboardInputs(bool value) { m_handleKeyboardInputs = value; }
inline bool isHandleKeyboardInputsEnabled() const { return m_handleKeyboardInputs; }
private:
std::string getText(const Selection &selection) const;
std::string getText(const Selection &selection);
void deleteRange(const Selection &selection);
i32 insertTextAt(Coordinates &where, const std::string &value);
void removeLine(i32 start, i32 end);
@@ -568,18 +571,18 @@ namespace hex::ui {
void moveEnd(bool select = false);
void moveToMatchedBracket(bool select = false);
void setScrollY();
Coordinates getCursorPosition() const { return setCoordinates(m_state.m_cursorPosition); }
Coordinates getCursorPosition() { return setCoordinates(m_state.m_cursorPosition); }
void setCursorPosition(const Coordinates &position);
void setCursorPosition();
private:
Coordinates setCoordinates(const Coordinates &value) const;
Coordinates setCoordinates(i32 line, i32 column) const;
Selection setCoordinates(const Selection &value) const;
Coordinates setCoordinates(const Coordinates &value);
Coordinates setCoordinates(i32 line, i32 column);
Selection setCoordinates(const Selection &value);
void advance(Coordinates &coordinates) const;
Coordinates findWordStart(const Coordinates &from) const;
Coordinates findWordEnd(const Coordinates &from) const;
Coordinates findPreviousWord(const Coordinates &from) const;
Coordinates findNextWord(const Coordinates &from) const;
Coordinates findWordStart(const Coordinates &from);
Coordinates findWordEnd(const Coordinates &from);
Coordinates findPreviousWord(const Coordinates &from);
Coordinates findNextWord(const Coordinates &from);
u32 skipSpaces(const Coordinates &from);
//Support
public:
@@ -614,13 +617,13 @@ namespace hex::ui {
static TextEditor::Coordinates stringIndexToCoordinates(i32 strIndex, const std::string &input);
private:
Coordinates screenPosToCoordinates(const ImVec2 &position) const;
Coordinates screenPosToCoordinates(const ImVec2 &position);
Coordinates lineCoordsToIndexCoords(const Coordinates &coordinates) const;
i32 lineCoordinatesToIndex(const Coordinates &coordinates) const;
Coordinates getCharacterCoordinates(i32 line, i32 index) const;
i32 getLineCharacterCount(i32 line) const;
Coordinates getCharacterCoordinates(i32 line, i32 index);
i32 getLineCharacterCount(i32 line);
u64 getLineByteCount(i32 line) const;
i32 getLineMaxColumn(i32 line) const;
i32 getLineMaxColumn(i32 line);
public:
FindReplaceHandler m_findReplaceHandler;
@@ -680,6 +683,7 @@ namespace hex::ui {
bool m_raiseContextMenu = false;
Coordinates m_focusAtCoords = {};
bool m_updateFocus = false;
bool m_ensureCursorVisible = false;
std::vector<std::string> m_clickableText;

View File

@@ -29,7 +29,7 @@ namespace hex::ui {
TextEditor::~TextEditor() {
}
std::string TextEditor::getText(const Selection &from) const {
std::string TextEditor::getText(const Selection &from) {
std::string result;
auto selection = setCoordinates(from);
auto columns = selection.getSelectedColumns();
@@ -734,7 +734,7 @@ namespace hex::ui {
refreshSearchResults();
}
std::string TextEditor::getText() const {
std::string TextEditor::getText() {
auto start = setCoordinates(0, 0);
auto end = setCoordinates(-1, -1);
if (start == Invalid || end == Invalid)
@@ -755,11 +755,11 @@ namespace hex::ui {
return result;
}
std::string TextEditor::getSelectedText() const {
std::string TextEditor::getSelectedText() {
return getText(m_state.m_selection);
}
std::string TextEditor::getLineText(i32 line) const {
std::string TextEditor::getLineText(i32 line) {
auto sanitizedLine = setCoordinates(line, 0);
auto endLine = setCoordinates(line, -1);
if (sanitizedLine == Invalid || endLine == Invalid)

View File

@@ -328,7 +328,7 @@ namespace hex::ui {
setCursorPosition(m_state.m_selection.m_end);
}
TextEditor::Coordinates TextEditor::setCoordinates(i32 line, i32 column) const {
TextEditor::Coordinates TextEditor::setCoordinates(i32 line, i32 column) {
if (isEmpty())
return Coordinates(0, 0);
@@ -348,12 +348,12 @@ namespace hex::ui {
return result;
}
TextEditor::Coordinates TextEditor::setCoordinates(const Coordinates &value) const {
TextEditor::Coordinates TextEditor::setCoordinates(const Coordinates &value) {
auto sanitized_value = setCoordinates(value.m_line, value.m_column);
return sanitized_value;
}
TextEditor::Selection TextEditor::setCoordinates(const Selection &value) const {
TextEditor::Selection TextEditor::setCoordinates(const Selection &value) {
auto start = setCoordinates(value.m_start);
auto end = setCoordinates(value.m_end);
if (start == Invalid || end == Invalid)
@@ -379,7 +379,7 @@ namespace hex::ui {
coordinates.m_column += incr;
}
TextEditor::Coordinates TextEditor::findWordStart(const Coordinates &from) const {
TextEditor::Coordinates TextEditor::findWordStart(const Coordinates &from) {
Coordinates at = setCoordinates(from);
if (at.m_line >= (i32) m_lines.size())
return at;
@@ -400,7 +400,7 @@ namespace hex::ui {
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findWordEnd(const Coordinates &from) const {
TextEditor::Coordinates TextEditor::findWordEnd(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;
@@ -421,7 +421,7 @@ namespace hex::ui {
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findNextWord(const Coordinates &from) const {
TextEditor::Coordinates TextEditor::findNextWord(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;
@@ -443,7 +443,7 @@ namespace hex::ui {
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findPreviousWord(const Coordinates &from) const {
TextEditor::Coordinates TextEditor::findPreviousWord(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;

View File

@@ -18,9 +18,10 @@ namespace hex::ui {
m_topMarginChanged = true;
}
void TextEditor::setFocusAtCoords(const Coordinates &coords) {
void TextEditor::setFocusAtCoords(const Coordinates &coords, bool ensureVisible) {
m_focusAtCoords = coords;
m_updateFocus = true;
m_ensureCursorVisible = ensureVisible;
}
void TextEditor::clearErrorMarkers() {
@@ -286,11 +287,15 @@ namespace hex::ui {
continue;
}
auto colors = m_lines[lineNo].m_colors;
u64 colorsSize = colors.size();
u64 i = skipSpaces(setCoordinates(lineNo, 0));
while (i < colorsSize) {
u64 colorsSize = std::min((u64)std::floor(textEditorSize.x / m_charAdvance.x), (u64) colors.size());
u64 i = ImGui::GetScrollX() / m_charAdvance.x;
u64 maxI = i + colorsSize;
while (i < maxI) {
char color = std::clamp(colors[i], (char) PaletteIndex::Default, (char) ((u8) PaletteIndex::Max - 1));
u32 tokenLength = std::clamp((u64) (colors.find_first_not_of(color, i) - i),(u64) 1, colorsSize - i);
auto index = colors.find_first_not_of(color, i);
index -= i;
u32 tokenLength = std::clamp((u64) index,(u64) 1, maxI - i);
if (m_updateFocus)
setFocus();
auto lineStart = setCoordinates(lineNo, i);
@@ -314,7 +319,9 @@ namespace hex::ui {
void TextEditor::setFocus() {
m_state.m_cursorPosition = m_focusAtCoords;
resetCursorBlinkTime();
ensureCursorVisible();
if (m_ensureCursorVisible)
ensureCursorVisible();
if (!this->m_readOnly)
ImGui::SetKeyboardFocusHere(0);
m_updateFocus = false;
@@ -364,7 +371,7 @@ namespace hex::ui {
void TextEditor::drawLineNumbers(ImVec2 position, float lineNo, const ImVec2 &contentSize, bool focused, ImDrawList *drawList) {
ImVec2 lineStartScreenPos = s_cursorScreenPosition + ImVec2(m_leftMargin, m_topMargin + std::floor(lineNo) * m_charAdvance.y);
ImVec2 lineNoStartScreenPos = ImVec2(position.x, m_topMargin + s_cursorScreenPosition.y + std::floor(lineNo) * m_charAdvance.y);
auto start = ImVec2(lineNoStartScreenPos.x + m_lineNumberFieldWidth, lineStartScreenPos.y);
auto start = ImVec2(lineNoStartScreenPos.x + m_lineNumberFieldWidth - m_charAdvance.x / 2, lineStartScreenPos.y);
i32 totalDigitCount = std::floor(std::log10(m_lines.size())) + 1;
ImGui::SetCursorScreenPos(position);
if (!m_ignoreImGuiChild)
@@ -380,13 +387,18 @@ namespace hex::ui {
else
m_breakpoints.insert(lineNo + 1);
m_breakPointsChanged = true;
auto cursorPosition = setCoordinates(lineNo, 0);
if (cursorPosition == Invalid)
return;
setFocusAtCoords(m_state.m_cursorPosition, false);
}
auto color = m_palette[(i32) PaletteIndex::LineNumber];
m_state.m_cursorPosition = cursorPosition;
jumpToCoords(m_state.m_cursorPosition);
if (m_state.m_cursorPosition.m_line == lineNo && m_showCursor) {
color = m_palette[(i32) PaletteIndex::Cursor];
// Highlight the current line (where the cursor is)
if (!hasSelection()) {
auto end = ImVec2(lineNoStartScreenPos.x + contentSize.x + m_lineNumberFieldWidth, lineStartScreenPos.y + m_charAdvance.y);
drawList->AddRectFilled(ImVec2(position.x, lineStartScreenPos.y), end, m_palette[(i32) (focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]);
drawList->AddRect(ImVec2(position.x, lineStartScreenPos.y), end, m_palette[(i32) PaletteIndex::CurrentLineEdge], 1.0f);
}
}
// Draw breakpoints
if (m_breakpoints.count(lineNo + 1) != 0) {
@@ -395,20 +407,9 @@ namespace hex::ui {
drawList->AddCircleFilled(start + ImVec2(0, m_charAdvance.y) / 2, m_charAdvance.y / 3, m_palette[(i32) PaletteIndex::Breakpoint]);
drawList->AddCircle(start + ImVec2(0, m_charAdvance.y) / 2, m_charAdvance.y / 3, m_palette[(i32) PaletteIndex::Default]);
drawList->AddText(ImVec2(lineNoStartScreenPos.x + m_leftMargin, lineStartScreenPos.y), m_palette[(i32) PaletteIndex::LineNumber], lineNoStr.c_str());
drawList->AddText(ImVec2(lineNoStartScreenPos.x + m_leftMargin, lineStartScreenPos.y), color, lineNoStr.c_str());
}
if (m_state.m_cursorPosition.m_line == lineNo && m_showCursor) {
// Highlight the current line (where the cursor is)
if (!hasSelection()) {
auto end = ImVec2(lineNoStartScreenPos.x + contentSize.x + m_lineNumberFieldWidth, lineStartScreenPos.y + m_charAdvance.y);
drawList->AddRectFilled(ImVec2(position.x, lineStartScreenPos.y), end, m_palette[(i32) (focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]);
drawList->AddRect(ImVec2(position.x, lineStartScreenPos.y), end, m_palette[(i32) PaletteIndex::CurrentLineEdge], 1.0f);
}
}
TextUnformattedColoredAt(ImVec2(m_leftMargin + lineNoStartScreenPos.x, lineStartScreenPos.y), m_palette[(i32) PaletteIndex::LineNumber], lineNoStr.c_str());
TextUnformattedColoredAt(ImVec2(m_leftMargin + lineNoStartScreenPos.x, lineStartScreenPos.y), color, lineNoStr.c_str());
if (!m_ignoreImGuiChild)
ImGui::EndChild();

View File

@@ -156,6 +156,7 @@ namespace hex::ui {
m_colors = line.m_colors;
m_flags = line.m_flags;
m_colorized = line.m_colorized;
m_lineMaxColumn = line.m_lineMaxColumn;
return *this;
}
@@ -164,6 +165,7 @@ namespace hex::ui {
m_colors = std::move(line.m_colors);
m_flags = std::move(line.m_flags);
m_colorized = line.m_colorized;
m_lineMaxColumn = line.m_lineMaxColumn;
return *this;
}
@@ -196,6 +198,7 @@ namespace hex::ui {
m_colors.push_back(0x00);
m_flags.push_back(0x00);
m_colorized = false;
m_lineMaxColumn = -1;
}
bool TextEditor::Line::empty() const {
@@ -273,8 +276,11 @@ namespace hex::ui {
}
void TextEditor::Line::append(LineIterator begin, LineIterator end) {
if (begin.m_charsIter < end.m_charsIter)
if (begin.m_charsIter < end.m_charsIter) {
m_chars.append(begin.m_charsIter, end.m_charsIter);
std::string charsAppended(begin.m_charsIter, end.m_charsIter);
m_lineMaxColumn += TextEditor::getStringCharacterCount(charsAppended);
}
if (begin.m_colorsIter < end.m_colorsIter)
m_colors.append(begin.m_colorsIter, end.m_colorsIter);
if (begin.m_flagsIter < end.m_flagsIter)
@@ -307,6 +313,8 @@ namespace hex::ui {
m_colors.insert(iter.m_colorsIter, beginLine.m_colorsIter, endLine.m_colorsIter);
m_flags.insert(iter.m_flagsIter, beginLine.m_flagsIter, endLine.m_flagsIter);
m_colorized = false;
std::string charsInserted(beginLine.m_charsIter, endLine.m_charsIter);
m_lineMaxColumn += TextEditor::getStringCharacterCount(charsInserted);
}
}
@@ -315,6 +323,8 @@ namespace hex::ui {
m_colors.erase(begin.m_colorsIter);
m_flags.erase(begin.m_flagsIter);
m_colorized = false;
std::string charsErased(begin.m_charsIter, end().m_charsIter);
m_lineMaxColumn -= TextEditor::getStringCharacterCount(charsErased);
}
void TextEditor::Line::erase(LineIterator begin, u64 count) {
@@ -324,6 +334,8 @@ namespace hex::ui {
m_colors.erase(begin.m_colorsIter, begin.m_colorsIter + count);
m_flags.erase(begin.m_flagsIter, begin.m_flagsIter + count);
m_colorized = false;
std::string charsErased(begin.m_charsIter, begin.m_charsIter + count);
m_lineMaxColumn -= TextEditor::getStringCharacterCount(charsErased);
}
void TextEditor::Line::erase(u64 start, u64 length) {
@@ -346,6 +358,7 @@ namespace hex::ui {
m_colors.clear();
m_flags.clear();
m_colorized = false;
m_lineMaxColumn = 0;
}
void TextEditor::Line::setLine(const std::string &text) {
@@ -353,6 +366,7 @@ namespace hex::ui {
m_colors = std::string(text.size(), 0x00);
m_flags = std::string(text.size(), 0x00);
m_colorized = false;
m_lineMaxColumn = -1;
}
void TextEditor::Line::setLine(const Line &text) {
@@ -360,6 +374,7 @@ namespace hex::ui {
m_colors = text.m_colors;
m_flags = text.m_flags;
m_colorized = text.m_colorized;
m_lineMaxColumn = text.m_lineMaxColumn;
}
bool TextEditor::Line::needsUpdate() const {

View File

@@ -27,6 +27,22 @@ namespace hex::ui {
return count;
}
i32 TextEditor::getLineCharacterCount(i32 lineIndex) {
if (lineIndex >= (i64) m_lines.size() || lineIndex < 0)
return 0;
Line &line = m_lines[lineIndex];
if (line.m_lineMaxColumn != -1)
return line.m_lineMaxColumn;
else {
auto str = line.m_chars;
i32 count = 0;
for (u32 idx = 0; idx < str.size(); count++)
idx += TextEditor::utf8CharLength(str[idx]);
line.m_lineMaxColumn = count;
return count;
}
}
// "Borrowed" from ImGui source
void TextEditor::imTextCharToUtf8(std::string &buffer, u32 c) {
if (c < 0x80) {
@@ -78,7 +94,7 @@ namespace hex::ui {
return index;
}
TextEditor::Coordinates TextEditor::screenPosToCoordinates(const ImVec2 &position) const {
TextEditor::Coordinates TextEditor::screenPosToCoordinates(const ImVec2 &position) {
ImVec2 local = position - ImGui::GetCursorScreenPos();
i32 lineNo = std::max(0, (i32) floor(local.y / m_charAdvance.y));
if (local.x < (m_leftMargin - 2) || lineNo >= (i32) m_lines.size() || m_lines[lineNo].empty())
@@ -129,7 +145,11 @@ namespace hex::ui {
return col;
}
TextEditor::Coordinates TextEditor::getCharacterCoordinates(i32 lineIndex, i32 strIndex) const {
i32 TextEditor::Line::getMaxCharColumn() const {
return getCharacterColumn(size());
}
TextEditor::Coordinates TextEditor::getCharacterCoordinates(i32 lineIndex, i32 strIndex) {
if (lineIndex < 0 || lineIndex >= (i32) m_lines.size())
return Coordinates(0, 0);
auto &line = m_lines[lineIndex];
@@ -144,14 +164,8 @@ namespace hex::ui {
return line.size();
}
i32 TextEditor::getLineCharacterCount(i32 line) const {
return getLineMaxColumn(line);
}
i32 TextEditor::getLineMaxColumn(i32 line) const {
if (line >= (i64) m_lines.size() || line < 0)
return 0;
return getStringCharacterCount(m_lines[line].m_chars);
i32 TextEditor::getLineMaxColumn(i32 lineIndex) {
return getLineCharacterCount(lineIndex);
}
TextEditor::Coordinates TextEditor::stringIndexToCoordinates(i32 strIndex, const std::string &input) {