diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index 091f49fbc..ce0f3b879 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -398,6 +398,18 @@ class EE_API String { /** @return The next character in a utf8 null terminated string */ static Uint32 utf8Next( char*& utf8String ); + /** glob matches a string against a glob + ** @return True if matches + */ + static bool globMatch( const std::string& text, const std::string_view& glob, + bool caseInsensitive = false ); + + /** glob matches a string against a set of globs + ** @return True if matches + */ + static bool globMatch( const std::string& text, const std::vector& globs, + bool caseInsensitive = false ); + /** @brief Default constructor ** This constructor creates an empty string. **/ diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index 8a5daddf3..edfd7c305 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -14,6 +14,152 @@ namespace EE { const std::size_t String::InvalidPos = StringType::npos; +// Author: Robert A. van Engelen, engelen@genivia.com +// Date: August 5, 2019 +// License: The Code Project Open License (CPOL) +// https://www.codeproject.com/info/cpol10.aspx + +#define DOTGLOB 1 +#define PATHSEP '/' +#define CASE( c, caseInsensitive ) ( caseInsensitive ? std::tolower( c ) : ( c ) ) + +bool String::globMatch( const std::string& text, const std::string_view& glob, + bool caseInsensitive ) { + size_t i = 0; + size_t j = 0; + size_t n = text.size(); + size_t m = glob.size(); + size_t text1_backup = std::string::npos; + size_t glob1_backup = std::string::npos; + size_t text2_backup = std::string::npos; + size_t glob2_backup = std::string::npos; + bool nodot = !DOTGLOB; + // match pathname if glob contains a / otherwise match the basename + if ( j + 1 < m && glob[j] == '/' ) { + // if pathname starts with ./ then ignore these pairs + while ( i + 1 < n && text[i] == '.' && text[i + 1] == PATHSEP ) + i += 2; + // if pathname starts with a / then ignore it + if ( i < n && text[i] == PATHSEP ) + i++; + j++; + } else if ( glob.find( '/' ) == std::string::npos ) { + size_t sep = text.rfind( PATHSEP ); + if ( sep != std::string::npos ) + i = sep + 1; + } + while ( i < n ) { + if ( j < m ) { + switch ( glob[j] ) { + case '*': + // match anything except . after / + if ( nodot && text[i] == '.' ) + break; + if ( ++j < m && glob[j] == '*' ) { + // trailing ** match everything after / + if ( ++j >= m ) + return true; + // ** followed by a / match zero or more directories + if ( glob[j] != '/' ) + return false; + // new **-loop, discard *-loop + text1_backup = std::string::npos; + glob1_backup = std::string::npos; + text2_backup = i; + glob2_backup = ++j; + continue; + } + // trailing * matches everything except / + text1_backup = i; + glob1_backup = j; + continue; + case '?': + // match anything except . after / + if ( nodot && text[i] == '.' ) + break; + // match any character except / + if ( text[i] == PATHSEP ) + break; + i++; + j++; + continue; + case '[': { + // match anything except . after / + if ( nodot && text[i] == '.' ) + break; + // match any character in [...] except / + if ( text[i] == PATHSEP ) + break; + int lastchr; + bool matched = false; + bool reverse = j + 1 < m && ( glob[j + 1] == '^' || glob[j + 1] == '!' ); + // inverted character class + if ( reverse ) + j++; + // match character class + for ( lastchr = 256; ++j < m && glob[j] != ']'; + lastchr = CASE( glob[j], caseInsensitive ) ) + if ( lastchr < 256 && glob[j] == '-' && j + 1 < m && glob[j + 1] != ']' + ? CASE( text[i], caseInsensitive ) <= + CASE( glob[++j], caseInsensitive ) && + CASE( text[i], caseInsensitive ) >= lastchr + : CASE( text[i], caseInsensitive ) == + CASE( glob[j], caseInsensitive ) ) + matched = true; + if ( matched == reverse ) + break; + i++; + if ( j < m ) + j++; + continue; + } + case '\\': + // literal match \-escaped character + if ( j + 1 < m ) + j++; + // FALLTHROUGH + default: + // match the current non-NUL character + if ( CASE( glob[j], caseInsensitive ) != CASE( text[i], caseInsensitive ) && + !( glob[j] == '/' && text[i] == PATHSEP ) ) + break; + // do not match a . with *, ? [] after / + nodot = !DOTGLOB && glob[j] == '/'; + i++; + j++; + continue; + } + } + if ( glob1_backup != std::string::npos && text[text1_backup] != PATHSEP ) { + // *-loop: backtrack to the last * but do not jump over / + i = ++text1_backup; + j = glob1_backup; + continue; + } + if ( glob2_backup != std::string::npos ) { + // **-loop: backtrack to the last ** + i = ++text2_backup; + j = glob2_backup; + continue; + } + return false; + } + // ignore trailing stars + while ( j < m && glob[j] == '*' ) + j++; + // at end of text means success if nothing else is left to match + return j >= m; +} + +bool String::globMatch( const std::string& text, const std::vector& globs, + bool caseInsensitive ) { + for ( const auto& glob : globs ) { + if ( globMatch( text, glob, caseInsensitive ) ) + return true; + } + return false; +} + /* * Originally written by Joel Yliluoma * http://en.wikipedia.org/wiki/Talk:Boyer%E2%80%93Moore_string_search_algorithm#Common_functions @@ -919,7 +1065,7 @@ void String::numberCleanInPlace( std::string& strNumber ) { std::string String::fromFloat( const Float& value, const std::string& append, const std::string& prepend, size_t digitsAfterComma ) { std::ostringstream ss; - ss.precision(digitsAfterComma); + ss.precision( digitsAfterComma ); ss << std::fixed << value; std::string val( ss.str() ); numberCleanInPlace( val ); @@ -929,7 +1075,7 @@ std::string String::fromFloat( const Float& value, const std::string& append, std::string String::fromDouble( const double& value, const std::string& append, const std::string& prepend, size_t digitsAfterComma ) { std::ostringstream ss; - ss.precision(digitsAfterComma); + ss.precision( digitsAfterComma ); ss << std::fixed << value; std::string val( ss.str() ); numberCleanInPlace( val ); diff --git a/src/tools/ecode/applayout.xml.hpp b/src/tools/ecode/applayout.xml.hpp index 3b9971dfb..3e66626ac 100644 --- a/src/tools/ecode/applayout.xml.hpp +++ b/src/tools/ecode/applayout.xml.hpp @@ -498,15 +498,24 @@ R"html( - + + + + - - - - + + + + + + + + diff --git a/src/tools/ecode/globalsearchcontroller.cpp b/src/tools/ecode/globalsearchcontroller.cpp index c8c578835..c756093a7 100644 --- a/src/tools/ecode/globalsearchcontroller.cpp +++ b/src/tools/ecode/globalsearchcontroller.cpp @@ -1,5 +1,5 @@ -#include "ecode.hpp" #include "globalsearchcontroller.hpp" +#include "ecode.hpp" #include "uitreeviewglobalsearch.hpp" namespace ecode { @@ -120,7 +120,7 @@ void GlobalSearchController::initGlobalSearchBar( mGlobalSearchBarLayout = globalSearchBar; mGlobalSearchBarLayout->setVisible( false )->setEnabled( false ); auto addClickListener = [this]( UIWidget* widget, std::string cmd ) { - widget->addEventListener( Event::MouseClick, [this, cmd]( const Event* event ) { + widget->on( Event::MouseClick, [this, cmd]( const Event* event ) { const MouseEvent* mouseEvent = static_cast( event ); if ( mouseEvent->getFlags() & EE_BUTTON_LMASK ) mGlobalSearchBarLayout->execute( cmd ); @@ -165,33 +165,34 @@ void GlobalSearchController::initGlobalSearchBar( escapeSequenceChk->setChecked( globalSearchBarConfig.escapeSequence ); mGlobalSearchInput = mGlobalSearchBarLayout->find( "global_search_find" ); + mGlobalSearchWhereInput = mGlobalSearchBarLayout->find( "global_search_where" ); mGlobalSearchHistoryList = mGlobalSearchBarLayout->find( "global_search_history" ); mGlobalSearchBarLayout->setCommand( "global-search-clear-history", [this] { clearHistory(); } ); mGlobalSearchBarLayout->setCommand( "search-in-files", [this, caseSensitiveChk, wholeWordChk, luaPatternChk, escapeSequenceChk] { - doGlobalSearch( mGlobalSearchInput->getText(), caseSensitiveChk->isChecked(), - wholeWordChk->isChecked(), luaPatternChk->isChecked(), - escapeSequenceChk->isChecked(), false ); + doGlobalSearch( mGlobalSearchInput->getText(), mGlobalSearchWhereInput->getText(), + caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), + luaPatternChk->isChecked(), escapeSequenceChk->isChecked(), false ); } ); mGlobalSearchBarLayout->setCommand( "search-again", [this, caseSensitiveChk, wholeWordChk, luaPatternChk, escapeSequenceChk] { auto listBox = mGlobalSearchHistoryList->getListBox(); if ( listBox->getItemSelectedIndex() < mGlobalSearchHistory.size() ) { - doGlobalSearch( mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - - listBox->getItemSelectedIndex()] - .first, - caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), - luaPatternChk->isChecked(), escapeSequenceChk->isChecked(), + const auto& item = mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - + listBox->getItemSelectedIndex()]; + doGlobalSearch( item.search, item.filter, caseSensitiveChk->isChecked(), + wholeWordChk->isChecked(), luaPatternChk->isChecked(), + escapeSequenceChk->isChecked(), mGlobalSearchTreeReplace == mGlobalSearchTree, true ); } } ); mGlobalSearchBarLayout->setCommand( "search-set-string", [this] { auto listBox = mGlobalSearchHistoryList->getListBox(); - mGlobalSearchInput->setText( - mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - listBox->getItemSelectedIndex()] - .first ); + const auto& item = + mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - listBox->getItemSelectedIndex()]; + mGlobalSearchInput->setText( item.search ); ; } ); mGlobalSearchBarLayout->setCommand( "close-global-searchbar", [this] { @@ -220,15 +221,17 @@ void GlobalSearchController::initGlobalSearchBar( escapeSequenceChk->setChecked( !escapeSequenceChk->isChecked() ); } ); mGlobalSearchBarLayout->setCommand( "find-replace", [this] { mApp->showFindView(); } ); - mGlobalSearchInput->addEventListener( Event::OnPressEnter, [this]( const Event* ) { - if ( mGlobalSearchInput->hasFocus() ) { + const auto pressEnterCb = [this]( const Event* event ) { + if ( event->getNode()->hasFocus() ) { mGlobalSearchBarLayout->execute( "search-in-files" ); } else { KeyEvent keyEvent( mGlobalSearchTree, Event::KeyDown, KEY_RETURN, SCANCODE_UNKNOWN, 0, 0 ); mGlobalSearchTree->forceKeyDown( keyEvent ); } - } ); + }; + mGlobalSearchInput->on( Event::OnPressEnter, pressEnterCb ); + mGlobalSearchWhereInput->on( Event::OnPressEnter, pressEnterCb ); auto switchInputToTree = [this]( const Event* event ) { const KeyEvent* keyEvent = static_cast( event ); Uint32 keyCode = keyEvent->getKeyCode(); @@ -238,8 +241,8 @@ void GlobalSearchController::initGlobalSearchBar( mGlobalSearchTree->setFocus(); } }; - mGlobalSearchInput->addEventListener( Event::KeyDown, switchInputToTree ); - mGlobalSearchInput->addEventListener( Event::OnSizeChange, [this]( const Event* ) { + mGlobalSearchInput->on( Event::KeyDown, switchInputToTree ); + mGlobalSearchInput->on( Event::OnSizeChange, [this]( const Event* ) { if ( mGlobalSearchBarLayout->isVisible() ) updateGlobalSearchBar(); } ); @@ -292,12 +295,12 @@ void GlobalSearchController::initGlobalSearchBar( addClickListener( replaceButton, "replace-in-files" ); addClickListener( searchExpandButton, "expand-all" ); addClickListener( searchCollapseButton, "collapse-all" ); - replaceInput->addEventListener( Event::OnPressEnter, [this, replaceInput]( const Event* ) { + replaceInput->on( Event::OnPressEnter, [this, replaceInput]( const Event* ) { if ( replaceInput->hasFocus() ) mGlobalSearchBarLayout->execute( "replace-in-files" ); } ); - replaceInput->addEventListener( Event::KeyDown, switchInputToTree ); - mGlobalSearchLayout->addEventListener( Event::KeyDown, [this]( const Event* event ) { + replaceInput->on( Event::KeyDown, switchInputToTree ); + mGlobalSearchLayout->on( Event::KeyDown, [this]( const Event* event ) { const KeyEvent* keyEvent = static_cast( event ); if ( keyEvent->getKeyCode() == KEY_ESCAPE ) { mGlobalSearchBarLayout->execute( "close-global-searchbar" ); @@ -322,9 +325,9 @@ void GlobalSearchController::initGlobalSearchBar( mGlobalSearchHistory.back().second, true, false ); } else*/ { - doGlobalSearch( mGlobalSearchInput->getText(), caseSensitiveChk->isChecked(), - wholeWordChk->isChecked(), luaPatternChk->isChecked(), - escapeSequenceChk->isChecked(), true ); + doGlobalSearch( mGlobalSearchInput->getText(), mGlobalSearchWhereInput->getText(), + caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), + luaPatternChk->isChecked(), escapeSequenceChk->isChecked(), true ); } } ); mGlobalSearchBarLayout->setCommand( @@ -336,7 +339,7 @@ void GlobalSearchController::initGlobalSearchBar( String text( replaceInput->getText() ); if ( escapeSequenceChk->isChecked() ) text.unescape(); - size_t count = replaceInFiles( text.toUtf8(), replaceData.second ); + size_t count = replaceInFiles( text.toUtf8(), replaceData.result ); mGlobalSearchBarLayout->execute( "search-again" ); mGlobalSearchBarLayout->execute( "close-global-searchbar" ); mApp->getNotificationCenter()->addNotification( @@ -349,7 +352,7 @@ void GlobalSearchController::initGlobalSearchBar( UITreeViewGlobalSearch::New( mSplitter->getCurrentColorScheme(), true ); initGlobalSearchTree( mGlobalSearchTreeSearch ); initGlobalSearchTree( mGlobalSearchTreeReplace ); - mGlobalSearchTreeReplace->addEventListener( Event::KeyDown, [this]( const Event* event ) { + mGlobalSearchTreeReplace->on( Event::KeyDown, [this]( const Event* event ) { const KeyEvent* keyEvent = static_cast( event ); if ( keyEvent->getSanitizedMod() == KeyMod::getDefaultModifier() && keyEvent->getKeyCode() == KEY_RETURN ) @@ -416,6 +419,7 @@ void GlobalSearchController::clearHistory() { mGlobalSearchTree->setSearchStr( "" ); mGlobalSearchTree->setModel( nullptr ); mGlobalSearchInput->setText( "" ); + mGlobalSearchWhereInput->setText( "" ); mGlobalSearchLayout->findByClass( "status_box" )->setVisible( false ); mGlobalSearchLayout->findByClass( "search_str" )->setText( "" ); updateGlobalSearchBar(); @@ -494,18 +498,18 @@ void GlobalSearchController::updateGlobalSearchBarResults( void GlobalSearchController::updateGlobalSearchHistory( std::shared_ptr model, const std::string& search, - bool searchReplace, bool searchAgain, bool escapeSequence ) { + const std::string& filter, bool searchReplace, bool searchAgain, bool escapeSequence ) { auto listBox = mGlobalSearchHistoryList->getListBox(); if ( !searchAgain ) { - mGlobalSearchHistory.push_back( std::make_pair( search, model ) ); + mGlobalSearchHistory.push_back( { search, filter, model } ); if ( mGlobalSearchHistory.size() > 10 ) mGlobalSearchHistory.pop_front(); std::vector items; for ( auto item = mGlobalSearchHistory.rbegin(); item != mGlobalSearchHistory.rend(); item++ ) { - items.push_back( item->first ); + items.push_back( item->search ); } listBox->clear(); @@ -513,21 +517,35 @@ void GlobalSearchController::updateGlobalSearchHistory( if ( mGlobalSearchHistoryOnItemSelectedCb ) mGlobalSearchHistoryList->removeEventListener( mGlobalSearchHistoryOnItemSelectedCb ); listBox->setSelected( 0 ); - mGlobalSearchHistoryOnItemSelectedCb = mGlobalSearchHistoryList->addEventListener( + mGlobalSearchHistoryOnItemSelectedCb = mGlobalSearchHistoryList->on( Event::OnItemSelected, [this, escapeSequence, searchReplace]( const Event* ) { auto idx = mGlobalSearchHistoryList->getListBox()->getItemSelectedIndex(); auto idxItem = mGlobalSearchHistory.at( mGlobalSearchHistory.size() - 1 - idx ); - updateGlobalSearchBarResults( idxItem.first, idxItem.second, searchReplace, + updateGlobalSearchBarResults( idxItem.search, idxItem.result, searchReplace, escapeSequence ); } ); } else if ( listBox->getItemSelectedIndex() < mGlobalSearchHistory.size() ) { mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - listBox->getItemSelectedIndex()] - .second = model; + .result = model; } } -void GlobalSearchController::doGlobalSearch( String text, bool caseSensitive, bool wholeWord, - bool luaPattern, bool escapeSequence, +std::vector GlobalSearchController::parseGlobMatches( const String& str ) { + std::vector ret; + auto globs = str.split( ',' ); + for ( auto& glob : globs ) { + String::trimInPlace( glob ); + if ( !glob.empty() && glob[0] == '-' ) { + ret.push_back( { glob.substr( 1 ), true } ); + } else { + ret.push_back( { glob, false } ); + } + } + return ret; +} + +void GlobalSearchController::doGlobalSearch( String text, String filter, bool caseSensitive, + bool wholeWord, bool luaPattern, bool escapeSequence, bool searchReplace, bool searchAgain ) { if ( mApp->getDirTree() && mApp->getDirTree()->getFilesCount() > 0 && !text.empty() ) { mGlobalSearchTree = searchReplace ? mGlobalSearchTreeReplace : mGlobalSearchTreeSearch; @@ -552,16 +570,16 @@ void GlobalSearchController::doGlobalSearch( String text, bool caseSensitive, bo #if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN || defined( __EMSCRIPTEN_PTHREADS__ ) mApp->getThreadPool(), #endif - [this, clock, search, loader, searchReplace, searchAgain, escapeSequence, - luaPattern]( const ProjectSearch::Result& res ) { + [this, clock, search, loader, searchReplace, searchAgain, escapeSequence, luaPattern, + filter]( const ProjectSearch::Result& res ) { Log::info( "Global search for \"%s\" took %.2fms", search.c_str(), clock->getElapsedTime().asMilliseconds() ); eeDelete( clock ); mUISceneNode->runOnMainThread( [this, loader, res, search, searchReplace, - searchAgain, escapeSequence, luaPattern] { + searchAgain, escapeSequence, luaPattern, filter] { auto model = ProjectSearch::asModel( res ); model->setResultFromLuaPattern( luaPattern ); - updateGlobalSearchHistory( model, search, searchReplace, searchAgain, + updateGlobalSearchHistory( model, search, filter, searchReplace, searchAgain, escapeSequence ); updateGlobalSearchBarResults( search, model, searchReplace, escapeSequence ); loader->setVisible( false ); @@ -570,7 +588,8 @@ void GlobalSearchController::doGlobalSearch( String text, bool caseSensitive, bo }, caseSensitive, wholeWord, luaPattern ? TextDocument::FindReplaceType::LuaPattern - : TextDocument::FindReplaceType::Normal ); + : TextDocument::FindReplaceType::Normal, + parseGlobMatches( filter ) ); } } @@ -594,7 +613,7 @@ void GlobalSearchController::initGlobalSearchTree( UITreeViewGlobalSearch* searc searchTree->setHeadersVisible( false ); searchTree->setColumnsHidden( { ProjectSearch::ResultModel::Line, ProjectSearch::ResultModel::ColumnStart }, true ); - searchTree->addEventListener( Event::OnModelEvent, [this]( const Event* event ) { + searchTree->on( Event::OnModelEvent, [this]( const Event* event ) { const ModelEvent* modelEvent = static_cast( event ); if ( modelEvent->getModelEventType() == ModelEventType::Open ) { const Model* model = modelEvent->getModel(); @@ -678,7 +697,8 @@ PluginRequestHandle GlobalSearchController::processMessage( const PluginMessage& mUISceneNode->runOnMainThread( [this, search, model] { showGlobalSearch( false ); mGlobalSearchInput->setText( search ); - updateGlobalSearchHistory( model, search, false, false, false ); + mGlobalSearchWhereInput->setText( "" ); + updateGlobalSearchHistory( model, search, "", false, false, false ); updateGlobalSearchBarResults( search, model, false, false ); } ); diff --git a/src/tools/ecode/globalsearchcontroller.hpp b/src/tools/ecode/globalsearchcontroller.hpp index 41ec3980c..32df7f35d 100644 --- a/src/tools/ecode/globalsearchcontroller.hpp +++ b/src/tools/ecode/globalsearchcontroller.hpp @@ -47,8 +47,9 @@ class GlobalSearchController { void initGlobalSearchTree( UITreeViewGlobalSearch* searchTree ); - void doGlobalSearch( String text, bool caseSensitive, bool wholeWord, bool luaPattern, - bool escapeSequence, bool searchReplace, bool searchAgain = false ); + void doGlobalSearch( String text, String filter, bool caseSensitive, bool wholeWord, + bool luaPattern, bool escapeSequence, bool searchReplace, + bool searchAgain = false ); size_t replaceInFiles( const std::string& replaceText, std::shared_ptr model ); @@ -76,18 +77,25 @@ class GlobalSearchController { UITreeViewGlobalSearch* mGlobalSearchTreeSearch{ nullptr }; UITreeViewGlobalSearch* mGlobalSearchTreeReplace{ nullptr }; UITextInput* mGlobalSearchInput{ nullptr }; + UITextInput* mGlobalSearchWhereInput{ nullptr }; UIDropDownList* mGlobalSearchHistoryList{ nullptr }; Uint32 mGlobalSearchHistoryOnItemSelectedCb{ 0 }; - std::deque>> - mGlobalSearchHistory; + struct SearchHistoryItem { + std::string search; + std::string filter; + std::shared_ptr result; + }; + std::deque mGlobalSearchHistory; void onLoadDone( const Variant& lineNum, const Variant& colNum ); PluginRequestHandle processMessage( const PluginMessage& msg ); void updateGlobalSearchHistory( std::shared_ptr model, - const std::string& search, bool searchReplace, bool searchAgain, - bool escapeSequence ); + const std::string& search, const std::string& filter, + bool searchReplace, bool searchAgain, bool escapeSequence ); + + std::vector parseGlobMatches( const String& str ); }; } // namespace ecode diff --git a/src/tools/ecode/ignorematcher.cpp b/src/tools/ecode/ignorematcher.cpp index 578318277..81fb013d2 100644 --- a/src/tools/ecode/ignorematcher.cpp +++ b/src/tools/ecode/ignorematcher.cpp @@ -5,152 +5,6 @@ namespace ecode { -// Author: Robert A. van Engelen, engelen@genivia.com -// Date: August 5, 2019 -// License: The Code Project Open License (CPOL) -// https://www.codeproject.com/info/cpol10.aspx - -#define DOTGLOB 1 -#define PATHSEP '/' -#define CASE( c, caseInsensitive ) ( caseInsensitive ? std::tolower( c ) : ( c ) ) - -bool IgnoreMatcher::globMatch( const std::string& text, const std::string_view& glob, - bool caseInsensitive ) { - size_t i = 0; - size_t j = 0; - size_t n = text.size(); - size_t m = glob.size(); - size_t text1_backup = std::string::npos; - size_t glob1_backup = std::string::npos; - size_t text2_backup = std::string::npos; - size_t glob2_backup = std::string::npos; - bool nodot = !DOTGLOB; - // match pathname if glob contains a / otherwise match the basename - if ( j + 1 < m && glob[j] == '/' ) { - // if pathname starts with ./ then ignore these pairs - while ( i + 1 < n && text[i] == '.' && text[i + 1] == PATHSEP ) - i += 2; - // if pathname starts with a / then ignore it - if ( i < n && text[i] == PATHSEP ) - i++; - j++; - } else if ( glob.find( '/' ) == std::string::npos ) { - size_t sep = text.rfind( PATHSEP ); - if ( sep != std::string::npos ) - i = sep + 1; - } - while ( i < n ) { - if ( j < m ) { - switch ( glob[j] ) { - case '*': - // match anything except . after / - if ( nodot && text[i] == '.' ) - break; - if ( ++j < m && glob[j] == '*' ) { - // trailing ** match everything after / - if ( ++j >= m ) - return true; - // ** followed by a / match zero or more directories - if ( glob[j] != '/' ) - return false; - // new **-loop, discard *-loop - text1_backup = std::string::npos; - glob1_backup = std::string::npos; - text2_backup = i; - glob2_backup = ++j; - continue; - } - // trailing * matches everything except / - text1_backup = i; - glob1_backup = j; - continue; - case '?': - // match anything except . after / - if ( nodot && text[i] == '.' ) - break; - // match any character except / - if ( text[i] == PATHSEP ) - break; - i++; - j++; - continue; - case '[': { - // match anything except . after / - if ( nodot && text[i] == '.' ) - break; - // match any character in [...] except / - if ( text[i] == PATHSEP ) - break; - int lastchr; - bool matched = false; - bool reverse = j + 1 < m && ( glob[j + 1] == '^' || glob[j + 1] == '!' ); - // inverted character class - if ( reverse ) - j++; - // match character class - for ( lastchr = 256; ++j < m && glob[j] != ']'; - lastchr = CASE( glob[j], caseInsensitive ) ) - if ( lastchr < 256 && glob[j] == '-' && j + 1 < m && glob[j + 1] != ']' - ? CASE( text[i], caseInsensitive ) <= - CASE( glob[++j], caseInsensitive ) && - CASE( text[i], caseInsensitive ) >= lastchr - : CASE( text[i], caseInsensitive ) == - CASE( glob[j], caseInsensitive ) ) - matched = true; - if ( matched == reverse ) - break; - i++; - if ( j < m ) - j++; - continue; - } - case '\\': - // literal match \-escaped character - if ( j + 1 < m ) - j++; - // FALLTHROUGH - default: - // match the current non-NUL character - if ( CASE( glob[j], caseInsensitive ) != CASE( text[i], caseInsensitive ) && - !( glob[j] == '/' && text[i] == PATHSEP ) ) - break; - // do not match a . with *, ? [] after / - nodot = !DOTGLOB && glob[j] == '/'; - i++; - j++; - continue; - } - } - if ( glob1_backup != std::string::npos && text[text1_backup] != PATHSEP ) { - // *-loop: backtrack to the last * but do not jump over / - i = ++text1_backup; - j = glob1_backup; - continue; - } - if ( glob2_backup != std::string::npos ) { - // **-loop: backtrack to the last ** - i = ++text2_backup; - j = glob2_backup; - continue; - } - return false; - } - // ignore trailing stars - while ( j < m && glob[j] == '*' ) - j++; - // at end of text means success if nothing else is left to match - return j >= m; -} - -bool IgnoreMatcher::globMatch( const std::string& text, const std::vector& globs, - bool caseInsensitive ) { - for ( const auto& glob : globs ) { - if ( globMatch( text, glob, caseInsensitive ) ) - return true; - } - return false; -} - IgnoreMatcher::IgnoreMatcher( const std::string& rootPath ) : mPath( rootPath ) { FileSystem::dirAddSlashAtEnd( mPath ); } @@ -207,7 +61,7 @@ bool GitIgnoreMatcher::match( const std::string& value ) const { bool match = false; for ( size_t i = 0; i < mPatterns.size(); i++ ) { auto& pattern = mPatterns[i]; - match = globMatch( value, pattern.first ); + match = String::globMatch( value, pattern.first ); if ( pattern.second ) match = !match; if ( match && !pattern.second ) { @@ -215,7 +69,7 @@ bool GitIgnoreMatcher::match( const std::string& value ) const { for ( size_t n = i + 1; n < mPatterns.size(); n++ ) { // Check if there's a positive negate after the match if ( mPatterns[n].second ) { - if ( globMatch( value, mPatterns[n].first ) ) + if ( String::globMatch( value, mPatterns[n].first ) ) return false; } else { break; diff --git a/src/tools/ecode/ignorematcher.hpp b/src/tools/ecode/ignorematcher.hpp index 22a2f94a8..8de5c3df2 100644 --- a/src/tools/ecode/ignorematcher.hpp +++ b/src/tools/ecode/ignorematcher.hpp @@ -28,12 +28,6 @@ class IgnoreMatcher { const bool& matcherReady() const { return mMatcherReady; } - static bool globMatch( const std::string& text, const std::string_view& glob, - bool caseInsensitive = false ); - - static bool globMatch( const std::string& text, const std::vector& globs, - bool caseInsensitive = false ); - protected: std::string mPath; bool mMatcherReady{ false }; diff --git a/src/tools/ecode/projectsearch.cpp b/src/tools/ecode/projectsearch.cpp index 03a54f9ba..f98e64dd6 100644 --- a/src/tools/ecode/projectsearch.cpp +++ b/src/tools/ecode/projectsearch.cpp @@ -144,13 +144,25 @@ searchInFileLuaPattern( const std::string& file, const std::string& text, const void ProjectSearch::find( const std::vector files, const std::string& string, ResultCb result, bool caseSensitive, bool wholeWord, - const TextDocument::FindReplaceType& type ) { + const TextDocument::FindReplaceType& type, + const std::vector& pathFilters ) { Result res; const auto occ = type == TextDocument::FindReplaceType::Normal ? String::BMH::createOccTable( (const unsigned char*)string.c_str(), string.size() ) : std::vector(); for ( auto& file : files ) { + bool skip = false; + for ( const auto& filter : pathFilters ) { + bool matches = String::globMatch( file, filter.first ); + if ( ( matches && filter.second ) || ( !matches && !filter.second ) ) { + skip = true; + break; + } + } + if ( skip ) + continue; + auto fileRes = type == TextDocument::FindReplaceType::Normal ? searchInFileHorspool( file, string, caseSensitive, wholeWord, occ ) : searchInFileLuaPattern( file, string, caseSensitive, wholeWord ); @@ -169,7 +181,8 @@ struct FindData { void ProjectSearch::find( const std::vector files, std::string string, std::shared_ptr pool, ResultCb result, bool caseSensitive, - bool wholeWord, const TextDocument::FindReplaceType& type ) { + bool wholeWord, const TextDocument::FindReplaceType& type, + const std::vector& pathFilters ) { if ( files.empty() ) result( {} ); FindData* findData = eeNew( FindData, () ); @@ -180,7 +193,43 @@ void ProjectSearch::find( const std::vector files, std::string stri type == TextDocument::FindReplaceType::Normal ? String::BMH::createOccTable( (const unsigned char*)string.c_str(), string.size() ) : std::vector(); + std::vector search; + search.resize( files.size() ); + size_t pos = 0; + size_t count = 0; for ( auto& file : files ) { + bool skip = false; + for ( const auto& filter : pathFilters ) { + bool matches = String::globMatch( file, filter.first ); + if ( ( matches && filter.second ) || ( !matches && !filter.second ) ) { + skip = true; + break; + } + } + if ( skip ) { + search[pos++] = false; + continue; + } + search[pos++] = true; + count++; + } + + findData->resCount = count; + + if ( count == 0 ) { + result( findData->res ); + eeDelete( findData ); + return; + } + + pos = 0; + for ( const auto& file : files ) { + if ( !search[pos] ) { + pos++; + continue; + } + pos++; + pool->run( [findData, file, string, caseSensitive, wholeWord, occ, type] { auto fileRes = diff --git a/src/tools/ecode/projectsearch.hpp b/src/tools/ecode/projectsearch.hpp index 2af00a29f..57f7422d5 100644 --- a/src/tools/ecode/projectsearch.hpp +++ b/src/tools/ecode/projectsearch.hpp @@ -16,6 +16,9 @@ using namespace EE::UI::Models; namespace ecode { +using GlobMatch = std::pair; // where string is the glob and bool true + // indicates that it's inverted / negated + class ProjectSearch { public: struct ResultData { @@ -208,13 +211,15 @@ class ProjectSearch { static void find( const std::vector files, const std::string& string, ResultCb result, bool caseSensitive, bool wholeWord = false, - const TextDocument::FindReplaceType& type = TextDocument::FindReplaceType::Normal ); + const TextDocument::FindReplaceType& type = TextDocument::FindReplaceType::Normal, + const std::vector& pathFilters = {} ); static void find( const std::vector files, std::string string, std::shared_ptr pool, ResultCb result, bool caseSensitive, bool wholeWord = false, - const TextDocument::FindReplaceType& type = TextDocument::FindReplaceType::Normal ); + const TextDocument::FindReplaceType& type = TextDocument::FindReplaceType::Normal, + const std::vector& pathFilters = {} ); }; } // namespace ecode