Files
imhex/plugins/ui/source/ui/text_editor/navigate.cpp
paxcut 52952652de Fixes for next release. (#2429)
Attempt to fix WerWolv's strange off by one problem when using the
mouse. Added a popup question for files that contain long lines (>1024
bytes). Also improved the handling of large lines, so it won't stall the
app. May also contain other smaller issue fixes.
2025-09-02 15:30:50 -07:00

706 lines
27 KiB
C++

#include "imgui.h"
#include <ui/text_editor.hpp>
#include <algorithm>
namespace hex::ui {
bool isWordChar(char c) {
auto asUChar = static_cast<u8>(c);
return std::isalnum(asUChar) || c == '_' || asUChar > 0x7F;
}
void TextEditor::jumpToLine(i32 line) {
auto newPos = m_state.m_cursorPosition;
if (line != -1) {
newPos = setCoordinates(line, 0);
}
jumpToCoords(newPos);
}
void TextEditor::jumpToCoords(const Coordinates &coords) {
setSelection(Selection(coords, coords));
setCursorPosition(coords);
ensureCursorVisible();
setFocusAtCoords(coords);
}
void TextEditor::moveToMatchedBracket(bool select) {
resetCursorBlinkTime();
if (m_matchedBracket.isNearABracket(this, m_state.m_cursorPosition)) {
m_matchedBracket.findMatchingBracket(this);
auto oldPos = m_matchedBracket.m_nearCursor;
auto newPos = m_matchedBracket.m_matched;
if (newPos != setCoordinates(-1, -1)) {
if (select) {
if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = newPos;
else if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = newPos;
else {
m_interactiveSelection = Selection(newPos, oldPos);
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = newPos;
setSelection(m_interactiveSelection);
setCursorPosition(newPos);
ensureCursorVisible();
}
}
}
void TextEditor::moveUp(i32 amount, bool select) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
if (amount < 0) {
m_scrollYIncrement = -1.0;
setScrollY();
return;
}
m_state.m_cursorPosition.m_line = std::max(0, m_state.m_cursorPosition.m_line - amount);
if (oldPos != m_state.m_cursorPosition) {
if (select) {
if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = m_state.m_cursorPosition;
m_interactiveSelection.m_end = oldPos;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
ensureCursorVisible();
}
}
void TextEditor::moveDown(i32 amount, bool select) {
IM_ASSERT(m_state.m_cursorPosition.m_column >= 0);
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
if (amount < 0) {
m_scrollYIncrement = 1.0;
setScrollY();
return;
}
m_state.m_cursorPosition.m_line = std::clamp(m_state.m_cursorPosition.m_line + amount, 0, (i32) m_lines.size() - 1);
if (oldPos.m_line == (i64) (m_lines.size() - 1)) {
m_topLine += amount;
m_topLine = std::clamp(m_topLine, 0.0F, m_lines.size() - 1.0F);
setTopLine();
ensureCursorVisible();
return;
}
if (m_state.m_cursorPosition != oldPos) {
if (select) {
if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = m_state.m_cursorPosition;
else if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = oldPos;
m_interactiveSelection.m_end = m_state.m_cursorPosition;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
ensureCursorVisible();
}
}
void TextEditor::moveLeft(i32 amount, bool select, bool wordMode) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
if (isEmpty() || oldPos.m_line >= (i64)m_lines.size())
return;
auto lindex = m_state.m_cursorPosition.m_line;
auto lineMaxColumn = getLineMaxCharColumn(lindex);
auto column = std::min(m_state.m_cursorPosition.m_column, lineMaxColumn);
while (amount-- > 0) {
if (column == 0) {
if (lindex == 0)
m_state.m_cursorPosition = Coordinates(0, 0);
else {
lindex--;
m_state.m_cursorPosition = setCoordinates(lindex, -1);
}
} else if (wordMode)
m_state.m_cursorPosition = findPreviousWord(m_state.m_cursorPosition);
else
m_state.m_cursorPosition = Coordinates(lindex, column - 1);
}
if (select) {
if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = m_state.m_cursorPosition;
m_interactiveSelection.m_end = oldPos;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
ensureCursorVisible();
}
void TextEditor::moveRight(i32 amount, bool select, bool wordMode) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
if (isEmpty() || oldPos.m_line >= (i64) m_lines.size())
return;
auto lindex = m_state.m_cursorPosition.m_line;
auto lineMaxColumn = getLineMaxCharColumn(lindex);
auto column = std::min(m_state.m_cursorPosition.m_column, lineMaxColumn);
while (amount-- > 0) {
if (isEndOfLine(oldPos)) {
if (!isEndOfFile(oldPos)) {
lindex++;
m_state.m_cursorPosition = Coordinates(lindex, 0);
} else
m_state.m_cursorPosition = setCoordinates(-1, -1);
} else if (wordMode)
m_state.m_cursorPosition = findNextWord(m_state.m_cursorPosition);
else
m_state.m_cursorPosition = Coordinates(lindex, column + 1);
}
if (select) {
if (oldPos == m_interactiveSelection.m_end) {
m_interactiveSelection.m_end = Coordinates(m_state.m_cursorPosition);
if (m_interactiveSelection.m_end == Invalid)
return;
} else if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = oldPos;
m_interactiveSelection.m_end = m_state.m_cursorPosition;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
ensureCursorVisible();
}
void TextEditor::moveTop(bool select) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
setCursorPosition(setCoordinates(0, 0));
if (m_state.m_cursorPosition != oldPos) {
if (select) {
m_interactiveSelection = Selection(m_state.m_cursorPosition, oldPos);
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
}
}
void TextEditor::TextEditor::moveBottom(bool select) {
resetCursorBlinkTime();
auto oldPos = getCursorPosition();
auto newPos = setCoordinates(-1, -1);
setCursorPosition(newPos);
if (select) {
m_interactiveSelection = Selection(oldPos, newPos);
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = newPos;
setSelection(m_interactiveSelection);
}
void TextEditor::moveHome(bool select) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
auto &line = m_lines[oldPos.m_line];
auto prefix = line.substr(0, oldPos.m_column);
auto postfix = line.substr(oldPos.m_column);
if (prefix.empty() && postfix.empty())
return;
i32 home;
if (!prefix.empty()) {
auto idx = prefix.find_first_not_of(" ");
if (idx == std::string::npos) {
auto postIdx = postfix.find_first_of(" ");
if (postIdx == std::string::npos || postIdx == 0)
home = 0;
else {
postIdx = postfix.find_first_not_of(" ");
if (postIdx == std::string::npos)
home = getLineMaxCharColumn(oldPos.m_line);
else if (postIdx == 0)
home = 0;
else
home = oldPos.m_column + postIdx;
}
} else
home = idx;
} else {
auto postIdx = postfix.find_first_of(" ");
if (postIdx == std::string::npos)
home = 0;
else {
postIdx = postfix.find_first_not_of(" ");
if (postIdx == std::string::npos)
home = getLineMaxCharColumn(oldPos.m_line);
else
home = oldPos.m_column + postIdx;
}
}
setCursorPosition(Coordinates(m_state.m_cursorPosition.m_line, home));
if (m_state.m_cursorPosition != oldPos) {
if (select) {
if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = m_state.m_cursorPosition;
m_interactiveSelection.m_end = oldPos;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
}
}
void TextEditor::moveEnd(bool select) {
resetCursorBlinkTime();
auto oldPos = m_state.m_cursorPosition;
setCursorPosition(setCoordinates(m_state.m_cursorPosition.m_line, getLineMaxCharColumn(oldPos.m_line)));
if (m_state.m_cursorPosition != oldPos) {
if (select) {
if (oldPos == m_interactiveSelection.m_end)
m_interactiveSelection.m_end = m_state.m_cursorPosition;
else if (oldPos == m_interactiveSelection.m_start)
m_interactiveSelection.m_start = m_state.m_cursorPosition;
else {
m_interactiveSelection.m_start = oldPos;
m_interactiveSelection.m_end = m_state.m_cursorPosition;
}
} else
m_interactiveSelection.m_start = m_interactiveSelection.m_end = m_state.m_cursorPosition;
setSelection(m_interactiveSelection);
}
}
void TextEditor::setScrollY() {
if (!m_withinRender) {
m_setScrollY = true;
return;
} else {
m_setScrollY = false;
auto scrollY = ImGui::GetScrollY();
ImGui::SetScrollY(std::clamp(scrollY + m_scrollYIncrement, 0.0f, ImGui::GetScrollMaxY()));
}
}
void TextEditor::setCursorPosition(const Coordinates &position) {
if (m_state.m_cursorPosition != position) {
m_state.m_cursorPosition = position;
ensureCursorVisible();
}
}
void TextEditor::setCursorPosition() {
setCursorPosition(m_state.m_selection.m_end);
}
TextEditor::Coordinates TextEditor::setCoordinates(i32 line, i32 column) {
if (isEmpty())
return Coordinates(0, 0);
Coordinates result = Coordinates(0, 0);
auto lineCount = (i32) m_lines.size();
if (line < 0 && lineCount + line >= 0)
result.m_line = lineCount + line;
else
result.m_line = std::clamp(line, 0, lineCount - 1);
auto maxColumn = getLineMaxCharColumn(result.m_line) + 1;
if (column < 0 && maxColumn + column >= 0)
result.m_column = maxColumn + column;
else
result.m_column = std::clamp(column, 0, maxColumn - 1);
return result;
}
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) {
auto start = setCoordinates(value.m_start);
auto end = setCoordinates(value.m_end);
if (start == Invalid || end == Invalid)
return Selection(Invalid, Invalid);
if (start > end) {
std::swap(start, end);
}
return Selection(start, end);
}
void TextEditor::advance(Coordinates &coordinates) const {
if (isEndOfFile(coordinates))
return;
if (isEndOfLine(coordinates)) {
++coordinates.m_line;
coordinates.m_column = 0;
return;
}
auto &line = m_lines[coordinates.m_line];
i64 column = coordinates.m_column;
std::string lineChar = line[column];
auto incr = getStringCharacterCount(lineChar);
coordinates.m_column += incr;
}
TextEditor::Coordinates TextEditor::findWordStart(const Coordinates &from) {
Coordinates at = setCoordinates(from);
if (at.m_line >= (i32) m_lines.size())
return at;
auto &line = m_lines[at.m_line];
auto charIndex = lineCoordinatesToIndex(at);
if (isWordChar(line.m_chars[charIndex])) {
while (charIndex > 0 && isWordChar(line.m_chars[charIndex - 1]))
--charIndex;
} else if (ispunct(line.m_chars[charIndex])) {
while (charIndex > 0 && ispunct(line.m_chars[charIndex - 1]))
--charIndex;
} else if (isspace(line.m_chars[charIndex])) {
while (charIndex > 0 && isspace(line.m_chars[charIndex - 1]))
--charIndex;
}
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findWordEnd(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;
auto &line = m_lines[at.m_line];
auto charIndex = lineCoordinatesToIndex(at);
if (isWordChar(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && isWordChar(line.m_chars[charIndex]))
++charIndex;
} else if (ispunct(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && ispunct(line.m_chars[charIndex]))
++charIndex;
} else if (isspace(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && isspace(line.m_chars[charIndex]))
++charIndex;
}
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findNextWord(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;
auto &line = m_lines[at.m_line];
auto charIndex = lineCoordinatesToIndex(at);
if (isspace(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && isspace(line.m_chars[charIndex]))
++charIndex;
}
if (isWordChar(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && (isWordChar(line.m_chars[charIndex])))
++charIndex;
} else if (ispunct(line.m_chars[charIndex])) {
while (charIndex < (i32) line.m_chars.size() && (ispunct(line.m_chars[charIndex])))
++charIndex;
}
return getCharacterCoordinates(at.m_line, charIndex);
}
TextEditor::Coordinates TextEditor::findPreviousWord(const Coordinates &from) {
Coordinates at = from;
if (at.m_line >= (i32) m_lines.size())
return at;
auto &line = m_lines[at.m_line];
auto charIndex = lineCoordinatesToIndex(at);
if (isspace(line.m_chars[charIndex - 1])) {
while (charIndex > 0 && isspace(line.m_chars[charIndex - 1]))
--charIndex;
}
if (isWordChar(line.m_chars[charIndex - 1])) {
while (charIndex > 0 && isWordChar(line.m_chars[charIndex - 1]))
--charIndex;
} else if (ispunct(line.m_chars[charIndex - 1])) {
while (charIndex > 0 && ispunct(line.m_chars[charIndex - 1]))
--charIndex;
}
return getCharacterCoordinates(at.m_line, charIndex);
}
u32 TextEditor::skipSpaces(const Coordinates &from) {
auto line = from.m_line;
if (line >= (i64) m_lines.size())
return 0;
auto &lines = m_lines[line].m_chars;
auto &colors = m_lines[line].m_colors;
auto charIndex = lineCoordinatesToIndex(from);
u32 s = 0;
while (charIndex < (i32) lines.size() && lines[charIndex] == ' ' && colors[charIndex] == 0x00) {
++s;
++charIndex;
}
if (m_updateFocus)
setFocus();
return s;
}
bool TextEditor::MatchedBracket::checkPosition(TextEditor *editor, const Coordinates &from) {
auto lineIndex = from.m_line;
auto line = editor->m_lines[lineIndex].m_chars;
auto colors = editor->m_lines[lineIndex].m_colors;
if (!line.empty() && colors.empty())
return false;
auto result = editor->lineCoordinatesToIndex(from);
auto character = line[result];
auto color = colors[result];
if ((s_separators.find(character) != std::string::npos && (static_cast<PaletteIndex>(color) == PaletteIndex::Separator)) || (static_cast<PaletteIndex>(color) == PaletteIndex::WarningText) ||
(s_operators.find(character) != std::string::npos && (static_cast<PaletteIndex>(color) == PaletteIndex::Operator)) || (static_cast<PaletteIndex>(color) == PaletteIndex::WarningText)) {
if (m_nearCursor != editor->getCharacterCoordinates(lineIndex, result)) {
m_nearCursor = editor->getCharacterCoordinates(lineIndex, result);
m_changed = true;
}
m_active = true;
return true;
}
return false;
}
i32 TextEditor::MatchedBracket::detectDirection(TextEditor *editor, const Coordinates &from) {
std::string brackets = "()[]{}<>";
i32 result = -2; // dont check either
auto start = editor->setCoordinates(from);
if (start == TextEditor::Invalid)
return result;
auto lineIndex = start.m_line;
auto line = editor->m_lines[lineIndex].m_chars;
auto charIndex = editor->lineCoordinatesToIndex(start);
auto ch2 = line[charIndex];
auto idx2 = brackets.find(ch2);
if (charIndex == 0) {// no previous character
if (idx2 == std::string::npos) // no brackets
return -2;// dont check either
else
return 1; // check only current
}// charIndex > 0
auto ch1 = line[charIndex - 1];
auto idx1 = brackets.find(ch1);
if (idx1 == std::string::npos && idx2 == std::string::npos) // no brackets
return -2;
if (idx1 != std::string::npos && idx2 != std::string::npos) {
if (idx1 % 2) // closing bracket + any bracket
return -1; // check both and previous first
else if (!(idx1 % 2) && !(idx2 % 2)) // opening bracket + opening bracket
return 0; // check both and current first
} else if (idx1 != std::string::npos) // only first bracket
return 1; // check only previous
else // only second bracket
return 2; // check only current
return result;
}
bool TextEditor::MatchedBracket::isNearABracket(TextEditor *editor, const Coordinates &from) {
if (editor->isEmpty())
return false;
auto start = editor->setCoordinates(from);
if (start == TextEditor::Invalid)
return false;
auto lineIndex = start.m_line;
auto charIndex = editor->lineCoordinatesToIndex(start);
auto direction1 = detectDirection(editor, start);
auto charCoords = editor->getCharacterCoordinates(lineIndex, charIndex);
i32 direction2 = 1;
if (direction1 == -1 || direction1 == 1) {
if (checkPosition(editor, editor->setCoordinates(charCoords.m_line, charCoords.m_column - 1)))
return true;
if (direction1 == -1)
direction2 = 0;
} else if (direction1 == 2 || direction1 == 0) {
if (checkPosition(editor, charCoords))
return true;
if (direction1 == 0)
direction2 = -1;
}
if (direction2 != 1) {
if (checkPosition(editor, editor->setCoordinates(charCoords.m_line, charCoords.m_column + direction2)))
return true;
}
u64 result;
auto strLine = editor->m_lines[lineIndex].m_chars;
if (charIndex == 0) {
if (strLine[0] == ' ') {
result = std::string::npos;
} else {
result = 0;
}
} else {
result = strLine.find_last_not_of(' ', charIndex - 1);
}
if (result != std::string::npos) {
auto resultCoords = editor->getCharacterCoordinates(lineIndex, result);
if (checkPosition(editor, resultCoords))
return true;
}
result = strLine.find_first_not_of(' ', charIndex);
if (result != std::string::npos) {
auto resultCoords = editor->getCharacterCoordinates(lineIndex, result);
if (checkPosition(editor, resultCoords))
return true;
}
if (isActive()) {
editor->m_lines[m_nearCursor.m_line].m_colorized = false;
editor->m_lines[m_matched.m_line].m_colorized = false;
m_active = false;
editor->colorize();
}
return false;
}
void TextEditor::MatchedBracket::findMatchingBracket(TextEditor *editor) {
auto from = editor->setCoordinates(m_nearCursor);
if (from == TextEditor::Invalid) {
m_active = false;
return;
}
m_matched = from;
auto lineIndex = from.m_line;
auto maxLineIndex = editor->m_lines.size() - 1;
auto charIndex = editor->lineCoordinatesToIndex(from);
std::string line = editor->m_lines[lineIndex].m_chars;
std::string colors = editor->m_lines[lineIndex].m_colors;
if (!line.empty() && colors.empty()) {
m_active = false;
return;
}
std::string brackets = "()[]{}<>";
char bracketChar = line[charIndex];
char color1;
auto idx = brackets.find_first_of(bracketChar);
if (idx == std::string::npos) {
if (m_active) {
m_active = false;
editor->colorize();
}
return;
}
auto bracketChar2 = brackets[idx ^ 1];
brackets = bracketChar;
brackets += bracketChar2;
i32 direction = 1 - 2 * (idx % 2);
if (idx > 5)
color1 = static_cast<char>(PaletteIndex::Operator);
else
color1 = static_cast<char>(PaletteIndex::Separator);
char color = static_cast<char>(PaletteIndex::WarningText);
i32 depth = 1;
if (charIndex == (i64) (line.size() - 1) * (1 + direction) / 2) {
if (lineIndex == (i64) maxLineIndex * (1 + direction) / 2) {
m_active = false;
return;
}
lineIndex += direction;
line = editor->m_lines[lineIndex].m_chars;
colors = editor->m_lines[lineIndex].m_colors;
if (!line.empty() && colors.empty()) {
m_active = false;
return;
}
charIndex = (line.size() - 1) * (1 - direction) / 2 - direction;
}
for (i32 i = charIndex + direction;; i += direction) {
if (direction == 1)
idx = line.find_first_of(brackets, i);
else
idx = line.find_last_of(brackets, i);
if (idx != std::string::npos) {
if (line[idx] == bracketChar && (colors[idx] == color || colors[idx] == color1)) {
++depth;
i = idx;
} else if ((line[idx] == bracketChar2 && (colors[idx] == color)) || colors[idx] == color1) {
--depth;
if (depth == 0) {
if (m_matched != editor->getCharacterCoordinates(lineIndex, idx)) {
m_matched = editor->getCharacterCoordinates(lineIndex, idx);
m_changed = true;
}
m_active = true;
break;
}
i = idx;
} else {
i = idx;
}
} else {
if (direction == 1)
i = line.size() - 1;
else
i = 0;
}
if ((i32) (direction * i) >= (i32) ((line.size() - 1) * (1 + direction) / 2)) {
if (lineIndex == (i64) maxLineIndex * (1 + direction) / 2) {
if (m_active) {
m_active = false;
m_changed = true;
}
break;
} else {
lineIndex += direction;
line = editor->m_lines[lineIndex].m_chars;
colors = editor->m_lines[lineIndex].m_colors;
if (!line.empty() && colors.empty()) {
m_active = false;
return;
}
i = (line.size() - 1) * (1 - direction) / 2 - direction;
}
}
}
if (hasChanged()) {
editor->m_lines[m_nearCursor.m_line].m_colorized = false;
editor->m_lines[m_matched.m_line].m_colorized = false;
editor->colorize();
m_changed = false;
}
}
}