improv: moved text editor to the ui plugin. (#2397)

Reorganized source code into files named in the fashion of imhex and
split large functions into smaller ones. Moved all function definitions
out of the header except for one-liners. All variable types were
switched to use imHex standard (u8,...) and removed duplicated functions
that were needed when the text editor was isolated.

Minor improvements to find/replace while making sure they still worked
with utf-8 chars.
This commit is contained in:
paxcut
2025-08-10 14:35:21 -07:00
committed by GitHub
parent 75e73ddcd9
commit 50f1fe2b2d
19 changed files with 4455 additions and 4697 deletions

View File

@@ -2,7 +2,7 @@
#include <pl/core/token.hpp>
#include <pl/core/preprocessor.hpp>
#include <pl/helpers/safe_iterator.hpp>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
#include <hex/helpers/types.hpp>
namespace pl {

View File

@@ -2,7 +2,7 @@
#include <hex/ui/view.hpp>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
#include <list>

View File

@@ -17,7 +17,7 @@
#include <filesystem>
#include <functional>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
#include <popups/popup_file_chooser.hpp>
#include <content/text_highlighting/pattern_language.hpp>

View File

@@ -8,7 +8,7 @@
#include <imgui_internal.h>
#include <implot.h>
#include <imnodes.h>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
#include <romfs/romfs.hpp>
#include <wolv/io/file.hpp>

View File

@@ -4,7 +4,7 @@
#include <hex/ui/imgui_imhex_extensions.h>
#include <imgui.h>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
namespace hex::plugin::builtin {

View File

@@ -13,6 +13,12 @@ add_imhex_plugin(
source/ui/visualizer_drawer.cpp
source/ui/menu_items.cpp
source/ui/pattern_value_editor.cpp
source/ui/text_editor/editor.cpp
source/ui/text_editor/highlighter.cpp
source/ui/text_editor/navigate.cpp
source/ui/text_editor/render.cpp
source/ui/text_editor/support.cpp
source/ui/text_editor/utf8.cpp
INCLUDES
include
LIBRARIES

View File

@@ -0,0 +1,698 @@
#pragma once
#include <string>
#include <vector>
#include <array>
#include <memory>
#include <functional>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <regex>
#include <chrono>
#include <iostream>
#include "imgui.h"
#include "imgui_internal.h"
#include <hex/helpers/utils.hpp>
namespace hex::ui {
using strConstIter = std::string::const_iterator;
class TextEditor {
public:
// indices of the arrays that contain the lines (vector) and the columns (a string) of the
// text editor. Negative values indicate the distance to the last element of the array.
// When comparing coordinates ensure they have the same sign because coordinates don't have
// information about the size of the array. Currently positive coordinates are always bigger
// than negatives even if that gives a wrong result.
struct Coordinates {
i32 m_line, m_column;
Coordinates() : m_line(0), m_column(0) {}
Coordinates(i32 line, i32 column) : m_line(line), m_column(column) {}
bool operator==(const Coordinates &o) const;
bool operator!=(const Coordinates &o) const;
bool operator<(const Coordinates &o) const;
bool operator>(const Coordinates &o) const;
bool operator<=(const Coordinates &o) const;
bool operator>=(const Coordinates &o) const;
Coordinates operator+(const Coordinates &o) const;
Coordinates operator-(const Coordinates &o) const;
};
inline static const Coordinates Invalid = Coordinates(0x80000000, 0x80000000);
struct Selection {
Coordinates m_start;
Coordinates m_end;
Selection() = default;
Selection(Coordinates start, Coordinates end) : m_start(start), m_end(end) {
if (m_start > m_end) {
std::swap(m_start, m_end);
}
}
Coordinates getSelectedLines();
Coordinates getSelectedColumns();
bool isSingleLine();
bool contains(Coordinates coordinates, int8_t endsInclusive=1);
};
struct EditorState {
Selection m_selection;
Coordinates m_cursorPosition;
};
class FindReplaceHandler {
public:
FindReplaceHandler();
typedef std::vector<EditorState> Matches;
Matches &getMatches() { return m_matches; }
bool findNext(TextEditor *editor);
u32 findMatch(TextEditor *editor, bool isNex);
bool replace(TextEditor *editor, bool right);
bool replaceAll(TextEditor *editor);
std::string &getFindWord() { return m_findWord; }
void setFindWord(TextEditor *editor, const std::string &findWord) {
if (findWord != m_findWord) {
findAllMatches(editor, findWord);
m_findWord = findWord;
}
}
std::string &getReplaceWord() { return m_replaceWord; }
void setReplaceWord(const std::string &replaceWord) { m_replaceWord = replaceWord; }
void selectFound(TextEditor *editor, i32 found);
void findAllMatches(TextEditor *editor, std::string findWord);
u32 findPosition(TextEditor *editor, Coordinates pos, bool isNext);
bool getMatchCase() const { return m_matchCase; }
void setMatchCase(TextEditor *editor, bool matchCase) {
if (matchCase != m_matchCase) {
m_matchCase = matchCase;
m_optionsChanged = true;
findAllMatches(editor, m_findWord);
}
}
bool getWholeWord() const { return m_wholeWord; }
void setWholeWord(TextEditor *editor, bool wholeWord) {
if (wholeWord != m_wholeWord) {
m_wholeWord = wholeWord;
m_optionsChanged = true;
findAllMatches(editor, m_findWord);
}
}
bool getFindRegEx() const { return m_findRegEx; }
void setFindRegEx(TextEditor *editor, bool findRegEx) {
if (findRegEx != m_findRegEx) {
m_findRegEx = findRegEx;
m_optionsChanged = true;
findAllMatches(editor, m_findWord);
}
}
void resetMatches() {
m_matches.clear();
m_findWord = "";
}
private:
std::string m_findWord;
std::string m_replaceWord;
bool m_matchCase;
bool m_wholeWord;
bool m_findRegEx;
bool m_optionsChanged;
Matches m_matches;
};
enum class PaletteIndex {
Default,
Identifier,
Directive,
Operator,
Separator,
BuiltInType,
Keyword,
NumericLiteral,
StringLiteral,
CharLiteral,
Cursor,
Background,
LineNumber,
Selection,
Breakpoint,
ErrorMarker,
PreprocessorDeactivated,
CurrentLineFill,
CurrentLineFillInactive,
CurrentLineEdge,
ErrorText,
WarningText,
DebugText,
DefaultText,
Attribute,
PatternVariable,
LocalVariable,
CalculatedPointer,
TemplateArgument,
Function,
View,
FunctionVariable,
FunctionParameter,
UserDefinedType,
PlacedVariable,
GlobalVariable,
NameSpace,
TypeDef,
UnkIdentifier,
DocComment,
DocBlockComment,
BlockComment,
GlobalDocComment,
Comment,
PreprocIdentifier,
Max
};
typedef std::vector<std::pair<std::regex, PaletteIndex>> RegexList;
struct Identifier {
Coordinates m_location;
std::string m_declaration;
};
using String = std::string;
using Identifiers = std::unordered_map<std::string, Identifier>;
using Keywords = std::unordered_set<std::string>;
using ErrorMarkers = std::map<Coordinates, std::pair<u32, std::string>>;
using Breakpoints = std::unordered_set<u32>;
using Palette = std::array<ImU32, (u64) PaletteIndex::Max>;
using Glyph = uint8_t;
class ActionableBox {
ImRect m_box;
public:
ActionableBox() = default;
explicit ActionableBox(const ImRect &box) : m_box(box) {}
virtual bool trigger() {
return ImGui::IsMouseHoveringRect(m_box.Min, m_box.Max);
}
virtual void callback() {}
};
class CursorChangeBox : public ActionableBox {
public:
CursorChangeBox() = default;
explicit CursorChangeBox(const ImRect &box) : ActionableBox(box) {}
void callback() override {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
};
class ErrorGotoBox : public ActionableBox {
Coordinates m_pos;
public:
ErrorGotoBox() = default;
ErrorGotoBox(const ImRect &box, const Coordinates &pos, TextEditor *editor) : ActionableBox(box), m_pos(pos), m_editor(editor) {}
bool trigger() override {
return ActionableBox::trigger() && ImGui::IsMouseClicked(0);
}
void callback() override {
m_editor->jumpToCoords(m_pos);
}
private:
TextEditor *m_editor;
};
using ErrorGotoBoxes = std::map<Coordinates, ErrorGotoBox>;
using CursorBoxes = std::map<Coordinates, CursorChangeBox>;
class ErrorHoverBox : public ActionableBox {
Coordinates m_pos;
std::string m_errorText;
public:
ErrorHoverBox() = default;
ErrorHoverBox(const ImRect &box, const Coordinates &pos, const char *errorText) : ActionableBox(box), m_pos(pos), m_errorText(errorText) {}
void callback() override {
ImGui::BeginTooltip();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
ImGui::Text("Error at line %d:", m_pos.m_line);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.2f, 1.0f));
ImGui::TextUnformatted(m_errorText.c_str());
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
};
using ErrorHoverBoxes = std::map<Coordinates, ErrorHoverBox>;
// A line of text in the pattern editor consists of three strings; the character encoding, the color encoding and the flags.
// The char encoding is utf-8, the color encoding are indices to the color palette and the flags are used to override the colors
// depending on priorities; e.g. comments, strings, etc.
class Line {
public:
struct FlagBits {
bool comment: 1;
bool blockComment: 1;
bool docComment: 1;
bool blockDocComment: 1;
bool globalDocComment: 1;
bool deactivated: 1;
bool preprocessor: 1;
bool matchedBracket: 1;
};
union Flags {
Flags(char value) : m_value(value) {}
Flags(FlagBits bits) : m_bits(bits) {}
FlagBits m_bits;
char m_value;
};
constexpr static char inComment = 31;
class LineIterator {
public:
strConstIter m_charsIter;
strConstIter m_colorsIter;
strConstIter m_flagsIter;
LineIterator(const LineIterator &other) : m_charsIter(other.m_charsIter), m_colorsIter(other.m_colorsIter), m_flagsIter(other.m_flagsIter) {}
LineIterator() = default;
char operator*();
LineIterator operator++();
LineIterator operator=(const LineIterator &other);
bool operator!=(const LineIterator &other) const;
bool operator==(const LineIterator &other) const;
LineIterator operator+(i32 n);
i32 operator-(LineIterator l);
};
enum class LinePart {
Chars,
Utf8,
Colors,
Flags
};
LineIterator begin() const;
LineIterator end() const;
std::string m_chars;
std::string m_colors;
std::string m_flags;
bool m_colorized = false;
Line() : m_chars(), m_colors(), m_flags(), m_colorized(false) {}
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) {}
i32 getCharacterColumn(i32 index) const;
LineIterator begin();
LineIterator end();
Line &operator=(const Line &line);
Line &operator=(Line &&line) noexcept;
u64 size() const;
char front(LinePart part = LinePart::Chars) const;
std::string frontUtf8(LinePart part = LinePart::Chars) const;
void push_back(char c);
bool empty() const;
std::string substr(u64 start, u64 length = (u64) -1, LinePart part = LinePart::Chars) const;
char operator[](u64 index) const;
// C++ can't overload functions based on return type, so use any type other
// than u64 to avoid ambiguity.
std::string operator[](i64 column) const;
void setNeedsUpdate(bool needsUpdate);
void append(const char *text);
void append(const char text);
void append(const std::string &text);
void append(const Line &line);
void append(LineIterator begin, LineIterator end);
void insert(LineIterator iter, const std::string &text);
void insert(LineIterator iter, const char text);
void insert(LineIterator iter, strConstIter beginString, strConstIter endString);
void insert(LineIterator iter, const Line &line);
void insert(LineIterator iter, LineIterator beginLine, LineIterator endLine);
void erase(LineIterator begin);
void erase(LineIterator begin, u64 count);
void erase(u64 start, u64 length = (u64) -1);
void clear();
void setLine(const std::string &text);
void setLine(const Line &text);
bool needsUpdate() const;
};
using Lines = std::vector<Line>;
struct LanguageDefinition {
typedef std::pair<std::string, PaletteIndex> TokenRegexString;
typedef std::vector<TokenRegexString> TokenRegexStrings;
typedef bool(*TokenizeCallback)(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin,
strConstIter &out_end, PaletteIndex &paletteIndex);
std::string m_name;
Keywords m_keywords;
Identifiers m_identifiers;
Identifiers m_preprocIdentifiers;
std::string m_singleLineComment, m_commentEnd, m_commentStart, m_globalDocComment, m_docComment, m_blockDocComment;
char m_preprocChar;
bool m_autoIndentation;
TokenizeCallback m_tokenize;
TokenRegexStrings m_tokenRegexStrings;
bool m_caseSensitive;
LanguageDefinition() : m_name(""), m_keywords({}), m_identifiers({}), m_preprocIdentifiers({}),
m_singleLineComment(""), m_commentEnd(""),
m_commentStart(""), m_globalDocComment(""), m_docComment(""), m_blockDocComment(""),
m_preprocChar('#'), m_autoIndentation(true), m_tokenize(nullptr),
m_tokenRegexStrings({}), m_caseSensitive(true) {}
static const LanguageDefinition &CPlusPlus();
static const LanguageDefinition &HLSL();
static const LanguageDefinition &GLSL();
static const LanguageDefinition &C();
static const LanguageDefinition &SQL();
static const LanguageDefinition &AngelScript();
static const LanguageDefinition &Lua();
};
TextEditor();
~TextEditor();
private:
class UndoRecord {
public:
UndoRecord() {}
~UndoRecord() {}
UndoRecord( const std::string &added,
const TextEditor::Selection addedSelection,
const std::string &removed,
const TextEditor::Selection removedSelection,
TextEditor::EditorState &before,
TextEditor::EditorState &after);
void undo(TextEditor *editor);
void redo(TextEditor *editor);
std::string m_added;
Selection m_addedSelection;
std::string m_removed;
Selection m_removedSelection;
EditorState m_before;
EditorState m_after;
};
typedef std::vector<UndoRecord> UndoBuffer;
struct MatchedBracket {
bool m_active = false;
bool m_changed = false;
Coordinates m_nearCursor = {};
Coordinates m_matched = {};
static const std::string s_separators;
static const std::string s_operators;
MatchedBracket(const MatchedBracket &other) : m_active(other.m_active), m_changed(other.m_changed),
m_nearCursor(other.m_nearCursor),
m_matched(other.m_matched) {}
MatchedBracket() : m_active(false), m_changed(false), m_nearCursor(0, 0), m_matched(0, 0) {}
MatchedBracket(bool active, bool changed, const Coordinates &nearCursor, const Coordinates &matched)
: m_active(active), m_changed(changed), m_nearCursor(nearCursor), m_matched(matched) {}
bool checkPosition(TextEditor *editor, const Coordinates &from);
bool isNearABracket(TextEditor *editor, const Coordinates &from);
i32 detectDirection(TextEditor *editor, const Coordinates &from);
void findMatchingBracket(TextEditor *editor);
bool isActive() const { return m_active; }
bool hasChanged() const { return m_changed; }
};
public:
// Rendering
ImVec2 underwaves(ImVec2 pos, u32 nChars, ImColor color = ImGui::GetStyleColorVec4(ImGuiCol_Text), const ImVec2 &size_arg = ImVec2(0, 0));
void setTabSize(i32 value);
float getPageSize() const;
bool isEndOfLine(const Coordinates &coordinates) const;
bool isEndOfFile(const Coordinates &coordinates) const;
bool isEndOfLine() const;
bool isStartOfLine() const;
void setTopLine();
void render(const char *title, const ImVec2 &size = ImVec2(), bool border = false);
inline void setShowCursor(bool value) { m_showCursor = value; }
inline void setShowLineNumbers(bool value) { m_showLineNumbers = value; }
inline void setShowWhitespaces(bool value) { m_showWhitespaces = value; }
inline bool isShowingWhitespaces() const { return m_showWhitespaces; }
inline i32 getTabSize() const { return m_tabSize; }
ImVec2 &getCharAdvance() { return m_charAdvance; }
void clearGotoBoxes() { m_errorGotoBoxes.clear(); }
void clearCursorBoxes() { m_cursorBoxes.clear(); }
void addClickableText(std::string text) { m_clickableText.push_back(text); }
void setErrorMarkers(const ErrorMarkers &markers) { m_errorMarkers = markers; }
Breakpoints &getBreakpoints() { return m_breakpoints; }
void setBreakpoints(const Breakpoints &markers) { m_breakpoints = markers; }
void setLongestLineLength(u64 line) { m_longestLineLength = line; }
u64 getLongestLineLength() const { return m_longestLineLength; }
void setTopMarginChanged(i32 newMargin);
void setFocusAtCoords(const Coordinates &coords);
void clearErrorMarkers();
void clearActionables();
private:
void ensureCursorVisible();
void resetCursorBlinkTime();
void renderText(const char *title, const ImVec2 &lineNumbersStartPos, const ImVec2 &textEditorSize);
void setFocus();
void preRender();
void drawSelection(float lineNo);
void drawLineNumbers(ImVec2 position, float lineNo, const ImVec2 &contentSize, bool focused, ImDrawList *drawList);
void renderCursor(float lineNo, ImDrawList *drawList);
void renderGotoButtons(float lineNo);
void drawText(Coordinates &lineStart, u64 i, u32 tokenLength, char color);
void postRender(const char *title, ImVec2 position, float lineNo);
ImVec2 calculateCharAdvance() const;
float textDistanceToLineStart(const Coordinates &from) const;
// Highlighting
public:
void colorize();
void setLanguageDefinition(const LanguageDefinition &aLanguageDef);
static const Palette &getPalette();
static void setPalette(const Palette &value);
static const Palette &getDarkPalette();
static const Palette &getLightPalette();
static const Palette &getRetroBluePalette();
bool isColorizerEnabled() const { return m_colorizerEnabled; }
const LanguageDefinition &getLanguageDefinition() const { return m_languageDefinition; }
void setNeedsUpdate(u32 line, bool needsUpdate);
void setColorizedLine(u64 line, const std::string &tokens);
private:
void colorizeRange();
void colorizeInternal();
//Editing
public:
void deleteWordLeft();
void deleteWordRight();
void backspace();
bool canUndo();
bool canRedo() const;
void undo(i32 steps = 1);
void redo(i32 steps = 1);
void copy();
void cut();
void paste();
void deleteChar();
void insertText(const std::string &value);
void insertText(const char *value);
void appendLine(const std::string &value);
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::vector<std::string> getTextLines() const;
std::string getSelectedText() const;
std::string getLineText(i32 line) const;
inline void setTextChanged(bool value) { m_textChanged = value; }
inline bool isTextChanged() { return m_textChanged; }
inline void setReadOnly(bool value) { m_readOnly = value; }
inline void setHandleMouseInputs(bool value) { m_handleMouseInputs = value; }
inline bool isHandleMouseInputsEnabled() const { return m_handleKeyboardInputs; }
inline void setHandleKeyboardInputs(bool value) { m_handleKeyboardInputs = value; }
inline bool isHandleKeyboardInputsEnabled() const { return m_handleKeyboardInputs; }
private:
std::string getText(const Selection &selection) const;
void deleteRange(const Selection &selection);
i32 insertTextAt(Coordinates &where, const std::string &value);
void removeLine(i32 start, i32 end);
void removeLine(i32 index);
Line &insertLine(i32 index);
void insertLine(i32 index, const std::string &text);
void enterCharacter(ImWchar character, bool shift);
void deleteSelection();
// Navigating
public:
void jumpToLine(i32 line = -1);
void jumpToCoords(const Coordinates &coords);
void moveUp(i32 amount = 1, bool select = false);
void moveDown(i32 amount = 1, bool select = false);
void moveLeft(i32 amount = 1, bool select = false, bool wordMode = false);
void moveRight(i32 amount = 1, bool select = false, bool wordMode = false);
void moveTop(bool select = false);
void moveBottom(bool select = false);
void moveHome(bool select = false);
void moveEnd(bool select = false);
void moveToMatchedBracket(bool select = false);
void setScrollY();
Coordinates getCursorPosition() const { 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;
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;
u32 skipSpaces(const Coordinates &from);
//Support
public:
void setSelection(const Selection &selection);
Selection getSelection() const;
void selectWordUnderCursor();
void selectAll();
bool hasSelection() const;
void refreshSearchResults();
i32 getTotalLines() const { return (i32) m_lines.size(); }
FindReplaceHandler *getFindReplaceHandler() { return &m_findReplaceHandler; }
void setSourceCodeEditor(TextEditor *editor) { m_sourceCodeEditor = editor; }
inline void clearBreakpointsChanged() { m_breakPointsChanged = false; }
inline bool isBreakpointsChanged() { return m_breakPointsChanged; }
inline void setImGuiChildIgnored(bool value) { m_ignoreImGuiChild = value; }
inline bool isImGuiChildIgnored() const { return m_ignoreImGuiChild; }
bool raiseContextMenu() { return m_raiseContextMenu; }
void clearRaiseContextMenu() { m_raiseContextMenu = false; }
TextEditor *GetSourceCodeEditor();
bool isEmpty() const;
private:
void addUndo(UndoRecord &value);
TextEditor::PaletteIndex getColorIndexFromFlags(Line::Flags flags);
void handleKeyboardInputs();
void handleMouseInputs();
// utf8
public:
static i32 imTextCharToUtf8(char *buffer, i32 buf_size, u32 c);
static void imTextCharToUtf8(std::string &buffer, u32 c);
static i32 utf8CharLength(uint8_t c);
static i32 getStringCharacterCount(const std::string &str);
static TextEditor::Coordinates stringIndexToCoordinates(i32 strIndex, const std::string &input);
private:
Coordinates screenPosToCoordinates(const ImVec2 &position) const;
Coordinates lineCoordsToIndexCoords(const Coordinates &coordinates) const;
i32 lineCoordinatesToIndex(const Coordinates &coordinates) const;
Coordinates getCharacterCoordinates(i32 line, i32 index) const;
i32 getLineCharacterCount(i32 line) const;
u64 getLineByteCount(i32 line) const;
i32 getLineMaxColumn(i32 line) const;
public:
FindReplaceHandler m_findReplaceHandler;
private:
float m_lineSpacing = 1.0F;
Lines m_lines;
EditorState m_state = {};
UndoBuffer m_undoBuffer;
i32 m_undoIndex = 0;
bool m_scrollToBottom = false;
float m_topMargin = 0.0F;
float m_newTopMargin = 0.0F;
float m_oldTopMargin = 0.0F;
bool m_topMarginChanged = false;
i32 m_tabSize = 4;
bool m_overwrite = false;
bool m_readOnly = false;
bool m_withinRender = false;
bool m_scrollToCursor = false;
bool m_scrollToTop = false;
bool m_textChanged = false;
bool m_colorizerEnabled = true;
float m_lineNumberFieldWidth = 0.0F;
u64 m_longestLineLength = 0;
float m_leftMargin = 10.0;
float m_topLine = 0.0F;
bool m_setTopLine = false;
bool m_breakPointsChanged = false;
bool m_handleKeyboardInputs = true;
bool m_handleMouseInputs = true;
bool m_ignoreImGuiChild = false;
bool m_showWhitespaces = true;
MatchedBracket m_matchedBracket = {};
Palette m_palette = {};
LanguageDefinition m_languageDefinition = {};
RegexList m_regexList;
bool m_updateFlags = true;
Breakpoints m_breakpoints = {};
ErrorMarkers m_errorMarkers = {};
ErrorHoverBoxes m_errorHoverBoxes = {};
ErrorGotoBoxes m_errorGotoBoxes = {};
CursorBoxes m_cursorBoxes = {};
ImVec2 m_charAdvance = {};
Selection m_interactiveSelection = {};
u64 m_startTime = 0;
std::vector<std::string> m_defines;
TextEditor *m_sourceCodeEditor = nullptr;
float m_shiftedScrollY = 0;
float m_scrollYIncrement = 0.0F;
bool m_setScrollY = false;
float m_numberOfLinesDisplayed = 0;
float m_lastClick = -1.0F;
bool m_showCursor = true;
bool m_showLineNumbers = true;
bool m_raiseContextMenu = false;
Coordinates m_focusAtCoords = {};
bool m_updateFocus = false;
std::vector<std::string> m_clickableText;
static const i32 s_cursorBlinkInterval;
static const i32 s_cursorBlinkOnTime;
static ImVec2 s_cursorScreenPosition;
};
bool tokenizeCStyleString(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
bool tokenizeCStyleCharacterLiteral(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
bool tokenizeCStyleIdentifier(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
bool tokenizeCStyleNumber(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
bool tokenizeCStyleOperator(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
bool tokenizeCStyleSeparator(strConstIter in_begin, strConstIter in_end, strConstIter &out_begin, strConstIter &out_end);
}

View File

@@ -27,7 +27,7 @@
#include <hex/helpers/utils.hpp>
#include <wolv/math_eval/math_evaluator.hpp>
#include <TextEditor.h>
#include <ui/text_editor.hpp>
#include <imgui.h>
#include <hex/ui/imgui_imhex_extensions.h>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 BalazsJako
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,811 @@
#include <ui/text_editor.hpp>
#include <algorithm>
#include <chrono>
#include <string>
#include <regex>
#include <cmath>
#include <iostream>
#include <hex/helpers/utils.hpp>
#include <wolv/utils/string.hpp>
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui.h"
namespace hex::ui {
const i32 TextEditor::s_cursorBlinkInterval = 1200;
const i32 TextEditor::s_cursorBlinkOnTime = 800;
ImVec2 TextEditor::s_cursorScreenPosition;
TextEditor::FindReplaceHandler::FindReplaceHandler() : m_matchCase(false), m_wholeWord(false), m_findRegEx(false) {}
const std::string TextEditor::MatchedBracket::s_separators = "()[]{}";
const std::string TextEditor::MatchedBracket::s_operators = "<>";
TextEditor::TextEditor() {
m_startTime = ImGui::GetTime() * 1000;
setLanguageDefinition(LanguageDefinition::HLSL());
m_lines.push_back(Line());
}
TextEditor::~TextEditor() {
}
std::string TextEditor::getText(const Selection &from) const {
std::string result;
auto selection = setCoordinates(from);
auto columns = selection.getSelectedColumns();
if (selection.isSingleLine())
result = m_lines[selection.m_start.m_line].substr(columns.m_line, columns.m_column, Line::LinePart::Utf8);
else {
auto lines = selection.getSelectedLines();
result = m_lines[lines.m_line].substr(columns.m_line, (u64) -1, Line::LinePart::Utf8) + '\n';
for (i32 i = lines.m_line + 1; i < lines.m_column; i++) {
result += m_lines[i].m_chars + '\n';
}
result += m_lines[lines.m_column].substr(0, columns.m_column, Line::LinePart::Utf8);
}
return result;
}
void TextEditor::deleteRange(const Selection &rangeToDelete) {
if (m_readOnly)
return;
Selection selection = setCoordinates(rangeToDelete);
auto columns = selection.getSelectedColumns();
if (selection.isSingleLine()) {
auto &line = m_lines[selection.m_start.m_line];
line.erase(columns.m_line, columns.m_column);
} else {
auto lines = selection.getSelectedLines();
auto &firstLine = m_lines[lines.m_line];
auto &lastLine = m_lines[lines.m_column];
firstLine.erase(columns.m_line,(u64) -1);
lastLine.erase(0, columns.m_column);
if (!lastLine.empty()) {
firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end());
firstLine.m_colorized = false;
}
if (lines.m_line < lines.m_column)
removeLine(lines.m_line + 1, lines.m_column);
}
m_textChanged = true;
}
void TextEditor::appendLine(const std::string &value) {
auto text = wolv::util::replaceStrings(wolv::util::preprocessText(value), "\000", ".");
if (text.empty())
return;
if (isEmpty()) {
m_lines[0].m_chars = text;
m_lines[0].m_colors = std::string(text.size(), 0);
m_lines[0].m_flags = std::string(text.size(), 0);
} else
m_lines.push_back(Line(text));
setCursorPosition(setCoordinates((i32) m_lines.size() - 1, 0));
m_lines.back().m_colorized = false;
ensureCursorVisible();
m_textChanged = true;
}
i32 TextEditor::insertTextAt(Coordinates /* inout */ &where, const std::string &value) {
if (value.empty())
return 0;
auto start = setCoordinates(where);
if (start == Invalid)
return 0;
auto &line = m_lines[start.m_line];
auto stringVector = wolv::util::splitString(value, "\n", false);
auto lineCount = (i32) stringVector.size();
where.m_line += lineCount - 1;
where.m_column += getStringCharacterCount(stringVector[lineCount - 1]);
stringVector[lineCount - 1].append(line.substr(start.m_column,(u64) -1, Line::LinePart::Utf8));
line.erase(start.m_column, (u64) -1);
line.append(stringVector[0]);
line.m_colorized = false;
for (i32 i = 1; i < lineCount; i++) {
insertLine(start.m_line + i, stringVector[i]);
m_lines[start.m_line + i].m_colorized = false;
}
m_textChanged = true;
return lineCount;
}
void TextEditor::deleteWordLeft() {
const auto wordEnd = getCursorPosition();
const auto wordStart = findPreviousWord(getCursorPosition());
setSelection(Selection(wordStart, wordEnd));
backspace();
}
void TextEditor::deleteWordRight() {
const auto wordStart = getCursorPosition();
const auto wordEnd = findNextWord(getCursorPosition());
setSelection(Selection(wordStart, wordEnd));
backspace();
}
void TextEditor::removeLine(i32 lineStart, i32 lineEnd) {
ErrorMarkers errorMarker;
u32 uLineStart = static_cast<u32>(lineStart);
u32 uLineEnd = static_cast<u32>(lineEnd);
for (auto &i: m_errorMarkers) {
ErrorMarkers::value_type e(i.first.m_line >= lineStart ? setCoordinates(i.first.m_line - 1, i.first.m_column) : i.first, i.second);
if (e.first.m_line >= lineStart && e.first.m_line <= lineEnd)
continue;
errorMarker.insert(e);
}
m_errorMarkers = std::move(errorMarker);
Breakpoints breakpoints;
for (auto breakpoint: m_breakpoints) {
if (breakpoint <= uLineStart || breakpoint >= uLineEnd) {
if (breakpoint >= uLineEnd) {
breakpoints.insert(breakpoint - 1);
m_breakPointsChanged = true;
} else
breakpoints.insert(breakpoint);
}
}
m_breakpoints = std::move(breakpoints);
// use clamp to ensure valid results instead of assert.
auto start = std::clamp(lineStart, 0, (i32) m_lines.size() - 1);
auto end = std::clamp(lineEnd, 0, (i32) m_lines.size());
if (start > end)
std::swap(start, end);
m_lines.erase(m_lines.begin() + lineStart, m_lines.begin() + lineEnd + 1);
m_textChanged = true;
}
void TextEditor::removeLine(i32 index) {
removeLine(index, index);
}
void TextEditor::insertLine(i32 index, const std::string &text) {
if (index < 0 || index > (i32) m_lines.size())
return;
auto &line = insertLine(index);
line.append(text);
line.m_colorized = false;
m_textChanged = true;
}
TextEditor::Line &TextEditor::insertLine(i32 index) {
if (isEmpty())
return *m_lines.insert(m_lines.begin(), Line());
if (index == (i32)m_lines.size())
return *m_lines.insert(m_lines.end(), Line());
auto newLine = Line();
TextEditor::Line &result = *m_lines.insert(m_lines.begin() + index, newLine);
result.m_colorized = false;
ErrorMarkers errorMarker;
for (auto &i: m_errorMarkers)
errorMarker.insert(ErrorMarkers::value_type(i.first.m_line >= index ? setCoordinates(i.first.m_line + 1, i.first.m_column) : i.first, i.second));
m_errorMarkers = std::move(errorMarker);
Breakpoints breakpoints;
for (auto breakpoint: m_breakpoints) {
if (breakpoint >= (u32)index) {
breakpoints.insert(breakpoint + 1);
m_breakPointsChanged = true;
} else
breakpoints.insert(breakpoint);
}
if (m_breakPointsChanged)
m_breakpoints = std::move(breakpoints);
return result;
}
void TextEditor::setText(const std::string &text, bool undo) {
UndoRecord u;
if (!m_readOnly && undo) {
u.m_before = m_state;
u.m_removed = getText();
u.m_removedSelection.m_start = setCoordinates(0, 0);
u.m_removedSelection.m_end = setCoordinates(-1, -1);
if (u.m_removedSelection.m_start == Invalid || u.m_removedSelection.m_end == Invalid)
return;
}
auto vectorString = wolv::util::splitString(text, "\n", false);
auto lineCount = vectorString.size();
if (lineCount == 0) {
m_lines.resize(1);
m_lines[0].clear();
} else {
m_lines.resize(lineCount);
u64 i = 0;
for (auto line: vectorString) {
m_lines[i].setLine(line);
m_lines[i].m_colorized = false;
i++;
}
}
if (!m_readOnly && undo) {
u.m_added = text;
u.m_addedSelection.m_start = setCoordinates(0, 0);
u.m_addedSelection.m_end = setCoordinates(-1, -1);
if (u.m_addedSelection.m_start == Invalid || u.m_addedSelection.m_end == Invalid)
return;
}
m_textChanged = true;
m_scrollToTop = true;
if (!m_readOnly && undo) {
u.m_after = m_state;
addUndo(u);
}
colorize();
}
void TextEditor::enterCharacter(ImWchar character, bool shift) {
if (m_readOnly)
return;
UndoRecord u;
u.m_before = m_state;
resetCursorBlinkTime();
if (hasSelection()) {
if (character == '\t') {
auto start = m_state.m_selection.m_start;
auto end = m_state.m_selection.m_end;
auto originalEnd = end;
start.m_column = 0;
if (end.m_column == 0 && end.m_line > 0)
--end.m_line;
if (end.m_line >= (i32) m_lines.size())
end.m_line = isEmpty() ? 0 : (i32) m_lines.size() - 1;
end.m_column = getLineMaxColumn(end.m_line);
u.m_removedSelection = Selection(start, end);
u.m_removed = getText(u.m_removedSelection);
bool modified = false;
for (i32 i = start.m_line; i <= end.m_line; i++) {
auto &line = m_lines[i];
if (shift) {
if (!line.empty()) {
auto index = line.m_chars.find_first_not_of(' ', 0);
if (index == std::string::npos)
index = line.size() - 1;
if (index == 0) continue;
u64 spacesToRemove = (index % m_tabSize) ? (index % m_tabSize) : m_tabSize;
spacesToRemove = std::min(spacesToRemove, line.size());
line.erase(line.begin(), spacesToRemove);
line.m_colorized = false;
modified = true;
}
} else {
auto spacesToInsert = m_tabSize - (start.m_column % m_tabSize);
std::string spaces(spacesToInsert, ' ');
line.insert(line.begin(), spaces.begin(), spaces.end());
line.m_colorized = false;
modified = true;
}
}
if (modified) {
Coordinates rangeEnd;
if (originalEnd.m_column != 0) {
end = setCoordinates(end.m_line, -1);
if (end == Invalid)
return;
rangeEnd = end;
u.m_added = getText(Selection(start, end));
} else {
end = setCoordinates(originalEnd.m_line, 0);
rangeEnd = setCoordinates(end.m_line - 1, -1);
if (end == Invalid || rangeEnd == Invalid)
return;
u.m_added = getText(Selection(start, rangeEnd));
}
u.m_addedSelection = Selection(start, rangeEnd);
u.m_after = m_state;
m_state.m_selection = Selection(start, end);
addUndo(u);
m_textChanged = true;
ensureCursorVisible();
}
return;
} // c == '\t'
else {
u.m_removed = getSelectedText();
u.m_removedSelection = Selection(m_state.m_selection);
deleteSelection();
}
} // HasSelection
auto coord = setCoordinates(m_state.m_cursorPosition);
u.m_addedSelection.m_start = coord;
if (m_lines.empty())
m_lines.push_back(Line());
if (character == '\n') {
insertLine(coord.m_line + 1);
auto &line = m_lines[coord.m_line];
auto &newLine = m_lines[coord.m_line + 1];
if (m_languageDefinition.m_autoIndentation)
for (u64 it = 0; it < line.size() && isascii(line[it]) && isblank(line[it]); ++it)
newLine.push_back(line[it]);
const u64 whitespaceSize = newLine.size();
i32 charStart;
i32 charPosition;
auto charIndex = lineCoordinatesToIndex(coord);
if (charIndex < (i32) whitespaceSize && m_languageDefinition.m_autoIndentation) {
charStart = (i32) whitespaceSize;
charPosition = charIndex;
} else {
charStart = charIndex;
charPosition = (i32) whitespaceSize;
}
newLine.insert(newLine.end(), line.begin() + charStart, line.end());
line.erase(line.begin() + charStart,(u64) -1);
line.m_colorized = false;
setCursorPosition(getCharacterCoordinates(coord.m_line + 1, charPosition));
u.m_added = (char) character;
u.m_addedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
} else if (character == '\t') {
auto &line = m_lines[coord.m_line];
auto charIndex = lineCoordinatesToIndex(coord);
if (!shift) {
auto spacesToInsert = m_tabSize - (charIndex % m_tabSize);
std::string spaces(spacesToInsert, ' ');
line.insert(line.begin() + charIndex, spaces.begin(), spaces.end());
line.m_colorized = false;
setCursorPosition(getCharacterCoordinates(coord.m_line, charIndex + spacesToInsert));
u.m_added = spaces;
u.m_addedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
} else {
auto spacesToRemove = (charIndex % m_tabSize);
if (spacesToRemove == 0) spacesToRemove = m_tabSize;
spacesToRemove = std::min(spacesToRemove, (i32) line.size());
auto spacesRemoved = 0;
for (i32 j = 0; j < spacesToRemove; j++) {
if (*(line.begin() + (charIndex - 1)) == ' ') {
line.erase(line.begin() + (charIndex - 1));
charIndex -= 1;
spacesRemoved++;
}
}
std::string spaces(spacesRemoved, ' ');
u.m_removed = spaces;
u.m_removedSelection = Selection(Coordinates(coord.m_line, charIndex), Coordinates(coord.m_line, charIndex + spacesRemoved));
line.m_colorized = false;
setCursorPosition(getCharacterCoordinates(coord.m_line, std::max(0, charIndex)));
}
u.m_addedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
} else {
std::string buf = "";
imTextCharToUtf8(buf, character);
if (buf.size() > 0) {
auto &line = m_lines[coord.m_line];
auto charIndex = lineCoordinatesToIndex(coord);
if (m_overwrite && charIndex < (i32) line.size()) {
i64 column = coord.m_column;
std::string c = line[column];
auto charCount = getStringCharacterCount(c);
auto d = c.size();
u.m_removedSelection = Selection(m_state.m_cursorPosition, getCharacterCoordinates(coord.m_line, coord.m_column + charCount));
u.m_removed = std::string(line.m_chars.begin() + charIndex, line.m_chars.begin() + charIndex + d);
line.erase(line.begin() + charIndex, d);
line.m_colorized = false;
}
auto charCount = getStringCharacterCount(buf);
if (buf == "{")
buf += "}";
else if (buf == "[")
buf += "]";
else if (buf == "(")
buf += ")";
if ((buf == "}" || buf == "]" || buf == ")") && buf == line.substr(charIndex, charCount))
buf = "";
if (buf == "\"") {
if (buf == line.substr(charIndex, charCount)) {
if (line.m_colors[charIndex + 1] == (char) PaletteIndex::StringLiteral)
buf += "\"";
else
buf = "";
} else
buf += "\"";
}
if (buf == "'") {
if (buf == line.substr(charIndex, charCount)) {
if (line.m_colors[charIndex + 1] == (char) PaletteIndex::CharLiteral)
buf += "'";
else
buf = "";
} else
buf += "'";
}
line.insert(line.begin() + charIndex, buf.begin(), buf.end());
line.m_colorized = false;
u.m_added = buf;
u.m_addedSelection.m_end = getCharacterCoordinates(coord.m_line, charIndex + buf.size());
setCursorPosition(getCharacterCoordinates(coord.m_line, charIndex + charCount));
} else
return;
}
u.m_after = m_state;
m_textChanged = true;
addUndo(u);
colorize();
refreshSearchResults();
ensureCursorVisible();
}
void TextEditor::refreshSearchResults() {
std::string findWord = m_findReplaceHandler.getFindWord();
if (!findWord.empty()) {
m_findReplaceHandler.resetMatches();
m_findReplaceHandler.findAllMatches(this, findWord);
}
}
void TextEditor::insertText(const std::string &value) {
insertText(value.c_str());
}
void TextEditor::insertText(const char *value) {
if (value == nullptr)
return;
auto pos = setCoordinates(m_state.m_cursorPosition);
//auto start = std::min(pos, m_state.m_selection.m_start);
insertTextAt(pos, value);
m_lines[pos.m_line].m_colorized = false;
setSelection(Selection(pos, pos));
setCursorPosition(pos);
refreshSearchResults();
colorize();
}
void TextEditor::deleteSelection() {
if (m_state.m_selection.m_end == m_state.m_selection.m_start)
return;
deleteRange(m_state.m_selection);
setSelection(Selection(m_state.m_selection.m_start, m_state.m_selection.m_start));
setCursorPosition(m_state.m_selection.m_start);
refreshSearchResults();
colorize();
}
void TextEditor::deleteChar() {
resetCursorBlinkTime();
if (isEmpty() || m_readOnly)
return;
UndoRecord u;
u.m_before = m_state;
if (hasSelection()) {
u.m_removed = getSelectedText();
u.m_removedSelection = m_state.m_selection;
deleteSelection();
} else {
auto pos = setCoordinates(m_state.m_cursorPosition);
setCursorPosition(pos);
auto &line = m_lines[pos.m_line];
if (pos.m_column == getLineMaxColumn(pos.m_line)) {
if (pos.m_line == (i32) m_lines.size() - 1)
return;
u.m_removed = '\n';
u.m_removedSelection.m_start = u.m_removedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
advance(u.m_removedSelection.m_end);
auto &nextLine = m_lines[pos.m_line + 1];
line.insert(line.end(), nextLine.begin(), nextLine.end());
line.m_colorized = false;
removeLine(pos.m_line + 1);
} else {
i64 charIndex = lineCoordinatesToIndex(pos);
u.m_removedSelection.m_start = u.m_removedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
u.m_removedSelection.m_end.m_column++;
u.m_removed = getText(u.m_removedSelection);
auto d = utf8CharLength(line[charIndex][0]);
line.erase(line.begin() + charIndex, d);
line.m_colorized = false;
}
m_textChanged = true;
colorize();
}
u.m_after = m_state;
addUndo(u);
refreshSearchResults();
}
void TextEditor::backspace() {
resetCursorBlinkTime();
if (isEmpty() || m_readOnly)
return;
UndoRecord u;
u.m_before = m_state;
if (hasSelection()) {
u.m_removed = getSelectedText();
u.m_removedSelection = m_state.m_selection;
deleteSelection();
} else {
auto pos = setCoordinates(m_state.m_cursorPosition);
auto &line = m_lines[pos.m_line];
if (pos.m_column == 0) {
if (pos.m_line == 0)
return;
u.m_removed = '\n';
u.m_removedSelection.m_start = u.m_removedSelection.m_end = setCoordinates(pos.m_line - 1, -1);
advance(u.m_removedSelection.m_end);
auto &prevLine = m_lines[pos.m_line - 1];
auto prevSize = getLineMaxColumn(pos.m_line - 1);
if (prevSize == 0)
prevLine = line;
else
prevLine.insert(prevLine.end(), line.begin(), line.end());
prevLine.m_colorized = false;
ErrorMarkers errorMarker;
for (auto &i: m_errorMarkers)
errorMarker.insert(ErrorMarkers::value_type(i.first.m_line - 1 == m_state.m_cursorPosition.m_line ? setCoordinates(i.first.m_line - 1, i.first.m_column) : i.first, i.second));
m_errorMarkers = std::move(errorMarker);
removeLine(m_state.m_cursorPosition.m_line);
--m_state.m_cursorPosition.m_line;
m_state.m_cursorPosition.m_column = prevSize;
} else {
pos.m_column -= 1;
i64 column = pos.m_column;
std::string charToRemove = line[column];
if (pos.m_column < (i32) line.size() - 1) {
std::string charToRemoveNext = line[column + 1];
if (charToRemove == "{" && charToRemoveNext == "}") {
charToRemove += "}";
m_state.m_cursorPosition.m_column += 1;
} else if (charToRemove == "[" && charToRemoveNext == "]") {
charToRemove += "]";
m_state.m_cursorPosition.m_column += 1;
} else if (charToRemove == "(" && charToRemoveNext == ")") {
charToRemove += ")";
m_state.m_cursorPosition.m_column += 1;
} else if (charToRemove == "\"" && charToRemoveNext == "\"") {
charToRemove += "\"";
m_state.m_cursorPosition.m_column += 1;
} else if (charToRemove == "'" && charToRemoveNext == "'") {
charToRemove += "'";
m_state.m_cursorPosition.m_column += 1;
}
}
u.m_removedSelection = Selection(pos, m_state.m_cursorPosition);
u.m_removed = charToRemove;
auto charStart = lineCoordinatesToIndex(pos);
auto charEnd = lineCoordinatesToIndex(m_state.m_cursorPosition);
line.erase(line.begin() + charStart, charEnd - charStart);
m_state.m_cursorPosition = pos;
line.m_colorized = false;
}
m_textChanged = true;
ensureCursorVisible();
colorize();
}
u.m_after = m_state;
addUndo(u);
refreshSearchResults();
}
void TextEditor::copy() {
if (hasSelection()) {
ImGui::SetClipboardText(getSelectedText().c_str());
} else {
if (!isEmpty()) {
std::string str;
const auto &line = m_lines[setCoordinates(m_state.m_cursorPosition).m_line];
std::copy(line.m_chars.begin(), line.m_chars.end(), std::back_inserter(str));
ImGui::SetClipboardText(str.c_str());
}
}
}
void TextEditor::cut() {
if (m_readOnly) {
copy();
} else {
if (!hasSelection()) {
auto lineIndex = setCoordinates(m_state.m_cursorPosition).m_line;
if (lineIndex < 0 || lineIndex >= (i32) m_lines.size())
return;
setSelection(Selection(setCoordinates(lineIndex, 0), setCoordinates(lineIndex + 1, 0)));
}
UndoRecord u;
u.m_before = m_state;
u.m_removed = getSelectedText();
u.m_removedSelection = m_state.m_selection;
copy();
deleteSelection();
u.m_after = m_state;
addUndo(u);
}
refreshSearchResults();
}
void TextEditor::paste() {
if (m_readOnly)
return;
auto clipText = ImGui::GetClipboardText();
if (clipText != nullptr) {
auto len = strlen(clipText);
if (len > 0) {
std::string text = wolv::util::preprocessText(clipText);
UndoRecord u;
u.m_before = m_state;
if (hasSelection()) {
u.m_removed = getSelectedText();
u.m_removedSelection = m_state.m_selection;
deleteSelection();
}
u.m_added = text;
u.m_addedSelection.m_start = setCoordinates(m_state.m_cursorPosition);
insertText(text);
u.m_addedSelection.m_end = setCoordinates(m_state.m_cursorPosition);
u.m_after = m_state;
addUndo(u);
}
}
refreshSearchResults();
}
bool TextEditor::canUndo() {
return !m_readOnly && m_undoIndex > 0;
}
bool TextEditor::canRedo() const {
return !m_readOnly && m_undoIndex < (i32) m_undoBuffer.size();
}
void TextEditor::undo(i32 steps) {
while (canUndo() && steps-- > 0)
m_undoBuffer[--m_undoIndex].undo(this);
refreshSearchResults();
}
void TextEditor::redo(i32 steps) {
while (canRedo() && steps-- > 0)
m_undoBuffer[m_undoIndex++].redo(this);
refreshSearchResults();
}
std::string TextEditor::getText() const {
auto start = setCoordinates(0, 0);
auto end = setCoordinates(-1, -1);
if (start == Invalid || end == Invalid)
return "";
return getText(Selection(start, end));
}
std::vector<std::string> TextEditor::getTextLines() const {
std::vector<std::string> result;
result.reserve(m_lines.size());
for (const auto &line: m_lines) {
std::string text = line.m_chars;
result.emplace_back(std::move(text));
}
return result;
}
std::string TextEditor::getSelectedText() const {
return getText(m_state.m_selection);
}
std::string TextEditor::getLineText(i32 line) const {
auto sanitizedLine = setCoordinates(line, 0);
auto endLine = setCoordinates(line, -1);
if (sanitizedLine == Invalid || endLine == Invalid)
return "";
return getText(Selection(sanitizedLine, endLine));
}
TextEditor::UndoRecord::UndoRecord(
const std::string &added,
const TextEditor::Selection addedSelection,
const std::string &removed,
const TextEditor::Selection removedSelection,
TextEditor::EditorState &before,
TextEditor::EditorState &after) : m_added(added), m_addedSelection(addedSelection), m_removed(removed), m_removedSelection(removedSelection), m_before(before), m_after(after) {}
void TextEditor::UndoRecord::undo(TextEditor *editor) {
if (!m_added.empty()) {
editor->deleteRange(m_addedSelection);
editor->colorize();
}
if (!m_removed.empty()) {
auto start = m_removedSelection.m_start;
editor->insertTextAt(start, m_removed.c_str());
editor->colorize();
}
editor->m_state = m_before;
editor->ensureCursorVisible();
}
void TextEditor::UndoRecord::redo(TextEditor *editor) {
if (!m_removed.empty()) {
editor->deleteRange(m_removedSelection);
editor->colorize();
}
if (!m_added.empty()) {
auto start = m_addedSelection.m_start;
editor->insertTextAt(start, m_added.c_str());
editor->colorize();
}
editor->m_state = m_after;
editor->ensureCursorVisible();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,705 @@
#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 = getLineMaxColumn(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 = getLineMaxColumn(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 = getLineMaxColumn(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 = getLineMaxColumn(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, getLineMaxColumn(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) const {
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 = getLineMaxColumn(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) const {
auto sanitized_value = setCoordinates(value.m_line, value.m_column);
return sanitized_value;
}
TextEditor::Selection TextEditor::setCoordinates(const Selection &value) const {
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) const {
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) const {
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) const {
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) const {
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;
}
}
}

View File

@@ -0,0 +1,579 @@
#include "imgui.h"
#include <ui/text_editor.hpp>
#include <algorithm>
namespace hex::ui {
TextEditor::Palette s_paletteBase = TextEditor::getDarkPalette();
inline void TextUnformattedColoredAt(const ImVec2 &pos, const ImU32 &color, const char *text) {
ImGui::SetCursorScreenPos(pos);
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void TextEditor::setTopMarginChanged(i32 newMargin) {
m_newTopMargin = newMargin;
m_topMarginChanged = true;
}
void TextEditor::setFocusAtCoords(const Coordinates &coords) {
m_focusAtCoords = coords;
m_updateFocus = true;
}
void TextEditor::clearErrorMarkers() {
m_errorMarkers.clear();
m_errorHoverBoxes.clear();
}
void TextEditor::clearActionables() {
clearErrorMarkers();
clearGotoBoxes();
clearCursorBoxes();
}
ImVec2 TextEditor::underwaves(ImVec2 pos, u32 nChars, ImColor color, const ImVec2 &size_arg) {
ImGui::GetStyle().AntiAliasedLines = false;
ImGuiWindow *window = ImGui::GetCurrentWindow();
window->DC.CursorPos = pos;
const ImVec2 label_size = ImGui::CalcTextSize("W", nullptr, true);
ImVec2 size = ImGui::CalcItemSize(size_arg, label_size.x, label_size.y);
float lineWidth = size.x / 3.0f + 0.5f;
float halfLineW = lineWidth / 2.0f;
for (u32 i = 0; i < nChars; i++) {
pos = window->DC.CursorPos;
float lineY = pos.y + size.y;
ImVec2 pos1_1 = ImVec2(pos.x + 0 * lineWidth, lineY + halfLineW);
ImVec2 pos1_2 = ImVec2(pos.x + 1 * lineWidth, lineY - halfLineW);
ImVec2 pos2_1 = ImVec2(pos.x + 2 * lineWidth, lineY + halfLineW);
ImVec2 pos2_2 = ImVec2(pos.x + 3 * lineWidth, lineY - halfLineW);
ImGui::GetWindowDrawList()->AddLine(pos1_1, pos1_2, ImU32(color), 0.4f);
ImGui::GetWindowDrawList()->AddLine(pos1_2, pos2_1, ImU32(color), 0.4f);
ImGui::GetWindowDrawList()->AddLine(pos2_1, pos2_2, ImU32(color), 0.4f);
window->DC.CursorPos = ImVec2(pos.x + size.x, pos.y);
}
auto ret = window->DC.CursorPos;
ret.y += size.y;
return ret;
}
void TextEditor::setTabSize(i32 value) {
m_tabSize = std::max(0, std::min(32, value));
}
float TextEditor::getPageSize() const {
auto height = ImGui::GetCurrentWindow()->InnerClipRect.GetHeight();
return height / m_charAdvance.y;
}
bool TextEditor::isEndOfLine() const {
return isEndOfLine(m_state.m_cursorPosition);
}
bool TextEditor::isStartOfLine() const {
return m_state.m_cursorPosition.m_column == 0;
}
bool TextEditor::isEndOfLine(const Coordinates &coordinates) const {
if (coordinates.m_line < (i32) m_lines.size())
return coordinates.m_column >= getStringCharacterCount(m_lines[coordinates.m_line].m_chars);
return true;
}
bool TextEditor::isEndOfFile(const Coordinates &coordinates) const {
if (coordinates.m_line < (i32) m_lines.size())
return coordinates.m_line >= (i32) m_lines.size() - 1 && isEndOfLine(coordinates);
return true;
}
void TextEditor::setTopLine() {
if (!m_withinRender) {
m_setTopLine = true;
return;
} else {
m_setTopLine = false;
ImGui::SetScrollY(m_topLine * m_charAdvance.y);
ensureCursorVisible();
}
}
void TextEditor::render(const char *title, const ImVec2 &size, bool border) {
m_withinRender = true;
if (m_lines.capacity() < 2 * m_lines.size())
m_lines.reserve(2 * m_lines.size());
auto scrollBg = ImGui::GetStyleColorVec4(ImGuiCol_ScrollbarBg);
scrollBg.w = 0.0f;
auto scrollBarSize = ImGui::GetStyle().ScrollbarSize;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(m_palette[(i32) PaletteIndex::Background]));
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImGui::ColorConvertFloat4ToU32(scrollBg));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollBarSize);
auto position = ImGui::GetCursorScreenPos();
if (m_showLineNumbers) {
std::string lineNumber = " " + std::to_string(m_lines.size()) + " ";
m_lineNumberFieldWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, lineNumber.c_str(), nullptr, nullptr).x + m_leftMargin;
ImGui::SetNextWindowPos(position);
ImGui::SetCursorScreenPos(position);
auto lineNoSize = ImVec2(m_lineNumberFieldWidth, size.y);
if (!m_ignoreImGuiChild) {
ImGui::BeginChild("##lineNumbers", lineNoSize, false, ImGuiWindowFlags_NoScrollbar);
ImGui::EndChild();
}
} else {
m_lineNumberFieldWidth = 0;
}
ImVec2 textEditorSize = size;
textEditorSize.x -= m_lineNumberFieldWidth;
bool scroll_x = m_longestLineLength * m_charAdvance.x >= textEditorSize.x;
bool scroll_y = m_lines.size() > 1;
if (!border)
textEditorSize.x -= scrollBarSize;
ImGui::SetCursorScreenPos(ImVec2(position.x + m_lineNumberFieldWidth, position.y));
ImGuiChildFlags childFlags = border ? ImGuiChildFlags_Borders : ImGuiChildFlags_None;
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove;
if (!m_ignoreImGuiChild)
ImGui::BeginChild(title, textEditorSize, childFlags, windowFlags);
auto window = ImGui::GetCurrentWindow();
window->ScrollbarSizes = ImVec2(scrollBarSize * scroll_x, scrollBarSize * scroll_y);
ImGui::GetCurrentWindowRead()->ScrollbarSizes = ImVec2(scrollBarSize * scroll_y, scrollBarSize * scroll_x);
if (scroll_y) {
ImGui::GetCurrentWindow()->ScrollbarY = true;
ImGui::Scrollbar(ImGuiAxis_Y);
ImGui::GetCurrentWindow()->ScrollbarY = false;
}
if (scroll_x) {
ImGui::GetCurrentWindow()->ScrollbarX = true;
ImGui::Scrollbar(ImGuiAxis_X);
ImGui::GetCurrentWindow()->ScrollbarX = false;
}
if (m_handleKeyboardInputs) {
handleKeyboardInputs();
}
if (m_handleMouseInputs)
handleMouseInputs();
colorizeInternal();
renderText(title, position, textEditorSize);
if (!m_ignoreImGuiChild)
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
m_withinRender = false;
ImGui::SetCursorScreenPos(ImVec2(position.x, position.y + size.y - 1));
ImGui::Dummy({});
}
void TextEditor::ensureCursorVisible() {
if (!m_withinRender) {
m_scrollToCursor = true;
return;
}
auto scrollBarSize = ImGui::GetStyle().ScrollbarSize;
float scrollX = ImGui::GetScrollX();
float scrollY = ImGui::GetScrollY();
auto windowPadding = ImGui::GetStyle().FramePadding * 2.0f;
auto height = ImGui::GetWindowHeight() - m_topMargin - scrollBarSize - m_charAdvance.y;
auto width = ImGui::GetWindowWidth() - windowPadding.x - scrollBarSize;
auto top = (i32) rint((m_topMargin > scrollY ? m_topMargin - scrollY : scrollY) / m_charAdvance.y);
auto bottom = top + (i32) rint(height / m_charAdvance.y);
auto left = (i32) rint(scrollX / m_charAdvance.x);
auto right = left + (i32) rint(width / m_charAdvance.x);
auto pos = setCoordinates(m_state.m_cursorPosition);
pos.m_column = (i32) rint(textDistanceToLineStart(pos) / m_charAdvance.x);
bool mScrollToCursorX = true;
bool mScrollToCursorY = true;
if (pos.m_line >= top && pos.m_line <= bottom)
mScrollToCursorY = false;
if ((pos.m_column >= left) && (pos.m_column <= right))
mScrollToCursorX = false;
if (!mScrollToCursorX && !mScrollToCursorY && m_oldTopMargin == m_topMargin) {
m_scrollToCursor = false;
return;
}
if (mScrollToCursorY) {
if (pos.m_line < top)
ImGui::SetScrollY(std::max(0.0f, pos.m_line * m_charAdvance.y));
if (pos.m_line > bottom)
ImGui::SetScrollY(std::max(0.0f, pos.m_line * m_charAdvance.y - height));
}
if (mScrollToCursorX) {
if (pos.m_column < left)
ImGui::SetScrollX(std::max(0.0f, pos.m_column * m_charAdvance.x));
if (pos.m_column > right)
ImGui::SetScrollX(std::max(0.0f, pos.m_column * m_charAdvance.x - width));
}
m_oldTopMargin = m_topMargin;
}
void TextEditor::resetCursorBlinkTime() {
m_startTime = ImGui::GetTime() * 1000 - s_cursorBlinkOnTime;
}
void TextEditor::renderText(const char *title, const ImVec2 &lineNumbersStartPos, const ImVec2 &textEditorSize) {
preRender();
auto drawList = ImGui::GetWindowDrawList();
s_cursorScreenPosition = ImGui::GetCursorScreenPos();
ImVec2 position = lineNumbersStartPos;
if (m_setScrollY)
setScrollY();
auto scrollY = ImGui::GetScrollY();
if (m_setTopLine)
setTopLine();
else
m_topLine = std::max<float>(0.0F, (scrollY - m_topMargin) / m_charAdvance.y);
auto lineNo = m_topLine;
auto lineMax = std::clamp(lineNo + m_numberOfLinesDisplayed, 0.0F, m_lines.size() - 1.0F);
if (!m_lines.empty()) {
bool focused = ImGui::IsWindowFocused();
while (lineNo <= lineMax) {
drawSelection(lineNo);
if (!m_ignoreImGuiChild)
ImGui::EndChild();
if (m_showLineNumbers)
drawLineNumbers(position, lineNo, textEditorSize, focused, drawList);
if (!m_ignoreImGuiChild)
ImGui::BeginChild(title);
if (m_state.m_cursorPosition.m_line == lineNo && m_showCursor && focused)
renderCursor(lineNo, drawList);
renderGotoButtons(lineNo);
// Render colorized text
auto &line = m_lines[lineNo];
if (line.empty()) {
ImGui::Dummy(m_charAdvance);
lineNo = std::floor(lineNo + 1.0F);
if (m_updateFocus)
setFocus();
continue;
}
auto colors = m_lines[lineNo].m_colors;
u64 colorsSize = colors.size();
u64 i = skipSpaces(setCoordinates(lineNo, 0));
while (i < colorsSize) {
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);
if (m_updateFocus)
setFocus();
auto lineStart = setCoordinates(lineNo, i);
drawText(lineStart, i, tokenLength, color);
i += (tokenLength + skipSpaces(lineStart));
}
lineNo = std::floor(lineNo + 1.0F);
}
}
if (m_scrollToCursor)
ensureCursorVisible();
postRender(title, position, lineNo);
}
void TextEditor::setFocus() {
m_state.m_cursorPosition = m_focusAtCoords;
resetCursorBlinkTime();
ensureCursorVisible();
if (!this->m_readOnly)
ImGui::SetKeyboardFocusHere(0);
m_updateFocus = false;
}
void TextEditor::preRender() {
m_charAdvance = calculateCharAdvance();
/* Update palette with the current alpha from style */
for (i32 i = 0; i < (i32) PaletteIndex::Max; ++i) {
auto color = ImGui::ColorConvertU32ToFloat4(s_paletteBase[i]);
color.w *= ImGui::GetStyle().Alpha;
m_palette[i] = ImGui::ColorConvertFloat4ToU32(color);
}
m_numberOfLinesDisplayed = getPageSize();
if (m_scrollToTop) {
m_scrollToTop = false;
ImGui::SetScrollY(0.f);
}
if (m_scrollToBottom && ImGui::GetScrollMaxY() >= ImGui::GetScrollY()) {
m_scrollToBottom = false;
ImGui::SetScrollY(ImGui::GetScrollMaxY());
}
}
void TextEditor::drawSelection(float lineNo) {
ImVec2 lineStartScreenPos = s_cursorScreenPosition + ImVec2(m_leftMargin, m_topMargin + std::floor(lineNo) * m_charAdvance.y);
Selection lineCoords = Selection(setCoordinates(lineNo, 0), setCoordinates(lineNo, -1));
auto drawList = ImGui::GetWindowDrawList();
if (m_state.m_selection.m_start <= lineCoords.m_end && m_state.m_selection.m_end > lineCoords.m_start) {
float selectionStart = textDistanceToLineStart(std::max(m_state.m_selection.m_start, lineCoords.m_start));
float selectionEnd = textDistanceToLineStart(std::min(m_state.m_selection.m_end, lineCoords.m_end)) + m_charAdvance.x * (m_state.m_selection.m_end.m_line > lineNo);
if (selectionStart < selectionEnd) {
ImVec2 rectStart(lineStartScreenPos.x + selectionStart, lineStartScreenPos.y);
ImVec2 rectEnd(lineStartScreenPos.x + selectionEnd, lineStartScreenPos.y + m_charAdvance.y);
drawList->AddRectFilled(rectStart, rectEnd, m_palette[(i32) PaletteIndex::Selection]);
}
}
}
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);
i32 totalDigitCount = std::floor(std::log10(m_lines.size())) + 1;
ImGui::SetCursorScreenPos(position);
if (!m_ignoreImGuiChild)
ImGui::BeginChild("##lineNumbers");
i32 padding = totalDigitCount - std::floor(std::log10(lineNo + 1)) - 1;
std::string space = std::string(padding, ' ');
std::string lineNoStr = space + std::to_string((i32) (lineNo + 1));
ImGui::SetCursorScreenPos(ImVec2(position.x, lineStartScreenPos.y));
if (ImGui::InvisibleButton(lineNoStr.c_str(), ImVec2(m_lineNumberFieldWidth, m_charAdvance.y))) {
if (m_breakpoints.contains(lineNo + 1))
m_breakpoints.erase(lineNo + 1);
else
m_breakpoints.insert(lineNo + 1);
m_breakPointsChanged = true;
auto cursorPosition = setCoordinates(lineNo, 0);
if (cursorPosition == Invalid)
return;
m_state.m_cursorPosition = cursorPosition;
jumpToCoords(m_state.m_cursorPosition);
}
// Draw breakpoints
if (m_breakpoints.count(lineNo + 1) != 0) {
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) PaletteIndex::Breakpoint]);
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());
}
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());
if (!m_ignoreImGuiChild)
ImGui::EndChild();
}
void TextEditor::renderCursor(float lineNo, ImDrawList *drawList) {
ImVec2 lineStartScreenPos = s_cursorScreenPosition + ImVec2(m_leftMargin, m_topMargin + std::floor(lineNo) * m_charAdvance.y);
auto timeEnd = ImGui::GetTime() * 1000;
auto elapsed = timeEnd - m_startTime;
if (elapsed > s_cursorBlinkOnTime) {
float width = 1.0f;
u64 charIndex = lineCoordinatesToIndex(m_state.m_cursorPosition);
float cx = textDistanceToLineStart(m_state.m_cursorPosition);
auto &line = m_lines[std::floor(lineNo)];
if (m_overwrite && charIndex < line.size()) {
char c = line[charIndex];
std::string s(1, c);
width = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, s.c_str()).x;
}
ImVec2 rectStart(lineStartScreenPos.x + cx, lineStartScreenPos.y);
ImVec2 rectEnd(lineStartScreenPos.x + cx + width, lineStartScreenPos.y + m_charAdvance.y);
drawList->AddRectFilled(rectStart, rectEnd, m_palette[(i32) PaletteIndex::Cursor]);
if (elapsed > s_cursorBlinkInterval)
m_startTime = timeEnd;
if (m_matchedBracket.isNearABracket(this, m_state.m_cursorPosition))
m_matchedBracket.findMatchingBracket(this);
}
}
void TextEditor::renderGotoButtons(float lineNo) {
ImVec2 lineStartScreenPos = s_cursorScreenPosition + ImVec2(m_leftMargin, m_topMargin + std::floor(lineNo) * m_charAdvance.y);
auto lineText = getLineText(lineNo);
Coordinates gotoKey = setCoordinates(lineNo + 1, 0);
if (gotoKey != Invalid) {
std::string errorLineColumn;
bool found = false;
for (auto text: m_clickableText) {
if (lineText.find(text) == 0) {
errorLineColumn = lineText.substr(text.size());
if (!errorLineColumn.empty()) {
found = true;
break;
}
}
}
if (found) {
i32 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 = GetSourceCodeEditor()->setCoordinates(currLine, currColumn);
if (errorPos != Invalid) {
ImVec2 errorStart = ImVec2(lineStartScreenPos.x, lineStartScreenPos.y);
auto lineEnd = setCoordinates(lineNo, -1);
if (lineEnd != Invalid) {
ImVec2 errorEnd = ImVec2(lineStartScreenPos.x + textDistanceToLineStart(lineEnd), lineStartScreenPos.y + m_charAdvance.y);
ErrorGotoBox box = ErrorGotoBox(ImRect({errorStart, errorEnd}), errorPos, GetSourceCodeEditor());
m_errorGotoBoxes[gotoKey] = box;
CursorChangeBox cursorBox = CursorChangeBox(ImRect({errorStart, errorEnd}));
m_cursorBoxes[gotoKey] = cursorBox;
}
}
}
if (m_cursorBoxes.find(gotoKey) != m_cursorBoxes.end()) {
auto box = m_cursorBoxes[gotoKey];
if (box.trigger()) box.callback();
}
if (m_errorGotoBoxes.find(gotoKey) != m_errorGotoBoxes.end()) {
auto box = m_errorGotoBoxes[gotoKey];
if (box.trigger()) box.callback();
}
}
}
void TextEditor::drawText(Coordinates &lineStart, u64 i, u32 tokenLength, char color) {
auto &line = m_lines[lineStart.m_line];
ImVec2 lineStartScreenPos = s_cursorScreenPosition + ImVec2(m_leftMargin, m_topMargin + std::floor(lineStart.m_line) * m_charAdvance.y);
auto textStart = textDistanceToLineStart(lineStart);
auto begin = lineStartScreenPos + ImVec2(textStart, 0);
TextUnformattedColoredAt(begin, m_palette[(i32) color], line.substr(i, tokenLength).c_str());
ErrorMarkers::iterator errorIt;
auto key = lineStart + Coordinates(1, 1);
if (errorIt = m_errorMarkers.find(key); errorIt != m_errorMarkers.end()) {
auto errorMessage = errorIt->second.second;
auto errorLength = errorIt->second.first;
if (errorLength == 0)
errorLength = line.size() - i - 1;
auto end = underwaves(begin, errorLength, m_palette[(i32) PaletteIndex::ErrorMarker]);
ErrorHoverBox box = ErrorHoverBox(ImRect({begin, end}), key, errorMessage.c_str());
m_errorHoverBoxes[key] = box;
}
if (m_errorHoverBoxes.find(key) != m_errorHoverBoxes.end()) {
auto box = m_errorHoverBoxes[key];
if (box.trigger()) box.callback();
}
lineStart = lineStart + Coordinates(0, tokenLength);
}
void TextEditor::postRender(const char *title, ImVec2 position, float lineNo) {
ImVec2 lineStartScreenPos = ImVec2(s_cursorScreenPosition.x + m_leftMargin, m_topMargin + s_cursorScreenPosition.y + std::floor(lineNo) * m_charAdvance.y);
float globalLineMax = m_lines.size();
auto lineMax = std::clamp(lineNo + m_numberOfLinesDisplayed, 0.0F, globalLineMax - 1.0F);
if (!m_ignoreImGuiChild)
ImGui::EndChild();
if (m_showLineNumbers && !m_ignoreImGuiChild) {
ImGui::BeginChild("##lineNumbers");
ImGui::SetCursorScreenPos(ImVec2(position.x, lineStartScreenPos.y));
ImGui::Dummy(ImVec2(m_lineNumberFieldWidth, (globalLineMax - lineMax - 1) * m_charAdvance.y + ImGui::GetCurrentWindow()->InnerClipRect.GetHeight() - m_charAdvance.y));
ImGui::EndChild();
}
if (!m_ignoreImGuiChild)
ImGui::BeginChild(title);
ImGui::SetCursorScreenPos(lineStartScreenPos);
if (m_showLineNumbers)
ImGui::Dummy(ImVec2(m_longestLineLength * m_charAdvance.x + m_charAdvance.x, (globalLineMax - lineMax - 2.0F) * m_charAdvance.y + ImGui::GetCurrentWindow()->InnerClipRect.GetHeight()));
else
ImGui::Dummy(ImVec2(m_longestLineLength * m_charAdvance.x + m_charAdvance.x, (globalLineMax - lineMax - 3.0F) * m_charAdvance.y + ImGui::GetCurrentWindow()->InnerClipRect.GetHeight() - 1.0f));
if (m_topMarginChanged) {
m_topMarginChanged = false;
auto window = ImGui::GetCurrentWindow();
auto maxScroll = window->ScrollMax.y;
if (maxScroll > 0) {
float pixelCount;
if (m_newTopMargin > m_topMargin) {
pixelCount = m_newTopMargin - m_topMargin;
} else if (m_newTopMargin > 0) {
pixelCount = m_topMargin - m_newTopMargin;
} else {
pixelCount = m_topMargin;
}
auto oldScrollY = ImGui::GetScrollY();
if (m_newTopMargin > m_topMargin)
m_shiftedScrollY = oldScrollY + pixelCount;
else
m_shiftedScrollY = oldScrollY - pixelCount;
ImGui::SetScrollY(m_shiftedScrollY);
m_topMargin = m_newTopMargin;
}
}
}
ImVec2 TextEditor::calculateCharAdvance() const {
/* Compute mCharAdvance regarding scaled font size (Ctrl + mouse wheel)*/
const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x;
return ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * m_lineSpacing);
}
float TextEditor::textDistanceToLineStart(const Coordinates &aFrom) const {
auto &line = m_lines[aFrom.m_line];
i32 colIndex = lineCoordinatesToIndex(aFrom);
auto substr = line.m_chars.substr(0, colIndex);
return ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, substr.c_str(), nullptr, nullptr).x;
}
}

