From 6d2761f141ba8372eba03539ac2fca759d214152 Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:37:05 -0700 Subject: [PATCH] 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(). --- .../ColorTextEditor/include/TextEditor.h | 8 +- .../ColorTextEditor/source/TextEditor.cpp | 213 +++++++----------- 2 files changed, 87 insertions(+), 134 deletions(-) diff --git a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h index 4439730c5..c24c8ff27 100644 --- a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h +++ b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h @@ -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& aLines); std::vector GetTextLines() const; @@ -314,7 +318,7 @@ public: FindReplaceHandler(); typedef std::vector 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; diff --git a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp index 759dd73c3..c17a93844 100644 --- a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp +++ b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp @@ -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 &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) {