1
0
mirror of synced 2025-01-09 13:11:38 +01:00

feat: Added multiline search/replace and fixed various crashes. (#1911)

### Problem description
Previous implementation ignored everything after the first newline.
Other changes include:
- Added isEmpty() function that checks if editor has no content.
- replaced asserts with default behavior to avoid unneeded crashes.
- fixed off by one error in DeleteRange()
- some crashes occurred because readily available corrective actions
were not being taken. For example start/end being -1 in DeleteRange().
- At the heart of search/replace is the ability to translate from
indices into the text string to line/column coordinates used for
everything. To this end a function (StringIndexToCoordinates) was added
to do the translation taking utf-8 chars into account. This made the
recently added Utf8BytesToChars function unneeded, so it was removed.
- Removed commented out code that is not useful anymore. Also removed
tooltip code which is also unused.
- Removed unused parameter wrapAround to FindNext().
This commit is contained in:
paxcut 2024-11-29 01:37:05 -07:00 committed by GitHub
parent 05c25b6aff
commit 6d2761f141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 87 additions and 134 deletions

View File

@ -211,6 +211,10 @@ public:
void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
void SetText(const std::string& aText);
std::string GetText() const;
bool isEmpty() const {
auto text = GetText();
return text.empty() || text == "\n";
}
void SetTextLines(const std::vector<std::string>& aLines);
std::vector<std::string> GetTextLines() const;
@ -314,7 +318,7 @@ public:
FindReplaceHandler();
typedef std::vector<EditorState> Matches;
Matches &GetMatches() { return mMatches; }
bool FindNext(TextEditor *editor,bool wrapAround);
bool FindNext(TextEditor *editor);
unsigned FindMatch(TextEditor *editor,bool isNex);
bool Replace(TextEditor *editor,bool right);
bool ReplaceAll(TextEditor *editor);
@ -428,10 +432,10 @@ private:
Coordinates FindWordStart(const Coordinates& aFrom) const;
Coordinates FindWordEnd(const Coordinates& aFrom) const;
Coordinates FindNextWord(const Coordinates& aFrom) const;
Coordinates StringIndexToCoordinates(int aIndex, const std::string &str) const;
int GetCharacterIndex(const Coordinates& aCoordinates) const;
int GetCharacterColumn(int aLine, int aIndex) const;
int GetLineCharacterCount(int aLine) const;
int Utf8BytesToChars(const Coordinates &aCoordinates) const;
int Utf8CharsToBytes(const Coordinates &aCoordinates) const;
unsigned long long GetLineByteCount(int aLine) const;
int GetStringCharacterCount(std::string str) const;

View File

@ -123,7 +123,7 @@ TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates &aValu
auto line = aValue.mLine;
auto column = aValue.mColumn;
if (line >= (int)mLines.size()) {
if (mLines.empty()) {
if (isEmpty()) {
line = 0;
column = 0;
} else {
@ -132,7 +132,7 @@ TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates &aValu
}
return Coordinates(line, column);
} else {
column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line));
column = isEmpty() ? 0 : std::min(column, GetLineMaxColumn(line));
return Coordinates(line, column);
}
}
@ -213,6 +213,8 @@ void TextEditor::DeleteRange(const Coordinates &aStart, const Coordinates &aEnd)
auto start = GetCharacterIndex(aStart);
auto end = GetCharacterIndex(aEnd);
if (start == -1 || end == -1)
return;
if (aStart.mLine == aEnd.mLine) {
auto &line = mLines[aStart.mLine];
@ -232,7 +234,7 @@ void TextEditor::DeleteRange(const Coordinates &aStart, const Coordinates &aEnd)
firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end());
if (aStart.mLine < aEnd.mLine)
RemoveLine(aStart.mLine + 1, aEnd.mLine + 1);
RemoveLine(aStart.mLine + 1, aEnd.mLine);
}
mTextChanged = true;
@ -242,7 +244,10 @@ int TextEditor::InsertTextAt(Coordinates & /* inout */ aWhere, const char *aValu
int cindex = GetCharacterIndex(aWhere);
int totalLines = 0;
while (*aValue != '\0') {
assert(!mLines.empty());
if (mLines.empty()) {
mLines.push_back(Line());
mTextChanged = true;
}
if (*aValue == '\r') {
// skip
@ -422,7 +427,7 @@ TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates &aFrom) const
bool isword = false;
bool skip = false;
if (cindex < (int)mLines[at.mLine].size()) {
auto &line = mLines[at.mLine];
const auto &line = mLines[at.mLine];
isword = isalnum(line[cindex].mChar);
skip = isword;
}
@ -455,26 +460,12 @@ TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates &aFrom) const
return at;
}
int TextEditor::Utf8BytesToChars(const Coordinates &aCoordinates) const {
if (aCoordinates.mLine >= mLines.size())
return -1;
auto &line = mLines[aCoordinates.mLine];
int c = 0;
int i = 0;
while (i < aCoordinates.mColumn) {
i += UTF8CharLength(line[i].mChar);
if (line[i].mChar == '\t')
c = (c / mTabSize) * mTabSize + mTabSize;
else
++c;
}
return c;
}
int TextEditor::Utf8CharsToBytes(const Coordinates &aCoordinates) const {
if (aCoordinates.mLine >= mLines.size())
return -1;
auto &line = mLines[aCoordinates.mLine];
if (line.empty())
return 0;
int c = 0;
int i = 0;
while (i < line.size() && c < aCoordinates.mColumn) {
@ -487,6 +478,18 @@ int TextEditor::Utf8CharsToBytes(const Coordinates &aCoordinates) const {
return i;
}
TextEditor::Coordinates TextEditor::StringIndexToCoordinates(int aIndex, const std::string &input ) const {
if (aIndex < 0 || aIndex > (int)input.size())
return Coordinates(0, 0);
std::string str = input.substr(0, aIndex);
auto line = std::count(str.begin(),str.end(),'\n');
auto index = str.find_last_of('\n');
str = str.substr(index+1);
auto col = GetStringCharacterCount(str);
return Coordinates(line, col);
}
int TextEditor::GetCharacterIndex(const Coordinates &aCoordinates) const {
if (aCoordinates.mLine >= mLines.size())
return -1;
@ -580,7 +583,6 @@ bool TextEditor::IsOnWordBoundary(const Coordinates &aAt) const {
void TextEditor::RemoveLine(int aStart, int aEnd) {
assert(!mReadOnly);
assert(aEnd >= aStart);
assert(mLines.size() > (size_t)(aEnd - aStart));
ErrorMarkers etmp;
for (auto &i : mErrorMarkers) {
@ -601,12 +603,12 @@ void TextEditor::RemoveLine(int aStart, int aEnd) {
btmp.insert(breakpoint);
}
}
if (mBreakPointsChanged)
mBreakpoints = std::move(btmp);
if (aStart == 0 && aEnd == (int32_t)mLines.size() - 1)
mLines.erase(mLines.begin() + aStart, mLines.end());
else
mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd + 1);
mBreakpoints = std::move(btmp);
// use clamp to ensure valid results instead of assert.
auto start = std::clamp(aStart, 0, (int)mLines.size()-1);
auto end = std::clamp(aEnd, 0, (int)mLines.size());
mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd + 1);
mTextChanged = true;
}
@ -642,7 +644,16 @@ void TextEditor::RemoveLine(int aIndex) {
}
TextEditor::Line &TextEditor::InsertLine(int aIndex) {
auto &result = *mLines.insert(mLines.begin() + aIndex, Line());
if (isEmpty())
return *mLines.insert(mLines.begin(), Line());
if (aIndex == mLines.size())
return *mLines.insert(mLines.end(), Line());
auto newLine = Line();
TextEditor::Line &result = *mLines.insert(mLines.begin() + aIndex, newLine);
ErrorMarkers etmp;
for (auto &i : mErrorMarkers)
@ -717,22 +728,10 @@ void TextEditor::HandleKeyboardInputs() {
auto ctrl = io.KeyCtrl;
auto alt = io.KeyAlt;
auto shift = io.KeyShift;
/* auto left = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
auto right = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
auto up = ImGui::IsKeyPressed(ImGuiKey_UpArrow);
auto down = ImGui::IsKeyPressed(ImGuiKey_DownArrow);
auto home = io.ConfigMacOSXBehaviors ? io.KeySuper && left : ImGui::IsKeyPressed(ImGuiKey_Home);
auto end = io.ConfigMacOSXBehaviors ? io.KeySuper && right : ImGui::IsKeyPressed(ImGuiKey_End);
auto top = io.ConfigMacOSXBehaviors ? io.KeySuper && up : ctrl && ImGui::IsKeyPressed(ImGuiKey_Home);
auto bottom = io.ConfigMacOSXBehaviors ? io.KeySuper && down : ctrl && ImGui::IsKeyPressed(ImGuiKey_End);
auto pageUp = io.ConfigMacOSXBehaviors ? ctrl && up : ImGui::IsKeyPressed(ImGuiKey_PageUp);
auto pageDown = io.ConfigMacOSXBehaviors ? ctrl && down : ImGui::IsKeyPressed(ImGuiKey_PageDown);
*/
if (ImGui::IsWindowFocused()) {
if (ImGui::IsWindowHovered())
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
// ImGui::CaptureKeyboardFromApp(true);
io.WantCaptureKeyboard = true;
io.WantTextInput = true;
@ -865,7 +864,7 @@ void TextEditor::Render() {
auto scrollX = ImGui::GetScrollX();
auto scrollY = ImGui::GetScrollY();
auto lineNo = (int)(std::floor(scrollY / mCharAdvance.y));// + linesAdded);
auto lineNo = (int)(std::floor(scrollY / mCharAdvance.y));
auto globalLineMax = (int)mLines.size();
auto lineMax = std::max(0, std::min((int)mLines.size() - 1, lineNo + (int)std::ceil((scrollY + contentSize.y) / mCharAdvance.y)));
@ -878,7 +877,7 @@ void TextEditor::Render() {
buf[0] = '\0';
mTextStart = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x + mLeftMargin;
if (!mLines.empty()) {
if (!isEmpty()) {
float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x;
while (lineNo <= lineMax) {
@ -1048,25 +1047,6 @@ void TextEditor::Render() {
if (lineNo < mLines.size() && ImGui::GetScrollMaxX() > 0.0f)
longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest);
// Draw a tooltip on known identifiers/preprocessor symbols
if (ImGui::IsMousePosValid()) {
auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos()));
if (!id.empty()) {
auto it = mLanguageDefinition.mIdentifiers.find(id);
if (it != mLanguageDefinition.mIdentifiers.end() && !it->second.mDeclaration.empty()) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(it->second.mDeclaration.c_str());
ImGui::EndTooltip();
} else {
auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id);
if (pi != mLanguageDefinition.mPreprocIdentifiers.end() && !pi->second.mDeclaration.empty()) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(pi->second.mDeclaration.c_str());
ImGui::EndTooltip();
}
}
}
}
}
ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y));
@ -1098,8 +1078,7 @@ void TextEditor::Render() {
auto popupStack = g.OpenPopupStack;
if (popupStack.Size > 0) {
for (int n = 0; n < popupStack.Size; n++){
auto window = popupStack[n].Window;
if (window != nullptr) {
if (auto window = popupStack[n].Window; window != nullptr) {
if (window->Size.x == mFindReplaceHandler.GetFindWindowSize().x &&
window->Size.y == mFindReplaceHandler.GetFindWindowSize().y &&
window->Pos.x == mFindReplaceHandler.GetFindWindowPos().x &&
@ -1112,11 +1091,9 @@ void TextEditor::Render() {
mTopMargin = 0;
}
static float linesAdded = 0;
static float pixelsAdded = 0;
static float savedScrollY = 0;
static float shiftedScrollY = 0;
if (mTopMargin != oldTopMargin) {
static float savedScrollY = 0;
if (oldTopMargin == 0)
savedScrollY = ImGui::GetScrollY();
auto window = ImGui::GetCurrentWindow();
@ -1124,6 +1101,7 @@ void TextEditor::Render() {
if (maxScroll > 0) {
float lineCount;
float pixelCount;
static float linesAdded = 0;
if (mTopMargin > oldTopMargin) {
pixelCount = mTopMargin - oldTopMargin;
lineCount = pixelCount / mCharAdvance.y;
@ -1148,6 +1126,7 @@ void TextEditor::Render() {
else
mLines.erase(mLines.begin() + mLines.size() - 1);
}
static float pixelsAdded = 0;
if (mTopMargin > oldTopMargin) {
linesAdded += lineCount;
pixelsAdded += pixelCount;
@ -1159,6 +1138,7 @@ void TextEditor::Render() {
pixelsAdded = 0;
}
if (oldScrollY + pixelCount < maxScroll) {
static float shiftedScrollY = 0;
if (mTopMargin > oldTopMargin)
shiftedScrollY = oldScrollY + pixelCount;
else if (mTopMargin > 0)
@ -1211,15 +1191,15 @@ void TextEditor::Render(const char *aTitle, const ImVec2 &aSize, bool aBorder) {
}
void TextEditor::SetText(const std::string &aText) {
mLines.clear();
mLines.emplace_back(Line());
mLines.resize(1);
mLines[0].clear();
for (auto chr : aText) {
if (chr == '\r') {
// ignore the carriage return character
} else if (chr == '\n')
mLines.emplace_back(Line());
mLines.push_back(Line());
else {
mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default));
mLines.back().push_back(Glyph(chr, PaletteIndex::Default));
}
}
@ -1235,7 +1215,7 @@ void TextEditor::SetText(const std::string &aText) {
void TextEditor::SetTextLines(const std::vector<std::string> &aLines) {
mLines.clear();
if (aLines.empty()) {
if (isEmpty()) {
mLines.emplace_back(Line());
} else {
mLines.resize(aLines.size());
@ -1281,7 +1261,7 @@ void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) {
if (end.mColumn == 0 && end.mLine > 0)
--end.mLine;
if (end.mLine >= (int)mLines.size())
end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1;
end.mLine = isEmpty() ? 0 : (int)mLines.size() - 1;
end.mColumn = GetLineMaxColumn(end.mLine);
u.mRemovedStart = start;
@ -1350,6 +1330,10 @@ void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) {
auto coord = GetActualCursorCoordinates();
u.mAddedStart = coord;
if (mLines.empty())
mLines.push_back(Line());
if (aChar == '\n') {
InsertLine(coord.mLine + 1);
auto &line = mLines[coord.mLine];
@ -1613,7 +1597,7 @@ void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) {
auto oldPos = mState.mCursorPosition;
ResetCursorBlinkTime();
if (mLines.empty() || oldPos.mLine >= mLines.size())
if (isEmpty() || oldPos.mLine >= mLines.size())
return;
mState.mCursorPosition = GetActualCursorCoordinates();
@ -1670,7 +1654,7 @@ void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) {
ResetCursorBlinkTime();
auto oldPos = mState.mCursorPosition;
if (mLines.empty() || oldPos.mLine >= mLines.size())
if (isEmpty() || oldPos.mLine >= mLines.size())
return;
mState.mCursorPosition = GetActualCursorCoordinates();
@ -1796,7 +1780,7 @@ void TextEditor::Delete() {
ResetCursorBlinkTime();
assert(!mReadOnly);
if (mLines.empty())
if (isEmpty())
return;
UndoRecord u;
@ -1853,7 +1837,7 @@ void TextEditor::Backspace() {
ResetCursorBlinkTime();
assert(!mReadOnly);
if (mLines.empty())
if (isEmpty())
return;
UndoRecord u;
@ -1932,14 +1916,14 @@ void TextEditor::SelectAll() {
}
bool TextEditor::HasSelection() const {
return mState.mSelectionEnd > mState.mSelectionStart;
return !isEmpty() && mState.mSelectionEnd > mState.mSelectionStart;
}
void TextEditor::Copy() {
if (HasSelection()) {
ImGui::SetClipboardText(GetSelectedText().c_str());
} else {
if (!mLines.empty()) {
if (!isEmpty()) {
std::string str;
auto &line = mLines[GetActualCursorCoordinates().mLine];
for (auto &g : line)
@ -2165,13 +2149,13 @@ std::string make_wholeWord(const std::string &s) {
}
// Performs actual search to fill mMatches
bool TextEditor::FindReplaceHandler::FindNext(TextEditor *editor, bool wrapAround) {
bool TextEditor::FindReplaceHandler::FindNext(TextEditor *editor) {
Coordinates curPos;
curPos.mLine = mMatches.empty() ? editor->mState.mCursorPosition.mLine : mMatches.back().mCursorPosition.mLine;
curPos.mColumn = mMatches.empty() ? editor->mState.mCursorPosition.mColumn : editor->Utf8CharsToBytes(
mMatches.back().mCursorPosition);
unsigned long selectionLength = editor->GetStringCharacterCount(mFindWord);
unsigned long matchLength = editor->GetStringCharacterCount(mFindWord);
size_t byteIndex = 0;
for (size_t ln = 0; ln < curPos.mLine; ln++)
@ -2218,67 +2202,32 @@ bool TextEditor::FindReplaceHandler::FindNext(TextEditor *editor, bool wrapAroun
if(firstLoc > byteIndex) {
pos = firstLoc;
selectionLength = firstLength;
matchLength = firstLength;
} else {
while (iter != end) {
iter++;
if (((pos = iter->position()) > byteIndex) && ((selectionLength = iter->length()) > 0))
if (((pos = iter->position()) > byteIndex) && ((matchLength = iter->length()) > 0))
break;
}
}
if (iter == end && !wrapAround)
if (iter == end)
return false;
textLoc = pos;
if (wrapAround) {
if (iter == end)
selectionLength = firstLength;
}
} else {
// non regex search
textLoc = textSrc.find(wordLower, byteIndex);
if (textLoc == std::string::npos) {
if (wrapAround)
textLoc = textSrc.find(wordLower, 0);
else
return false;
}
if (textLoc == std::string::npos)
return false;
}
if (textLoc != std::string::npos) {
curPos.mLine = curPos.mColumn = 0;
byteIndex = 0;
for (size_t ln = 0; ln < editor->mLines.size(); ln++) {
auto byteCount = editor->GetLineByteCount(ln) + 1;
if (byteIndex + byteCount > textLoc) {
curPos.mLine = ln;
curPos.mColumn = textLoc - byteIndex;
auto &line = editor->mLines[curPos.mLine];
int lineSize = line.size();
for (int i = 0; i < std::min(lineSize,curPos.mColumn); i++) {
if (line[i].mChar == '\t')
curPos.mColumn += (editor->mTabSize - 1);
}
break;
} else {// just keep adding
byteIndex += byteCount;
}
}
} else
if (textLoc == std::string::npos)
return false;
Coordinates selStart, selEnd;
selStart.mLine = curPos.mLine;
selStart.mColumn = editor->Utf8BytesToChars(curPos);
selEnd = selStart;
selEnd.mColumn += selectionLength;
TextEditor::EditorState state;
state.mSelectionStart = selStart;
state.mSelectionEnd = selEnd;
state.mCursorPosition = selEnd;
state.mSelectionStart = editor->StringIndexToCoordinates(textLoc,textSrc);
state.mSelectionEnd = editor->StringIndexToCoordinates(textLoc + matchLength,textSrc);
state.mCursorPosition = state.mSelectionEnd;
mMatches.push_back(state);
return true;
}
@ -2305,7 +2254,7 @@ void TextEditor::FindReplaceHandler::FindAllMatches(TextEditor *editor,std::stri
Coordinates begin = Coordinates(0,0);
editor->mState.mCursorPosition = begin;
if (!FindNext(editor,false)) {
if (!FindNext(editor)) {
editor->mState = saveState;
editor->EnsureCursorVisible();
return;
@ -2313,7 +2262,7 @@ void TextEditor::FindReplaceHandler::FindAllMatches(TextEditor *editor,std::stri
TextEditor::EditorState state = mMatches.back();
while( state.mCursorPosition < startingPos) {
if (!FindNext(editor,false)) {
if (!FindNext(editor)) {
editor->mState = saveState;
editor->EnsureCursorVisible();
return;
@ -2321,7 +2270,7 @@ void TextEditor::FindReplaceHandler::FindAllMatches(TextEditor *editor,std::stri
state = mMatches.back();
}
while (FindNext(editor,false));
while (FindNext(editor));
editor->mState = saveState;
editor->EnsureCursorVisible();
@ -2538,7 +2487,7 @@ void TextEditor::Colorize(int aFromLine, int aLines) {
}
void TextEditor::ColorizeRange(int aFromLine, int aToLine) {
if (mLines.empty() || aFromLine >= aToLine)
if (isEmpty() || aFromLine >= aToLine)
return;
std::string buffer;
@ -2628,7 +2577,7 @@ void TextEditor::ColorizeRange(int aFromLine, int aToLine) {
}
void TextEditor::ColorizeInternal() {
if (mLines.empty() || !mColorizerEnabled)
if (isEmpty() || !mColorizerEnabled)
return;
if (mCheckComments) {