View File

@@ -0,0 +1,841 @@
#include <ui/text_editor.hpp>
#include <hex/helpers/logger.hpp>
#include <algorithm>
namespace hex::ui {
bool TextEditor::Coordinates::operator==(const Coordinates &o) const {
return
m_line == o.m_line &&
m_column == o.m_column;
}
bool TextEditor::Coordinates::operator!=(const Coordinates &o) const {
return
m_line != o.m_line ||
m_column != o.m_column;
}
bool TextEditor::Coordinates::operator<(const Coordinates &o) const {
if (m_line != o.m_line)
return m_line < o.m_line;
return m_column < o.m_column;
}
bool TextEditor::Coordinates::operator>(const Coordinates &o) const {
if (m_line != o.m_line)
return m_line > o.m_line;
return m_column > o.m_column;
}
bool TextEditor::Coordinates::operator<=(const Coordinates &o) const {
if (m_line != o.m_line)
return m_line < o.m_line;
return m_column <= o.m_column;
}
bool TextEditor::Coordinates::operator>=(const Coordinates &o) const {
if (m_line != o.m_line)
return m_line > o.m_line;
return m_column >= o.m_column;
}
TextEditor::Coordinates TextEditor::Coordinates::operator+(const Coordinates &o) const {
return Coordinates(m_line + o.m_line, m_column + o.m_column);
}
TextEditor::Coordinates TextEditor::Coordinates::operator-(const Coordinates &o) const {
return Coordinates(m_line - o.m_line, m_column - o.m_column);
}
TextEditor::Coordinates TextEditor::Selection::getSelectedLines() {
return Coordinates(m_start.m_line, m_end.m_line);
}
TextEditor::Coordinates TextEditor::Selection::getSelectedColumns() {
if (isSingleLine())
return Coordinates(m_start.m_column, m_end.m_column - m_start.m_column);
return Coordinates(m_start.m_column, m_end.m_column);
}
bool TextEditor::Selection::isSingleLine() {
return m_start.m_line == m_end.m_line;
}
bool TextEditor::Selection::contains(Coordinates coordinates, int8_t endsInclusive) {
bool result = true;
if (endsInclusive & 2)
result &= m_start <= coordinates;
else
result &= m_start < coordinates;
if (!result)
return false;
if (endsInclusive & 1)
result &= coordinates <= m_end;
else
result &= coordinates < m_end;
return result;
}
char TextEditor::Line::LineIterator::operator*() {
return *m_charsIter;
}
TextEditor::Line::LineIterator TextEditor::Line::LineIterator::operator++() {
LineIterator iter = *this;
++iter.m_charsIter;
++iter.m_colorsIter;
++iter.m_flagsIter;
return iter;
}
TextEditor::Line::LineIterator TextEditor::Line::LineIterator::operator=(const LineIterator &other) {
m_charsIter = other.m_charsIter;
m_colorsIter = other.m_colorsIter;
m_flagsIter = other.m_flagsIter;
return *this;
}
bool TextEditor::Line::LineIterator::operator!=(const LineIterator &other) const {
return m_charsIter != other.m_charsIter || m_colorsIter != other.m_colorsIter ||
m_flagsIter != other.m_flagsIter;
}
bool TextEditor::Line::LineIterator::operator==(const LineIterator &other) const {
return m_charsIter == other.m_charsIter && m_colorsIter == other.m_colorsIter &&
m_flagsIter == other.m_flagsIter;
}
TextEditor::Line::LineIterator TextEditor::Line::LineIterator::operator+(i32 n) {
LineIterator iter = *this;
iter.m_charsIter += n;
iter.m_colorsIter += n;
iter.m_flagsIter += n;
return iter;
}
i32 TextEditor::Line::LineIterator::operator-(LineIterator l) {
return m_charsIter - l.m_charsIter;
}
TextEditor::Line::LineIterator TextEditor::Line::begin() const {
LineIterator iter;
iter.m_charsIter = m_chars.begin();
iter.m_colorsIter = m_colors.begin();
iter.m_flagsIter = m_flags.begin();
return iter;
}
TextEditor::Line::LineIterator TextEditor::Line::end() const {
LineIterator iter;
iter.m_charsIter = m_chars.end();
iter.m_colorsIter = m_colors.end();
iter.m_flagsIter = m_flags.end();
return iter;
}
TextEditor::Line::LineIterator TextEditor::Line::begin() {
LineIterator iter;
iter.m_charsIter = m_chars.begin();
iter.m_colorsIter = m_colors.begin();
iter.m_flagsIter = m_flags.begin();
return iter;
}
TextEditor::Line::LineIterator TextEditor::Line::end() {
LineIterator iter;
iter.m_charsIter = m_chars.end();
iter.m_colorsIter = m_colors.end();
iter.m_flagsIter = m_flags.end();
return iter;
}
TextEditor::Line &TextEditor::Line::operator=(const Line &line) {
m_chars = line.m_chars;
m_colors = line.m_colors;
m_flags = line.m_flags;
m_colorized = line.m_colorized;
return *this;
}
TextEditor::Line &TextEditor::Line::operator=(Line &&line) noexcept {
m_chars = std::move(line.m_chars);
m_colors = std::move(line.m_colors);
m_flags = std::move(line.m_flags);
m_colorized = line.m_colorized;
return *this;
}
u64 TextEditor::Line::size() const {
return m_chars.size();
}
char TextEditor::Line::front(LinePart part) const {
if (part == LinePart::Chars && !m_chars.empty())
return m_chars.front();
if (part == LinePart::Colors && !m_colors.empty())
return m_colors.front();
if (part == LinePart::Flags && !m_flags.empty())
return m_flags.front();
return 0x00;
}
std::string TextEditor::Line::frontUtf8(LinePart part) const {
if (part == LinePart::Chars && !m_chars.empty())
return m_chars.substr(0, TextEditor::utf8CharLength(m_chars[0]));
if (part == LinePart::Colors && !m_colors.empty())
return m_colors.substr(0, TextEditor::utf8CharLength(m_chars[0]));
if (part == LinePart::Flags && !m_flags.empty())
return m_flags.substr(0, TextEditor::utf8CharLength(m_chars[0]));
return "";
}
void TextEditor::Line::push_back(char c) {
m_chars.push_back(c);
m_colors.push_back(0x00);
m_flags.push_back(0x00);
m_colorized = false;
}
bool TextEditor::Line::empty() const {
return m_chars.empty();
}
std::string TextEditor::Line::substr(u64 start, u64 length, LinePart part) const {
if (start >= m_chars.size() || m_colors.size() != m_chars.size() || m_flags.size() != m_chars.size())
return "";
if (length == (u64) -1 || start + length >= m_chars.size())
length = m_chars.size() - start;
if (length == 0)
return "";
if (part == LinePart::Chars)
return m_chars.substr(start, length);
if (part == LinePart::Colors)
return m_colors.substr(start, length);
if (part == LinePart::Flags)
return m_flags.substr(start, length);
if (part == LinePart::Utf8) {
u64 utf8Start = 0;
for (u64 utf8Index = 0; utf8Index < start; ++utf8Index) {
utf8Start += TextEditor::utf8CharLength(m_chars[utf8Start]);
}
u64 utf8Length = 0;
for (u64 utf8Index = 0; utf8Index < length; ++utf8Index) {
utf8Length += TextEditor::utf8CharLength(m_chars[utf8Start + utf8Length]);
}
return m_chars.substr(utf8Start, utf8Length);
}
return "";
}
char TextEditor::Line::operator[](u64 index) const {
index = std::clamp(index, (u64) 0, (u64) (m_chars.size() - 1));
return m_chars[index];
}
// C++ can't overload functions based on return type, so use any type other
// than u64 to avoid ambiguity.
std::string TextEditor::Line::operator[](i64 column) const {
u64 utf8Length = TextEditor::getStringCharacterCount(m_chars);
u64 index = static_cast<u64>(column);
index = std::clamp(index, (u64) 0, utf8Length - 1);
u64 utf8Start = 0;
for (u64 utf8Index = 0; utf8Index < index; ++utf8Index) {
utf8Start += TextEditor::utf8CharLength(m_chars[utf8Start]);
}
u64 utf8CharLen = TextEditor::utf8CharLength(m_chars[utf8Start]);
if (utf8Start + utf8CharLen > m_chars.size())
utf8CharLen = m_chars.size() - utf8Start;
return m_chars.substr(utf8Start, utf8CharLen);
}
void TextEditor::Line::setNeedsUpdate(bool needsUpdate) {
m_colorized = m_colorized && !needsUpdate;
}
void TextEditor::Line::append(const char *text) {
append(std::string(text));
}
void TextEditor::Line::append(const char text) {
append(std::string(1, text));
}
void TextEditor::Line::append(const std::string &text) {
Line line(text);
append(line);
}
void TextEditor::Line::append(const Line &line) {
append(line.begin(), line.end());
}
void TextEditor::Line::append(LineIterator begin, LineIterator end) {
if (begin.m_charsIter < end.m_charsIter)
m_chars.append(begin.m_charsIter, end.m_charsIter);
if (begin.m_colorsIter < end.m_colorsIter)
m_colors.append(begin.m_colorsIter, end.m_colorsIter);
if (begin.m_flagsIter < end.m_flagsIter)
m_flags.append(begin.m_flagsIter, end.m_flagsIter);
m_colorized = false;
}
void TextEditor::Line::insert(LineIterator iter, const std::string &text) {
insert(iter, text.begin(), text.end());
}
void TextEditor::Line::insert(LineIterator iter, const char text) {
insert(iter, std::string(1, text));
}
void TextEditor::Line::insert(LineIterator iter, strConstIter beginString, strConstIter endString) {
Line line(std::string(beginString, endString));
insert(iter, line);
}
void TextEditor::Line::insert(LineIterator iter, const Line &line) {
insert(iter, line.begin(), line.end());
}
void TextEditor::Line::insert(LineIterator iter, LineIterator beginLine, LineIterator endLine) {
if (iter == end())
append(beginLine, endLine);
else {
m_chars.insert(iter.m_charsIter, beginLine.m_charsIter, endLine.m_charsIter);
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;
}
}
void TextEditor::Line::erase(LineIterator begin) {
m_chars.erase(begin.m_charsIter);
m_colors.erase(begin.m_colorsIter);
m_flags.erase(begin.m_flagsIter);
m_colorized = false;
}
void TextEditor::Line::erase(LineIterator begin, u64 count) {
if (count == (u64) -1)
count = m_chars.size() - (begin.m_charsIter - m_chars.begin());
m_chars.erase(begin.m_charsIter, begin.m_charsIter + count);
m_colors.erase(begin.m_colorsIter, begin.m_colorsIter + count);
m_flags.erase(begin.m_flagsIter, begin.m_flagsIter + count);
m_colorized = false;
}
void TextEditor::Line::erase(u64 start, u64 length) {
if (length == (u64) -1 || start + length >= m_chars.size())
length = m_chars.size() - start;
u64 utf8Start = 0;
for (u64 utf8Index = 0; utf8Index < start; ++utf8Index) {
utf8Start += TextEditor::utf8CharLength(m_chars[utf8Start]);
}
u64 utf8Length = 0;
for (u64 utf8Index = 0; utf8Index < length; ++utf8Index) {
utf8Length += TextEditor::utf8CharLength(m_chars[utf8Start + utf8Length]);
}
utf8Length = std::min(utf8Length, (u64) (m_chars.size() - utf8Start));
erase(begin() + utf8Start, utf8Length);
}
void TextEditor::Line::clear() {
m_chars.clear();
m_colors.clear();
m_flags.clear();
m_colorized = false;
}
void TextEditor::Line::setLine(const std::string &text) {
m_chars = text;
m_colors = std::string(text.size(), 0x00);
m_flags = std::string(text.size(), 0x00);
m_colorized = false;
}
void TextEditor::Line::setLine(const Line &text) {
m_chars = text.m_chars;
m_colors = text.m_colors;
m_flags = text.m_flags;
m_colorized = text.m_colorized;
}
bool TextEditor::Line::needsUpdate() const {
return !m_colorized;
}
TextEditor *TextEditor::GetSourceCodeEditor() {
if (m_sourceCodeEditor != nullptr)
return m_sourceCodeEditor;
return this;
}
bool TextEditor::isEmpty() const {
if (m_lines.empty())
return true;
if (m_lines.size() == 1) {
if (m_lines[0].empty())
return true;
if (m_lines[0].size() == 1 && m_lines[0].front() == '\n')
return true;
}
return false;
}
void TextEditor::setSelection(const Selection &selection) {
m_state.m_selection = setCoordinates(selection);
}
TextEditor::Selection TextEditor::getSelection() const {
return m_state.m_selection;
}
void TextEditor::selectWordUnderCursor() {
auto wordStart = findWordStart(getCursorPosition());
setSelection(Selection(wordStart, findWordEnd(wordStart)));
}
void TextEditor::selectAll() {
setSelection(Selection(setCoordinates(0, 0), setCoordinates(-1, -1)));
}
bool TextEditor::hasSelection() const {
return !isEmpty() && m_state.m_selection.m_end > m_state.m_selection.m_start;
}
void TextEditor::addUndo(UndoRecord &value) {
if (m_readOnly)
return;
m_undoBuffer.resize((u64) (m_undoIndex + 1));
m_undoBuffer.back() = value;
++m_undoIndex;
}
TextEditor::PaletteIndex TextEditor::getColorIndexFromFlags(Line::Flags flags) {
if (flags.m_bits.globalDocComment)
return PaletteIndex::GlobalDocComment;
if (flags.m_bits.blockDocComment)
return PaletteIndex::DocBlockComment;
if (flags.m_bits.docComment)
return PaletteIndex::DocComment;
if (flags.m_bits.blockComment)
return PaletteIndex::BlockComment;
if (flags.m_bits.comment)
return PaletteIndex::Comment;
if (flags.m_bits.deactivated)
return PaletteIndex::PreprocessorDeactivated;
if (flags.m_bits.preprocessor)
return PaletteIndex::Directive;
return PaletteIndex::Default;
}
void TextEditor::handleKeyboardInputs() {
ImGuiIO &io = ImGui::GetIO();
// command => Ctrl
// control => Super
// option => Alt
auto ctrl = io.KeyCtrl;
auto alt = io.KeyAlt;
auto shift = io.KeyShift;
if (ImGui::IsWindowFocused()) {
if (ImGui::IsWindowHovered())
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
io.WantCaptureKeyboard = true;
io.WantTextInput = true;
if (!m_readOnly && !ctrl && !shift && !alt && (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)))
enterCharacter('\n', false);
else if (!m_readOnly && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Tab))
enterCharacter('\t', shift);
if (!m_readOnly && !io.InputQueueCharacters.empty()) {
for (i32 i = 0; i < io.InputQueueCharacters.Size; i++) {
auto c = io.InputQueueCharacters[i];
if (c != 0 && (c == '\n' || c >= 32)) {
enterCharacter(c, shift);
}
}
io.InputQueueCharacters.resize(0);
}
}
}
void TextEditor::handleMouseInputs() {
ImGuiIO &io = ImGui::GetIO();
auto shift = io.KeyShift;
auto ctrl = io.ConfigMacOSXBehaviors ? io.KeyAlt : io.KeyCtrl;
auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt;
if (ImGui::IsWindowHovered()) {
if (!alt) {
auto click = ImGui::IsMouseClicked(0);
auto doubleClick = ImGui::IsMouseDoubleClicked(0);
auto rightClick = ImGui::IsMouseClicked(1);
auto t = ImGui::GetTime();
auto tripleClick = click && !doubleClick && (m_lastClick != -1.0f && (t - m_lastClick) < io.MouseDoubleClickTime);
bool resetBlinking = false;
/*
Left mouse button triple click
*/
if (tripleClick) {
if (!ctrl) {
m_state.m_cursorPosition = screenPosToCoordinates(ImGui::GetMousePos());
auto line = m_state.m_cursorPosition.m_line;
m_state.m_selection.m_start = setCoordinates(line, 0);
m_state.m_selection.m_end = setCoordinates(line, getLineMaxColumn(line));
}
m_lastClick = -1.0f;
resetBlinking = true;
}
/*
Left mouse button double click
*/
else if (doubleClick) {
if (!ctrl) {
m_state.m_cursorPosition = screenPosToCoordinates(ImGui::GetMousePos());
m_state.m_selection.m_start = findWordStart(m_state.m_cursorPosition);
m_state.m_selection.m_end = findWordEnd(m_state.m_cursorPosition);
}
m_lastClick = (float) ImGui::GetTime();
resetBlinking = true;
}
/*
Left mouse button click
*/
else if (click) {
if (ctrl) {
m_state.m_cursorPosition = m_interactiveSelection.m_start = m_interactiveSelection.m_end = screenPosToCoordinates(ImGui::GetMousePos());
selectWordUnderCursor();
} else if (shift) {
m_interactiveSelection.m_end = screenPosToCoordinates(ImGui::GetMousePos());
m_state.m_cursorPosition = m_interactiveSelection.m_end;
setSelection(m_interactiveSelection);
} else {
m_state.m_cursorPosition = m_interactiveSelection.m_start = m_interactiveSelection.m_end = screenPosToCoordinates(ImGui::GetMousePos());
setSelection(m_interactiveSelection);
}
resetCursorBlinkTime();
ensureCursorVisible();
m_lastClick = (float) ImGui::GetTime();
} else if (rightClick) {
auto cursorPosition = screenPosToCoordinates(ImGui::GetMousePos());
if (!hasSelection() || m_state.m_selection.m_start > cursorPosition || cursorPosition > m_state.m_selection.m_end) {
m_state.m_cursorPosition = m_interactiveSelection.m_start = m_interactiveSelection.m_end = cursorPosition;
setSelection(m_interactiveSelection);
}
resetCursorBlinkTime();
m_raiseContextMenu = true;
ImGui::SetWindowFocus();
}
// Mouse left button dragging (=> update selection)
else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) {
io.WantCaptureMouse = true;
m_state.m_cursorPosition = m_interactiveSelection.m_end = screenPosToCoordinates(ImGui::GetMousePos());
setSelection(m_interactiveSelection);
resetBlinking = true;
}
if (resetBlinking)
resetCursorBlinkTime();
}
}
}
// the index here is array index so zero based
void TextEditor::FindReplaceHandler::selectFound(TextEditor *editor, i32 found) {
if (found < 0 || found >= (i64) m_matches.size())
return;
editor->setSelection(m_matches[found].m_selection);
editor->setCursorPosition();
}
// The returned index is shown in the form
// 'index of count' so 1 based
u32 TextEditor::FindReplaceHandler::findMatch(TextEditor *editor, bool isNext) {
if (editor->m_textChanged || m_optionsChanged) {
std::string findWord = getFindWord();
if (findWord.empty())
return 0;
resetMatches();
findAllMatches(editor, findWord);
}
Coordinates targetPos = editor->m_state.m_cursorPosition;
if (editor->hasSelection())
targetPos = isNext ? editor->m_state.m_selection.m_end : editor->m_state.m_selection.m_start;
auto count = m_matches.size();
if (count == 0) {
editor->setCursorPosition(targetPos);
return 0;
}
if (isNext) {
for (u32 i = 0; i < count; i++) {
if (targetPos <= m_matches[i].m_selection.m_start) {
selectFound(editor, i);
return i + 1;
}
}
selectFound(editor, 0);
return 1;
} else {
for (i32 i = count - 1; i >= 0; i--) {
if (targetPos >= m_matches[i].m_selection.m_end) {
selectFound(editor, i);
return i + 1;
}
}
selectFound(editor, count - 1);
return count;
}
}
// returns 1 based index
u32 TextEditor::FindReplaceHandler::findPosition(TextEditor *editor, Coordinates pos, bool isNext) {
if (editor->m_textChanged || m_optionsChanged) {
std::string findWord = getFindWord();
if (findWord.empty())
return 0;
resetMatches();
findAllMatches(editor, findWord);
}
i32 count = m_matches.size();
if (count == 0)
return 0;
if (isNext) {
for (i32 i = 0; i < count; i++) {
auto interval = Selection(m_matches[i==0 ? count-1 : i - 1].m_selection.m_end,m_matches[i].m_selection.m_end);
if (interval.contains(pos))
return i + 1;
}
} else {
for (i32 i = 0; i < count; i++) {
auto interval = Selection(m_matches[i == 0 ? count - 1 : i - 1].m_selection.m_start, m_matches[i].m_selection.m_start);
if (interval.contains(pos, 2))
return i == 0 ? count : i;
}
}
return 0;
}
// Create a string that escapes special characters
// and separate word from non word
std::string make_wholeWord(const std::string &s) {
static const char metacharacters[] = R"(\.^$-+()[]{}|?*)";
std::string out;
out.reserve(s.size());
if (s[0] == '#')
out.push_back('#');
out.push_back('\\');
out.push_back('b');
for (auto ch: s) {
if (strchr(metacharacters, ch))
out.push_back('\\');
out.push_back(ch);
}
out.push_back('\\');
out.push_back('b');
return out;
}
// Performs actual search to fill mMatches
bool TextEditor::FindReplaceHandler::findNext(TextEditor *editor) {
Coordinates curPos = m_matches.empty() ? editor->m_state.m_cursorPosition : editor->lineCoordsToIndexCoords(m_matches.back().m_cursorPosition);
u64 matchLength = getStringCharacterCount(m_findWord);
u64 matchBytes = m_findWord.size();
u64 byteIndex = 0;
for (i64 ln = 0; ln < curPos.m_line; ln++)
byteIndex += editor->getLineByteCount(ln) + 1;
byteIndex += curPos.m_column;
std::string wordLower = m_findWord;
if (!getMatchCase())
std::transform(wordLower.begin(), wordLower.end(), wordLower.begin(), ::tolower);
std::string textSrc = editor->getText();
if (!getMatchCase())
std::transform(textSrc.begin(), textSrc.end(), textSrc.begin(), ::tolower);
u64 textLoc;
// TODO: use regexp find iterator in all cases
// to find all matches for FindAllMatches.
// That should make things faster (no need
// to call FindNext many times) and remove
// clunky match case code
if (getWholeWord() || getFindRegEx()) {
std::regex regularExpression;
if (getFindRegEx()) {
try {
regularExpression.assign(wordLower);
} catch (const std::regex_error &e) {
hex::log::error("Error in regular expression: {}", e.what());
return false;
}
} else {
try {
regularExpression.assign(make_wholeWord(wordLower));
} catch (const std::regex_error &e) {
hex::log::error("Error in regular expression: {}", e.what());
return false;
}
}
u64 pos = 0;
std::sregex_iterator iter = std::sregex_iterator(textSrc.begin(), textSrc.end(), regularExpression);
std::sregex_iterator end;
if (!iter->ready())
return false;
u64 firstLoc = iter->position();
u64 firstLength = iter->length();
if (firstLoc > byteIndex) {
pos = firstLoc;
matchLength = firstLength;
} else {
while (iter != end) {
iter++;
if (((pos = iter->position()) > byteIndex) && ((matchLength = iter->length()) > 0))
break;
}
}
if (iter == end)
return false;
textLoc = pos;
} else {
// non regex search
textLoc = textSrc.find(wordLower, byteIndex);
if (textLoc == std::string::npos)
return false;
}
if (textLoc == std::string::npos)
return false;
TextEditor::EditorState state;
state.m_selection = Selection(TextEditor::stringIndexToCoordinates(textLoc, textSrc), TextEditor::stringIndexToCoordinates(textLoc + matchBytes, textSrc));
state.m_cursorPosition = state.m_selection.m_end;
m_matches.push_back(state);
return true;
}
void TextEditor::FindReplaceHandler::findAllMatches(TextEditor *editor, std::string findWord) {
if (findWord.empty()) {
editor->ensureCursorVisible();
m_findWord = "";
m_matches.clear();
return;
}
if (findWord == m_findWord && !editor->m_textChanged && !m_optionsChanged)
return;
if (m_optionsChanged)
m_optionsChanged = false;
m_matches.clear();
m_findWord = findWord;
auto startingPos = editor->m_state.m_cursorPosition;
auto saveState = editor->m_state;
Coordinates begin = editor->setCoordinates(0, 0);
editor->m_state.m_cursorPosition = begin;
if (!findNext(editor)) {
editor->m_state = saveState;
editor->ensureCursorVisible();
return;
}
TextEditor::EditorState state = m_matches.back();
while (state.m_cursorPosition < startingPos) {
if (!findNext(editor)) {
editor->m_state = saveState;
editor->ensureCursorVisible();
return;
}
state = m_matches.back();
}
while (findNext(editor));
editor->m_state = saveState;
editor->ensureCursorVisible();
return;
}
bool TextEditor::FindReplaceHandler::replace(TextEditor *editor, bool right) {
if (m_matches.empty() || m_findWord == m_replaceWord || m_findWord.empty())
return false;
auto state = editor->m_state;
if (editor->m_state.m_selection.contains(editor->m_state.m_cursorPosition)) {
editor->m_state.m_cursorPosition = editor->m_state.m_selection.m_start;
if (editor->isStartOfLine()) {
editor->m_state.m_cursorPosition.m_line--;
editor->m_state.m_cursorPosition.m_column = editor->getLineMaxColumn(editor->m_state.m_cursorPosition.m_line);
} else
editor->m_state.m_cursorPosition.m_column--;
}
auto matchIndex = findMatch(editor, right);
if (matchIndex != 0) {
UndoRecord u;
u.m_before = editor->m_state;
u.m_removed = editor->getSelectedText();
u.m_removedSelection = editor->m_state.m_selection;
editor->deleteSelection();
if (getFindRegEx()) {
std::string replacedText = std::regex_replace(editor->getText(), std::regex(m_findWord), m_replaceWord, std::regex_constants::format_first_only | std::regex_constants::format_no_copy);
u.m_added = replacedText;
} else
u.m_added = m_replaceWord;
u.m_addedSelection.m_start = editor->setCoordinates(editor->m_state.m_cursorPosition);
editor->insertText(u.m_added);
editor->setCursorPosition(editor->m_state.m_selection.m_end);
u.m_addedSelection.m_end = editor->setCoordinates(editor->m_state.m_cursorPosition);
editor->ensureCursorVisible();
ImGui::SetKeyboardFocusHere(0);
u.m_after = editor->m_state;
editor->addUndo(u);
editor->m_textChanged = true;
return true;
}
editor->m_state = state;
return false;
}
bool TextEditor::FindReplaceHandler::replaceAll(TextEditor *editor) {
u32 count = m_matches.size();
for (u32 i = 0; i < count; i++)
replace(editor, true);
return true;
}
}

