From ccb22f34aacaa2676615eedf97c8b341ab484331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 25 Dec 2020 17:45:41 -0300 Subject: [PATCH] TextDocument find improvements: lua pattern find and whole word filter. --- include/eepp/core/string.hpp | 10 ++ include/eepp/ui/doc/textdocument.hpp | 24 ++++- include/eepp/ui/doc/textrange.hpp | 3 + src/eepp/core/string.cpp | 17 ++++ src/eepp/ui/doc/textdocument.cpp | 125 +++++++++++++++++++------ src/tools/codeeditor/codeeditor.cpp | 70 ++++++++------ src/tools/codeeditor/codeeditor.hpp | 1 + src/tools/codeeditor/lintermodule.cpp | 10 +- src/tools/codeeditor/projectsearch.cpp | 11 +-- 9 files changed, 195 insertions(+), 76 deletions(-) diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index 2a0c8e98f..aae0245de 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -88,9 +88,19 @@ class EE_API String { /** @return If the value passed is a letter */ static bool isLetter( const int& value ); + /** @return If the value passed is a letter or a number */ + static bool isAlphaNum( const int& value ); + /** @return If the string is a representation of a hexa number */ static bool isHexNotation( const std::string& value, const std::string& withPrefix = "" ); + /** @return If the needle substring, found starting at startPos is a whole-word. */ + static bool isWholeWord( const std::string& haystack, const std::string& needle, + const Int64& startPos ); + + /** @return If the needle substring, found starting at startPos is a whole-word. */ + static bool isWholeWord( const String& haystack, const String& needle, const Int64& startPos ); + /** Split a String and hold it on a vector */ static std::vector split( const String& str, const StringBaseType& delim = '\n', const bool& pushEmptyString = false ); diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index 5c8651e09..87d6f796b 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -31,6 +31,8 @@ class EE_API TextDocument { enum class LineEnding { LF, CRLF }; + enum class FindReplaceType { Normal, LuaPattern }; + class EE_API Client { public: virtual ~Client(); @@ -247,17 +249,31 @@ class EE_API TextDocument { void setCommand( const std::string& command, DocumentCommand func ); - TextPosition find( String text, TextPosition from = { 0, 0 }, const bool& caseSensitive = true, - TextRange restrictRange = TextRange() ); + TextRange find( String text, TextPosition from = { 0, 0 }, const bool& caseSensitive = true, + const bool& wholeWord = false, + const FindReplaceType& type = FindReplaceType::Normal, + TextRange restrictRange = TextRange() ); TextPosition findLast( String text, TextPosition from = { 0, 0 }, - const bool& caseSensitive = true, + const bool& caseSensitive = true, const bool& wholeWord = false, TextRange restrictRange = TextRange() ); + std::vector findAll( const String& text, const bool& caseSensitive = true, + const bool& wholeWord = false, + const FindReplaceType& type = FindReplaceType::Normal, + TextRange restrictRange = TextRange() ); + + int replaceAll( const String& text, const String& replace, const bool& caseSensitive = true, + const bool& wholeWord = false, + const FindReplaceType& type = FindReplaceType::Normal, + TextRange restrictRange = TextRange() ); + TextPosition replaceSelection( const String& replace ); TextPosition replace( String search, const String& replace, TextPosition from = { 0, 0 }, - const bool& caseSensitive = true, TextRange restrictRange = TextRange() ); + const bool& caseSensitive = true, const bool& wholeWord = false, + const FindReplaceType& type = FindReplaceType::Normal, + TextRange restrictRange = TextRange() ); String getIndentString(); diff --git a/include/eepp/ui/doc/textrange.hpp b/include/eepp/ui/doc/textrange.hpp index 72fe969b7..cc27d62a3 100644 --- a/include/eepp/ui/doc/textrange.hpp +++ b/include/eepp/ui/doc/textrange.hpp @@ -12,6 +12,7 @@ class EE_API TextRange { mStart( start ), mEnd( end ) {} bool isValid() const { return mStart.isValid() && mEnd.isValid(); } + void clear() { mStart = {}; mEnd = {}; @@ -27,6 +28,8 @@ class EE_API TextRange { TextRange normalized() const { return TextRange( normalizedStart(), normalizedEnd() ); } + TextRange reversed() { return TextRange( mEnd, mStart ); } + void setStart( const TextPosition& position ) { mStart = position; } void setEnd( const TextPosition& position ) { mEnd = position; } diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index c999af863..f064d2cc9 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -145,6 +145,10 @@ bool String::isLetter( const int& value ) { ( value != 215 ) && ( value != 247 ) ); } +bool String::isAlphaNum( const int& value ) { + return isLetter( value ) || isNumber( value ); +} + bool String::isHexNotation( const std::string& value, const std::string& withPrefix ) { if ( !withPrefix.empty() && !String::startsWith( value, withPrefix ) ) { return false; @@ -1165,4 +1169,17 @@ String operator+( const String& left, const String& right ) { return string; } +bool String::isWholeWord( const std::string& haystack, const std::string& needle, + const Int64& startPos ) { + return ( 0 == startPos || !( std::isalnum( haystack[startPos - 1] ) ) ) && + ( startPos + needle.size() >= haystack.size() || + !( std::isalnum( haystack[startPos + needle.size()] ) ) ); +} + +bool String::isWholeWord( const String& haystack, const String& needle, const Int64& startPos ) { + return ( 0 == startPos || !( isAlphaNum( haystack[startPos - 1] ) ) ) && + ( startPos + needle.size() >= haystack.size() || + !( isAlphaNum( haystack[startPos + needle.size()] ) ) ); +} + } // namespace EE diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 395046971..7418bdae5 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1235,10 +1235,31 @@ void TextDocument::setCommand( const std::string& command, TextDocument::Documen mCommands[command] = func; } -TextPosition TextDocument::find( String text, TextPosition from, const bool& caseSensitive, - TextRange restrictRange ) { +static std::pair findType( const String& str, const String& findStr, + const TextDocument::FindReplaceType& type ) { + switch ( type ) { + case TextDocument::FindReplaceType::LuaPattern: { + LuaPattern words( findStr ); + int start, end = 0; + words.find( str, start, end ); + if ( start < 0 ) + return { String::StringType::npos, String::StringType::npos }; + else + return { start, end }; + } + case TextDocument::FindReplaceType::Normal: + default: { + size_t res = str.find( findStr ); + return { res, String::InvalidPos == res ? res : res + findStr.size() }; + } + } +} + +TextRange TextDocument::find( String text, TextPosition from, const bool& caseSensitive, + const bool& wholeWord, const FindReplaceType& type, + TextRange restrictRange ) { if ( text.empty() ) - return TextPosition(); + return TextRange(); from = sanitizePosition( from ); TextPosition to = endOfDoc(); @@ -1246,38 +1267,42 @@ TextPosition TextDocument::find( String text, TextPosition from, const bool& cas restrictRange = sanitizeRange( restrictRange.normalized() ); to = restrictRange.end(); if ( from < restrictRange.start() || from > restrictRange.end() ) - return TextPosition(); + return TextRange(); } if ( !caseSensitive ) text.toLower(); for ( Int64 i = from.line(); i <= to.line(); i++ ) { - size_t col; + std::pair col; if ( i == from.line() ) { col = caseSensitive - ? line( i ).getText().substr( from.column() ).find( text ) - : String::toLower( line( i ).getText() ).substr( from.column() ).find( text ); - if ( String::StringType::npos != col ) - col += from.column(); + ? findType( line( i ).getText().substr( from.column() ), text, type ) + : findType( String::toLower( line( i ).getText() ).substr( from.column() ), + text, type ); + if ( String::StringType::npos != col.first ) { + col.first += from.column(); + col.second += from.column(); + } } else if ( i == to.line() && to != endOfDoc() ) { - col = - caseSensitive - ? line( i ).getText().substr( 0, to.column() ).find( text ) - : String::toLower( line( i ).getText() ).substr( 0, to.column() ).find( text ); + col = caseSensitive + ? findType( line( i ).getText().substr( 0, to.column() ), text, type ) + : findType( String::toLower( line( i ).getText() ).substr( 0, to.column() ), + text, type ); } else { - col = caseSensitive ? line( i ).getText().find( text ) - : String::toLower( line( i ).getText() ).find( text ); + col = caseSensitive ? findType( line( i ).getText(), text, type ) + : findType( String::toLower( line( i ).getText() ), text, type ); } - if ( String::StringType::npos != col ) { - return { (Int64)i, (Int64)col }; + if ( String::StringType::npos != col.first && + ( !wholeWord || String::isWholeWord( line( i ).getText(), text, col.first ) ) ) { + return { { (Int64)i, (Int64)col.first }, { (Int64)i, (Int64)col.second } }; } } - return TextPosition(); + return TextRange(); } TextPosition TextDocument::findLast( String text, TextPosition from, const bool& caseSensitive, - TextRange restrictRange ) { + const bool& wholeWord, TextRange restrictRange ) { if ( text.empty() ) return TextPosition(); from = sanitizePosition( from ); @@ -1309,13 +1334,55 @@ TextPosition TextDocument::findLast( String text, TextPosition from, const bool& col = caseSensitive ? line( i ).getText().rfind( text ) : String::toLower( line( i ).getText() ).rfind( text ); } - if ( String::StringType::npos != col ) { + if ( String::StringType::npos != col && + ( !wholeWord || String::isWholeWord( line( i ).getText(), text, col ) ) ) { return { (Int64)i, (Int64)col }; } } return TextPosition(); } +std::vector TextDocument::findAll( const String& text, const bool& caseSensitive, + const bool& wholeWord, const FindReplaceType& type, + TextRange restrictRange ) { + std::vector all; + TextRange found; + TextPosition from = startOfDoc(); + if ( restrictRange.isValid() ) + from = restrictRange.normalized().start(); + do { + found = find( text, from, caseSensitive, wholeWord, type, restrictRange ); + if ( found.isValid() ) { + from = found.end(); + all.push_back( found ); + } + } while ( found.isValid() ); + return all; +} + +int TextDocument::replaceAll( const String& text, const String& replace, const bool& caseSensitive, + const bool& wholeWord, const FindReplaceType& type, + TextRange restrictRange ) { + if ( text.empty() ) + return 0; + int count = 0; + TextRange found; + TextPosition startedPosition = getSelection().start(); + TextPosition from = startOfDoc(); + if ( restrictRange.isValid() ) + from = restrictRange.normalized().start(); + do { + found = find( text, from, caseSensitive, wholeWord, type, restrictRange ); + if ( found.isValid() ) { + setSelection( found ); + from = replaceSelection( replace ); + count++; + } + } while ( found.isValid() ); + setSelection( startedPosition ); + return count; +} + TextPosition TextDocument::replaceSelection( const String& replace ) { if ( hasSelection() ) { deleteTo( 0 ); @@ -1325,16 +1392,14 @@ TextPosition TextDocument::replaceSelection( const String& replace ) { } TextPosition TextDocument::replace( String search, const String& replace, TextPosition from, - const bool& caseSensitive, TextRange restrictRange ) { - TextPosition start( find( search, from, caseSensitive, restrictRange ) ); - if ( start.isValid() ) { - TextPosition end = positionOffset( start, search.size() ); - if ( end.isValid() ) { - setSelection( { start, end } ); - deleteTo( 0 ); - textInput( replace ); - return end; - } + const bool& caseSensitive, const bool& wholeWord, + const FindReplaceType& type, TextRange restrictRange ) { + TextRange found( find( search, from, caseSensitive, wholeWord, type, restrictRange ) ); + if ( found.isValid() ) { + setSelection( found ); + deleteTo( 0 ); + textInput( replace ); + return found.end(); } return TextPosition(); } diff --git a/src/tools/codeeditor/codeeditor.cpp b/src/tools/codeeditor/codeeditor.cpp index e2aba27f9..2dc098b57 100644 --- a/src/tools/codeeditor/codeeditor.cpp +++ b/src/tools/codeeditor/codeeditor.cpp @@ -271,7 +271,8 @@ bool App::findPrevText( SearchState& search ) { from = from < range.start() ? range.start() : from; } - TextPosition found = doc.findLast( search.text, from, search.caseSensitive, search.range ); + TextPosition found = + doc.findLast( search.text, from, search.caseSensitive, search.wholeWord, search.range ); if ( found.isValid() ) { doc.setSelection( { doc.positionOffset( found, search.text.size() ), found } ); return true; @@ -301,14 +302,16 @@ bool App::findNextText( SearchState& search ) { from = from < range.start() ? range.start() : from; } - TextPosition found = doc.find( search.text, from, search.caseSensitive, range ); + TextRange found = + doc.find( search.text, from, search.caseSensitive, search.wholeWord, search.type, range ); if ( found.isValid() ) { - doc.setSelection( { doc.positionOffset( found, search.text.size() ), found } ); + doc.setSelection( found.reversed() ); return true; } else { - found = doc.find( search.text, range.start(), search.caseSensitive, range ); + found = doc.find( search.text, range.start(), search.caseSensitive, search.wholeWord, + search.type, range ); if ( found.isValid() ) { - doc.setSelection( { doc.positionOffset( found, search.text.size() ), found } ); + doc.setSelection( found.reversed() ); return true; } } @@ -331,24 +334,12 @@ int App::replaceAll( SearchState& search, const String& replace ) { search.text = mLastSearch; if ( search.text.empty() ) return 0; - - int count = 0; search.editor->getDocument().setActiveClient( search.editor ); mLastSearch = search.text; TextDocument& doc = search.editor->getDocument(); - TextPosition found; TextPosition startedPosition = doc.getSelection().start(); - TextPosition from = doc.startOfDoc(); - if ( search.range.isValid() ) - from = search.range.normalized().start(); - do { - found = doc.find( search.text, from, search.caseSensitive, search.range ); - if ( found.isValid() ) { - doc.setSelection( { doc.positionOffset( found, search.text.size() ), found } ); - from = doc.replaceSelection( replace ); - count++; - } - } while ( found.isValid() ); + int count = doc.replaceAll( search.text, replace, search.caseSensitive, search.wholeWord, + search.type, search.range ); doc.setSelection( startedPosition ); return count; } @@ -583,6 +574,22 @@ void App::initSearchBar() { UITextInput* replaceInput = mSearchBarLayout->find( "search_replace" ); UICheckBox* caseSensitiveChk = mSearchBarLayout->find( "case_sensitive" ); UICheckBox* wholeWordChk = mSearchBarLayout->find( "whole_word" ); + UICheckBox* luaPatternChk = mSearchBarLayout->find( "lua_pattern" ); + + caseSensitiveChk->addEventListener( + Event::OnValueChange, [&, caseSensitiveChk]( const Event* ) { + mSearchState.caseSensitive = caseSensitiveChk->isChecked(); + } ); + + wholeWordChk->addEventListener( Event::OnValueChange, [&, wholeWordChk]( const Event* ) { + mSearchState.wholeWord = wholeWordChk->isChecked(); + } ); + + luaPatternChk->addEventListener( Event::OnValueChange, [&, luaPatternChk]( const Event* ) { + mSearchState.type = luaPatternChk->isChecked() ? TextDocument::FindReplaceType::LuaPattern + : TextDocument::FindReplaceType::Normal; + } ); + findInput->addEventListener( Event::OnTextChanged, [&, findInput]( const Event* ) { if ( mSearchState.editor && mEditorSplitter->editorExists( mSearchState.editor ) ) { mSearchState.text = findInput->getText(); @@ -626,20 +633,20 @@ void App::initSearchBar() { } ); mSearchBarLayout->addCommand( "change-case", [&, caseSensitiveChk] { caseSensitiveChk->setChecked( !caseSensitiveChk->isChecked() ); - mSearchState.caseSensitive = caseSensitiveChk->isChecked(); } ); mSearchBarLayout->addCommand( "change-whole-word", [&, wholeWordChk] { wholeWordChk->setChecked( !wholeWordChk->isChecked() ); - mSearchState.wholeWord = wholeWordChk->isChecked(); } ); - mSearchBarLayout->getKeyBindings().addKeybindsString( { - { "f3", "repeat-find" }, - { "ctrl+g", "repeat-find" }, - { "escape", "close-searchbar" }, - { "ctrl+r", "replace-all" }, - { "ctrl+s", "change-case" }, - { "ctrl+w", "change-whole-word" }, + mSearchBarLayout->addCommand( "toggle-lua-pattern", [&, luaPatternChk] { + luaPatternChk->setChecked( !luaPatternChk->isChecked() ); } ); + mSearchBarLayout->getKeyBindings().addKeybindsString( { { "f3", "repeat-find" }, + { "ctrl+g", "repeat-find" }, + { "escape", "close-searchbar" }, + { "ctrl+r", "replace-all" }, + { "ctrl+s", "change-case" }, + { "ctrl+w", "change-whole-word" }, + { "ctrl+l", "toggle-lua-pattern" } } ); addReturnListener( findInput, "repeat-find" ); addReturnListener( replaceInput, "find-and-replace" ); addClickListener( mSearchBarLayout->find( "find_prev" ), "find-prev" ); @@ -2451,12 +2458,15 @@ void App::init( const std::string& file, const Float& pidelDensity ) { + + + + + - - diff --git a/src/tools/codeeditor/codeeditor.hpp b/src/tools/codeeditor/codeeditor.hpp index f66ad3bb8..2ed4284e3 100644 --- a/src/tools/codeeditor/codeeditor.hpp +++ b/src/tools/codeeditor/codeeditor.hpp @@ -87,6 +87,7 @@ struct SearchState { TextRange range = TextRange(); bool caseSensitive{ false }; bool wholeWord{ false }; + TextDocument::FindReplaceType type{ TextDocument::FindReplaceType::Normal }; void reset() { editor = nullptr; range = TextRange(); diff --git a/src/tools/codeeditor/lintermodule.cpp b/src/tools/codeeditor/lintermodule.cpp index 27fdbad67..7539ae2be 100644 --- a/src/tools/codeeditor/lintermodule.cpp +++ b/src/tools/codeeditor/lintermodule.cpp @@ -28,7 +28,7 @@ LinterModule::LinterModule( const std::string& lintersPath, std::shared_ptrremoveEventListener( listener ); editor.first->unregisterModule( this ); @@ -294,8 +294,12 @@ void LinterModule::drawAfterLineText( UICodeEditor* editor, const Int64& index, const String& text = doc->line( index ).getText(); size_t minCol = text.find_first_not_of( " \t\f\v\n\r", match.pos.column() ); if ( minCol == String::InvalidPos ) - match.pos.column(); + minCol = match.pos.column(); minCol = std::max( (Int64)minCol, match.pos.column() ); + if ( minCol >= text.size() ) + minCol = match.pos.column(); + if ( minCol >= text.size() ) + minCol = text.size() - 1; std::string str( text.substr( minCol ).size() - 1, '~' ); String string( str ); @@ -312,7 +316,7 @@ bool LinterModule::onMouseMove( UICodeEditor* editor, const Vector2i& pos, const auto it = mMatches.find( editor->getDocumentRef().get() ); if ( it != mMatches.end() ) { Vector2f localPos( editor->convertToNodeSpace( pos.asFloat() ) ); - for ( auto matchIt : it->second ) { + for ( const auto& matchIt : it->second ) { auto& match = matchIt.second; if ( match.box.contains( localPos ) ) { editor->setTooltipText( match.text ); diff --git a/src/tools/codeeditor/projectsearch.cpp b/src/tools/codeeditor/projectsearch.cpp index 6a1af0e9f..cdefc677b 100644 --- a/src/tools/codeeditor/projectsearch.cpp +++ b/src/tools/codeeditor/projectsearch.cpp @@ -33,13 +33,6 @@ static String textLine( const std::string& fileText, const size_t& fromPos, size return fileText.substr( start, end - start ); } -static bool isWholeWord( const std::string& fileText, const std::string& text, - const Int64& searchRes ) { - return ( 0 == searchRes || !( std::isalnum( fileText[searchRes - 1] ) ) ) && - ( searchRes + text.size() >= fileText.size() || - !( std::isalnum( fileText[searchRes + text.size()] ) ) ); -} - static std::vector searchInFileHorspool( const std::string& file, const std::string& text, const bool& caseSensitive, const bool& wholeWord, const String::BMH::OccTable& occ ) { @@ -55,7 +48,7 @@ searchInFileHorspool( const std::string& file, const std::string& text, const bo do { searchRes = String::BMH::find( fileText, text, searchRes, occ ); if ( searchRes != -1 ) { - if ( wholeWord && !isWholeWord( fileText, text, searchRes ) ) { + if ( wholeWord && !String::isWholeWord( fileText, text, searchRes ) ) { lSearchRes = searchRes; searchRes += text.size(); continue; @@ -75,7 +68,7 @@ searchInFileHorspool( const std::string& file, const std::string& text, const bo do { searchRes = String::BMH::find( fileText, text, searchRes, occ ); if ( searchRes != -1 ) { - if ( wholeWord && !isWholeWord( fileText, text, searchRes ) ) { + if ( wholeWord && !String::isWholeWord( fileText, text, searchRes ) ) { lSearchRes = searchRes; searchRes += text.size(); continue;