diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index fc4cc776e..f18c88df7 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1360,12 +1360,12 @@ Uint32 UICodeEditor::onMouseDown( const Vector2i& position, const Uint32& flags if ( !mMinimapHover && !mMouseDown ) { mMinimapScrollOffset = 0; scrollToVisibleIndex( calculateMinimapClickedLine( position ), false, true ); - return 1; + } else { + mMinimapScrollOffset = calculateMinimapClickedLine( position ) - + static_cast( getVisibleLineRange().first ); } mMouseDown = true; mMouseDownMinimap = true; - mMinimapScrollOffset = calculateMinimapClickedLine( position ) - - static_cast( getVisibleLineRange().first ); mMinimapDragging = true; getEventDispatcher()->setNodeDragging( this ); mVScrollBar->setEnabled( false ); diff --git a/src/tests/unit_tests/regex.cpp b/src/tests/unit_tests/regex.cpp index 2502c357b..8b6644f6a 100644 --- a/src/tests/unit_tests/regex.cpp +++ b/src/tests/unit_tests/regex.cpp @@ -40,14 +40,32 @@ UTEST( RegEx, cacheHit ) { RegExCache::destroySingleton(); } +UTEST( RegEx, captures ) { + RegEx regex( "(\\d+) and (\\d+)" ); + std::string testStr = "The number is 42 and 23."; + PatternMatcher::Range matches[10]; + regex.matches( testStr, matches ); + ASSERT_EQ( regex.isValid(), true ); + ASSERT_EQ( regex.getNumMatches(), 3ul ); + EXPECT_EQ( matches[0].start, 14 ); + EXPECT_EQ( matches[0].end, 23 ); + EXPECT_EQ( matches[1].start, 14 ); + EXPECT_EQ( matches[1].end, 16 ); + EXPECT_EQ( matches[2].start, 21 ); + EXPECT_EQ( matches[2].end, 23 ); + RegExCache::destroySingleton(); +} + UTEST( RegEx, TextDocument ) { TextDocument doc; - doc.textInput( "This number is 42.\nThe number is 69.\n" ); + doc.textInput( "This number is 42.\nThe number is 23.\n" ); auto res = doc.findAll( "\\d+", true, false, TextDocument::FindReplaceType::RegEx ); - EXPECT_EQ( res.size(), 2ul ); + ASSERT_EQ( res.size(), 2ul ); if ( res.size() == 2ul ) { EXPECT_EQ( res[0].isValid(), true ); + EXPECT_TRUE( res[0].result == TextRange( { 0, 15 }, { 0, 17 } ) ); EXPECT_EQ( res[1].isValid(), true ); + EXPECT_TRUE( res[1].result == TextRange( { 1, 14 }, { 1, 16 } ) ); } RegExCache::destroySingleton(); } diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index f5e7e7f2a..139f35648 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -188,6 +188,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, globalSearchBarConfig.caseSensitive = ini.getValueB( "global_search_bar", "case_sensitive", false ); globalSearchBarConfig.luaPattern = ini.getValueB( "global_search_bar", "lua_pattern", false ); + globalSearchBarConfig.regex = ini.getValueB( "global_search_bar", "regex", false ); globalSearchBarConfig.wholeWord = ini.getValueB( "global_search_bar", "whole_word", false ); globalSearchBarConfig.escapeSequence = ini.getValueB( "global_search_bar", "escape_sequence", false ); @@ -327,6 +328,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "global_search_bar", "case_sensitive", globalSearchBarConfig.caseSensitive ); ini.setValueB( "global_search_bar", "lua_pattern", globalSearchBarConfig.luaPattern ); + ini.setValueB( "global_search_bar", "regex", globalSearchBarConfig.regex ); ini.setValueB( "global_search_bar", "whole_word", globalSearchBarConfig.wholeWord ); ini.setValueB( "global_search_bar", "escape_sequence", globalSearchBarConfig.escapeSequence ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index c9545d66d..ee3f5f52d 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -112,6 +112,7 @@ struct SearchBarConfig { struct GlobalSearchBarConfig { bool caseSensitive{ false }; + bool regex{ false }; bool luaPattern{ false }; bool wholeWord{ false }; bool escapeSequence{ false }; diff --git a/src/tools/ecode/applayout.xml.hpp b/src/tools/ecode/applayout.xml.hpp index e6079d685..f95f4d51b 100644 --- a/src/tools/ecode/applayout.xml.hpp +++ b/src/tools/ecode/applayout.xml.hpp @@ -529,24 +529,24 @@ R"html( - + - + - + - + - + " @@ -582,6 +582,7 @@ R"html( + diff --git a/src/tools/ecode/globalsearchcontroller.cpp b/src/tools/ecode/globalsearchcontroller.cpp index 16c557c9c..2eca7e4d8 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 { @@ -62,8 +62,8 @@ size_t GlobalSearchController::replaceInFiles( const std::string& replaceText, } const ProjectSearch::Result& res = model->getResult(); - bool hasCaptures = - model->isResultFromLuaPattern() && LuaPattern::hasMatches( replaceText, "$%d+" ); + bool hasCaptures = ( model->isResultFromLuaPattern() || model->isResultFromRegEx() ) && + LuaPattern::hasMatches( replaceText, "$%d+" ); if ( hasCaptures ) { for ( const auto& fileResult : res ) { @@ -166,6 +166,9 @@ void GlobalSearchController::initGlobalSearchBar( UICheckBox* wholeWordChk = mGlobalSearchBarLayout->find( "whole_word" ); wholeWordChk->setTooltipText( kbind.getCommandKeybindString( "change-whole-word" ) ); + UICheckBox* regexChk = mGlobalSearchBarLayout->find( "regex" ); + regexChk->setTooltipText( kbind.getCommandKeybindString( "toggle-regex" ) ); + UICheckBox* luaPatternChk = mGlobalSearchBarLayout->find( "lua_pattern" ); luaPatternChk->setTooltipText( kbind.getCommandKeybindString( "toggle-lua-pattern" ) ); @@ -178,6 +181,7 @@ void GlobalSearchController::initGlobalSearchBar( UIWidget* searchBarClose = mGlobalSearchBarLayout->find( "global_searchbar_close" ); caseSensitiveChk->setChecked( globalSearchBarConfig.caseSensitive ); + regexChk->setChecked( globalSearchBarConfig.regex ); luaPatternChk->setChecked( globalSearchBarConfig.luaPattern ); wholeWordChk->setChecked( globalSearchBarConfig.wholeWord ); escapeSequenceChk->setChecked( globalSearchBarConfig.escapeSequence ); @@ -188,24 +192,34 @@ void GlobalSearchController::initGlobalSearchBar( 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(), 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() ) { - 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 ); - } + "search-in-files", + [this, caseSensitiveChk, wholeWordChk, luaPatternChk, escapeSequenceChk, regexChk] { + doGlobalSearch( mGlobalSearchInput->getText(), mGlobalSearchWhereInput->getText(), + caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), + luaPatternChk->isChecked() + ? TextDocument::FindReplaceType::LuaPattern + : ( regexChk->isChecked() ? TextDocument::FindReplaceType::RegEx + : TextDocument::FindReplaceType::Normal ), + escapeSequenceChk->isChecked(), false ); } ); + mGlobalSearchBarLayout->setCommand( "search-again", [this, caseSensitiveChk, wholeWordChk, + luaPatternChk, escapeSequenceChk, + regexChk] { + auto listBox = mGlobalSearchHistoryList->getListBox(); + if ( listBox->getItemSelectedIndex() < mGlobalSearchHistory.size() ) { + const auto& item = mGlobalSearchHistory[mGlobalSearchHistory.size() - 1 - + listBox->getItemSelectedIndex()]; + doGlobalSearch( item.search, item.filter, caseSensitiveChk->isChecked(), + wholeWordChk->isChecked(), + luaPatternChk->isChecked() + ? TextDocument::FindReplaceType::LuaPattern + : ( regexChk->isChecked() ? TextDocument::FindReplaceType::RegEx + : TextDocument::FindReplaceType::Normal ), + escapeSequenceChk->isChecked(), + mGlobalSearchTreeReplace == mGlobalSearchTree, true ); + } + } ); mGlobalSearchBarLayout->setCommand( "search-set-string", [this] { auto listBox = mGlobalSearchHistoryList->getListBox(); const auto& item = @@ -232,6 +246,8 @@ void GlobalSearchController::initGlobalSearchBar( mGlobalSearchBarLayout->setCommand( "change-whole-word", [wholeWordChk] { wholeWordChk->setChecked( !wholeWordChk->isChecked() ); } ); + mGlobalSearchBarLayout->setCommand( + "toggle-regex", [regexChk] { regexChk->setChecked( !regexChk->isChecked() ); } ); mGlobalSearchBarLayout->setCommand( "toggle-lua-pattern", [luaPatternChk] { luaPatternChk->setChecked( !luaPatternChk->isChecked() ); } ); @@ -248,6 +264,22 @@ void GlobalSearchController::initGlobalSearchBar( mGlobalSearchTree->forceKeyDown( keyEvent ); } }; + + luaPatternChk->on( Event::OnValueChange, [this, luaPatternChk, regexChk]( const Event* ) { + if ( mValueChanging ) + return; + BoolScopedOp op( mValueChanging, true ); + if ( luaPatternChk->isChecked() && regexChk->isChecked() ) + regexChk->setChecked( false ); + } ); + regexChk->on( Event::OnValueChange, [this, luaPatternChk, regexChk]( const Event* ) { + if ( mValueChanging ) + return; + BoolScopedOp op( mValueChanging, true ); + if ( regexChk->isChecked() && luaPatternChk->isChecked() ) + luaPatternChk->setChecked( false ); + } ); + mGlobalSearchInput->setSelectAllDocOnTabNavigate( false ); mGlobalSearchWhereInput->setSelectAllDocOnTabNavigate( false ); mGlobalSearchInput->on( Event::OnPressEnter, pressEnterCb ); @@ -330,28 +362,33 @@ void GlobalSearchController::initGlobalSearchBar( } mGlobalSearchBarLayout->forceKeyDown( *keyEvent ); } ); - mGlobalSearchBarLayout->setCommand( - "search-replace-in-files", - [this, caseSensitiveChk, wholeWordChk, luaPatternChk, escapeSequenceChk, replaceInput] { - if ( mGlobalSearchTreeReplace == mGlobalSearchTree ) { - replaceInput->setFocus(); - replaceInput->getDocument().selectAll(); - return; - } + mGlobalSearchBarLayout->setCommand( "search-replace-in-files", [this, caseSensitiveChk, + wholeWordChk, luaPatternChk, + escapeSequenceChk, replaceInput, + regexChk] { + if ( mGlobalSearchTreeReplace == mGlobalSearchTree ) { + replaceInput->setFocus(); + replaceInput->getDocument().selectAll(); + return; + } - // TODO Implement replacement from result from symbol reference - /*if ( mGlobalSearchHistory.back().second->isResultFromSymbolReference() ) { - mGlobalSearchTreeReplace->setModel( mGlobalSearchHistory.back().second ); - showGlobalSearch( true ); - updateGlobalSearchBarResults( mGlobalSearchHistory.back().first, - mGlobalSearchHistory.back().second, true, false ); - } else*/ - { - doGlobalSearch( mGlobalSearchInput->getText(), mGlobalSearchWhereInput->getText(), - caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), - luaPatternChk->isChecked(), escapeSequenceChk->isChecked(), true ); - } - } ); + // TODO Implement replacement from result from symbol reference + /*if ( mGlobalSearchHistory.back().second->isResultFromSymbolReference() ) { + mGlobalSearchTreeReplace->setModel( mGlobalSearchHistory.back().second ); + showGlobalSearch( true ); + updateGlobalSearchBarResults( mGlobalSearchHistory.back().first, + mGlobalSearchHistory.back().second, true, false ); + } else*/ + { + doGlobalSearch( mGlobalSearchInput->getText(), mGlobalSearchWhereInput->getText(), + caseSensitiveChk->isChecked(), wholeWordChk->isChecked(), + luaPatternChk->isChecked() + ? TextDocument::FindReplaceType::LuaPattern + : ( regexChk->isChecked() ? TextDocument::FindReplaceType::RegEx + : TextDocument::FindReplaceType::Normal ), + escapeSequenceChk->isChecked(), true ); + } + } ); mGlobalSearchBarLayout->setCommand( "replace-in-files", [this, replaceInput, escapeSequenceChk] { auto listBox = mGlobalSearchHistoryList->getListBox(); @@ -482,10 +519,12 @@ void GlobalSearchController::clearHistory() { GlobalSearchBarConfig GlobalSearchController::getGlobalSearchBarConfig() const { UICheckBox* caseSensitiveChk = mGlobalSearchBarLayout->find( "case_sensitive" ); UICheckBox* wholeWordChk = mGlobalSearchBarLayout->find( "whole_word" ); + UICheckBox* regexChk = mGlobalSearchBarLayout->find( "regex" ); UICheckBox* luaPatternChk = mGlobalSearchBarLayout->find( "lua_pattern" ); UICheckBox* escapeSequenceChk = mGlobalSearchBarLayout->find( "escape_sequence" ); GlobalSearchBarConfig globalSeachBarConfig; globalSeachBarConfig.caseSensitive = caseSensitiveChk->isChecked(); + globalSeachBarConfig.regex = regexChk->isChecked(); globalSeachBarConfig.luaPattern = luaPatternChk->isChecked(); globalSeachBarConfig.wholeWord = wholeWordChk->isChecked(); globalSeachBarConfig.escapeSequence = escapeSequenceChk->isChecked(); @@ -599,8 +638,10 @@ std::vector GlobalSearchController::parseGlobMatches( const String& s } void GlobalSearchController::doGlobalSearch( String text, String filter, bool caseSensitive, - bool wholeWord, bool luaPattern, bool escapeSequence, - bool searchReplace, bool searchAgain ) { + bool wholeWord, + TextDocument::FindReplaceType searchType, + bool escapeSequence, bool searchReplace, + bool searchAgain ) { if ( mApp->getDirTree() && mApp->getDirTree()->getFilesCount() > 0 && !text.empty() ) { mGlobalSearchTree = searchReplace ? mGlobalSearchTreeReplace : mGlobalSearchTreeSearch; mGlobalSearchTreeSearch->setVisible( !searchReplace ); @@ -629,15 +670,17 @@ void GlobalSearchController::doGlobalSearch( String text, String filter, bool ca #if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN || defined( __EMSCRIPTEN_PTHREADS__ ) mApp->getThreadPool(), #endif - [this, clock, search, loader, searchReplace, searchAgain, escapeSequence, luaPattern, + [this, clock, search, loader, searchReplace, searchAgain, escapeSequence, searchType, 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, filter] { + searchAgain, escapeSequence, searchType, filter] { auto model = ProjectSearch::asModel( res ); - model->setResultFromLuaPattern( luaPattern ); + model->setResultFromLuaPattern( searchType == + TextDocument::FindReplaceType::LuaPattern ); + model->setResultFromRegEx( searchType == TextDocument::FindReplaceType::RegEx ); updateGlobalSearchHistory( model, search, filter, searchReplace, searchAgain, escapeSequence ); updateGlobalSearchBarResults( search, model, searchReplace, escapeSequence ); @@ -645,10 +688,8 @@ void GlobalSearchController::doGlobalSearch( String text, String filter, bool ca loader->close(); } ); }, - caseSensitive, wholeWord, - luaPattern ? TextDocument::FindReplaceType::LuaPattern - : TextDocument::FindReplaceType::Normal, - parseGlobMatches( filter ), mApp->getCurrentProject(), openDocs ); + caseSensitive, wholeWord, searchType, parseGlobMatches( filter ), + mApp->getCurrentProject(), openDocs ); } } diff --git a/src/tools/ecode/globalsearchcontroller.hpp b/src/tools/ecode/globalsearchcontroller.hpp index 32df7f35d..da875d7a0 100644 --- a/src/tools/ecode/globalsearchcontroller.hpp +++ b/src/tools/ecode/globalsearchcontroller.hpp @@ -19,6 +19,7 @@ class GlobalSearchController { { "escape", "close-global-searchbar" }, { "mod+s", "change-case" }, { "mod+w", "change-whole-word" }, + { "mod+p", "toggle-regex" }, { "mod+l", "toggle-lua-pattern" }, { "mod+r", "search-replace-in-files" }, { "mod+g", "search-again" }, @@ -48,8 +49,8 @@ class GlobalSearchController { void initGlobalSearchTree( UITreeViewGlobalSearch* searchTree ); void doGlobalSearch( String text, String filter, bool caseSensitive, bool wholeWord, - bool luaPattern, bool escapeSequence, bool searchReplace, - bool searchAgain = false ); + TextDocument::FindReplaceType searchType, bool escapeSequence, + bool searchReplace, bool searchAgain = false ); size_t replaceInFiles( const std::string& replaceText, std::shared_ptr model ); @@ -86,6 +87,7 @@ class GlobalSearchController { std::shared_ptr result; }; std::deque mGlobalSearchHistory; + bool mValueChanging{ false }; void onLoadDone( const Variant& lineNum, const Variant& colNum ); diff --git a/src/tools/ecode/projectsearch.cpp b/src/tools/ecode/projectsearch.cpp index d55308aad..427908b02 100644 --- a/src/tools/ecode/projectsearch.cpp +++ b/src/tools/ecode/projectsearch.cpp @@ -1,6 +1,7 @@ #include "projectsearch.hpp" #include #include +#include #if EE_PLATFORM == EE_PLATFORM_LINUX // For malloc_trim, which is a GNU extension @@ -142,6 +143,63 @@ searchInFileLuaPattern( const std::string& file, const std::string& text, const return results; } +static std::vector searchInFileRegEx( const std::string& file, + const std::string& text, + const bool& caseSensitive, + const bool& wholeWord ) { + std::string fileText; + FileSystem::fileGet( file, fileText ); + RegEx pattern( text ); + std::vector results; + Int64 totNl = 0; + bool matched = false; + Int64 searchRes = 0; + std::string fileTextOriginal; + + if ( !caseSensitive ) { + fileTextOriginal = fileText; + String::toLowerInPlace( fileText ); + } + + PatternMatcher::Range matches[12]; + do { + int start, end = 0; + + if ( ( matched = pattern.matches( fileText, matches, searchRes ) ) ) { + start = matches[0].start; + end = matches[0].end; + + if ( wholeWord && + !String::isWholeWord( fileText, fileText.substr( start, end - start ), start ) ) { + searchRes = end; + continue; + } + + Int64 relCol; + totNl += countNewLines( fileText, searchRes, start ); + String str( textLine( caseSensitive ? fileText : fileTextOriginal, start, relCol ) ); + int len = end - start; + ProjectSearch::ResultData::Result res; + res.line = std::move( str ); + res.position = { { totNl, (Int64)relCol }, { totNl, (Int64)( relCol + len ) } }; + res.start = start; + res.end = end; + for ( size_t c = 1; c < 12; c++ ) { + if ( matches[c].isValid() ) { + res.captures.push_back( + fileText.substr( matches[c].start, matches[c].end - matches[c].start ) ); + } else { + break; + } + } + results.emplace_back( std::move( res ) ); + searchRes = end; + } + } while ( matched ); + + return results; +} + void ProjectSearch::find( const std::vector files, const std::string& string, ResultCb result, bool caseSensitive, bool wholeWord, const TextDocument::FindReplaceType& type, @@ -168,9 +226,12 @@ void ProjectSearch::find( const std::vector files, const std::strin if ( skip ) continue; - auto fileRes = type == TextDocument::FindReplaceType::Normal - ? searchInFileHorspool( file, string, caseSensitive, wholeWord, occ ) - : searchInFileLuaPattern( file, string, caseSensitive, wholeWord ); + auto fileRes = + type == TextDocument::FindReplaceType::Normal + ? searchInFileHorspool( file, string, caseSensitive, wholeWord, occ ) + : ( type == TextDocument::FindReplaceType::LuaPattern + ? searchInFileLuaPattern( file, string, caseSensitive, wholeWord ) + : searchInFileRegEx( file, string, caseSensitive, wholeWord ) ); if ( !fileRes.empty() ) res.push_back( { file, fileRes } ); } @@ -306,11 +367,14 @@ void ProjectSearch::find( const std::vector files, std::string stri } else { pool->run( [findData, file, string, caseSensitive, wholeWord, occ, type] { - auto fileRes = - type == TextDocument::FindReplaceType::Normal - ? searchInFileHorspool( file, string, caseSensitive, wholeWord, - occ ) - : searchInFileLuaPattern( file, string, caseSensitive, wholeWord ); + auto fileRes = type == TextDocument::FindReplaceType::Normal + ? searchInFileHorspool( file, string, caseSensitive, + wholeWord, occ ) + : ( type == TextDocument::FindReplaceType::LuaPattern + ? searchInFileLuaPattern( + file, string, caseSensitive, wholeWord ) + : searchInFileRegEx( file, string, caseSensitive, + wholeWord ) ); if ( !fileRes.empty() ) { Lock l( findData->resMutex ); findData->res.push_back( { std::move( file ), std::move( fileRes ) } ); diff --git a/src/tools/ecode/projectsearch.hpp b/src/tools/ecode/projectsearch.hpp index 72c7320e5..f6bd18b03 100644 --- a/src/tools/ecode/projectsearch.hpp +++ b/src/tools/ecode/projectsearch.hpp @@ -199,10 +199,15 @@ class ProjectSearch { bool isResultFromLuaPattern() const { return mResultFromLuaPattern; } + void setResultFromRegEx( bool ref ) { mResultFromRegEx = ref; } + + bool isResultFromRegEx() const { return mResultFromRegEx; } + protected: Result mResult; bool mResultFromSymbolReference{ false }; bool mResultFromLuaPattern{ false }; + bool mResultFromRegEx{ false }; }; static std::shared_ptr asModel( const Result& result ) {