View File

@@ -0,0 +1,168 @@
#include <ui/text_editor.hpp>
#include <algorithm>
namespace hex::ui {
// https://en.wikipedia.org/wiki/UTF-8
// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code sequence (non-10xxxxxx code)
i32 TextEditor::utf8CharLength(u8 c) {
if ((c & 0xFE) == 0xFC)
return 6;
if ((c & 0xFC) == 0xF8)
return 5;
if ((c & 0xF8) == 0xF0)
return 4;
if ((c & 0xF0) == 0xE0)
return 3;
if ((c & 0xE0) == 0xC0)
return 2;
return 1;
}
i32 TextEditor::getStringCharacterCount(const std::string &str) {
if (str.empty())
return 0;
i32 count = 0;
for (u32 idx = 0; idx < str.size(); count++)
idx += TextEditor::utf8CharLength(str[idx]);
return count;
}
// "Borrowed" from ImGui source
void TextEditor::imTextCharToUtf8(std::string &buffer, u32 c) {
if (c < 0x80) {
buffer += (char) c;
return;
}
if (c < 0x800) {
buffer += (char) (0xc0 + (c >> 6));
buffer += (char) (0x80 + (c & 0x3f));
return;
}
if (c >= 0xdc00 && c < 0xe000)
return;
if (c >= 0xd800 && c < 0xdc00) {
buffer += (char) (0xf0 + (c >> 18));
buffer += (char) (0x80 + ((c >> 12) & 0x3f));
buffer += (char) (0x80 + ((c >> 6) & 0x3f));
buffer += (char) (0x80 + ((c) & 0x3f));
return;
}
// else if (c < 0x10000)
{
buffer += (char) (0xe0 + (c >> 12));
buffer += (char) (0x80 + ((c >> 6) & 0x3f));
buffer += (char) (0x80 + ((c) & 0x3f));
return;
}
}
i32 TextEditor::imTextCharToUtf8(char *buffer, i32 buf_size, u32 c) {
std::string input;
imTextCharToUtf8(input, c);
auto size = std::min((i32)input.size(),buf_size - 1);
i32 i = 0;
for (; i < size; i++)
buffer[i] = input[i];
buffer[i] = 0;
return size;
}
static i32 utf8CharCount(const std::string &line, i32 start, i32 numChars) {
if (line.empty())
return 0;
i32 index = 0;
for (i32 column = 0; start + index < (i32) line.size() && column < numChars; ++column)
index += TextEditor::utf8CharLength(line[start + index]);
return index;
}
TextEditor::Coordinates TextEditor::screenPosToCoordinates(const ImVec2 &position) const {
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())
return setCoordinates(std::min(lineNo, (i32) m_lines.size() - 1), 0);
std::string line = m_lines[lineNo].m_chars;
local.x -= (m_leftMargin - 5);
i32 count = 0;
u64 length;
i32 increase;
do {
increase = TextEditor::utf8CharLength(line[count]);
count += increase;
std::string partialLine = line.substr(0, count);
length = ImGui::CalcTextSize(partialLine.c_str(), nullptr, false, m_charAdvance.x * count).x;
} while (length < local.x && count < (i32) line.size() + increase);
auto result = getCharacterCoordinates(lineNo, count - increase);
result = setCoordinates(result);
if (result == Invalid)
return Coordinates(0, 0);
return result;
}
TextEditor::Coordinates TextEditor::lineCoordsToIndexCoords(const Coordinates &coordinates) const {
if (coordinates.m_line >= (i64) m_lines.size())
return Invalid;
const auto &line = m_lines[coordinates.m_line];
return Coordinates(coordinates.m_line,utf8CharCount(line.m_chars, 0, coordinates.m_column));
}
i32 TextEditor::lineCoordinatesToIndex(const Coordinates &coordinates) const {
if (coordinates.m_line >= (i64) m_lines.size())
return -1;
const auto &line = m_lines[coordinates.m_line];
return utf8CharCount(line.m_chars, 0, coordinates.m_column);
}
i32 TextEditor::Line::getCharacterColumn(i32 index) const {
i32 col = 0;
i32 i = 0;
while (i < index && i < (i32) size()) {
auto c = m_chars[i];
i += TextEditor::utf8CharLength(c);
col++;
}
return col;
}
TextEditor::Coordinates TextEditor::getCharacterCoordinates(i32 lineIndex, i32 strIndex) const {
if (lineIndex < 0 || lineIndex >= (i32) m_lines.size())
return Coordinates(0, 0);
auto &line = m_lines[lineIndex];
return setCoordinates(lineIndex, line.getCharacterColumn(strIndex));
}
u64 TextEditor::getLineByteCount(i32 lineIndex) const {
if (lineIndex >= (i64) m_lines.size() || lineIndex < 0)
return 0;
auto &line = m_lines[lineIndex];
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);
}
TextEditor::Coordinates TextEditor::stringIndexToCoordinates(i32 strIndex, const std::string &input) {
if (strIndex < 0 || strIndex > (i32) input.size())
return TextEditor::Coordinates(0, 0);
std::string str = input.substr(0, strIndex);
auto line = std::count(str.begin(), str.end(), '\n');
auto index = str.find_last_of('\n');
str = str.substr(index + 1);
auto col = TextEditor::getStringCharacterCount(str);
return TextEditor::Coordinates(line, col);
}
}