mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Project search filters WIP.
This commit is contained in:
@@ -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<std::string>& globs,
|
||||
bool caseInsensitive = false );
|
||||
|
||||
/** @brief Default constructor
|
||||
** This constructor creates an empty string.
|
||||
**/
|
||||
|
||||
+148
-2
@@ -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<std::string>& globs,
|
||||
bool caseInsensitive ) {
|
||||
for ( const auto& glob : globs ) {
|
||||
if ( globMatch( text, glob, caseInsensitive ) )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Originally written by Joel Yliluoma <joel.yliluoma@iki.fi>
|
||||
* 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 );
|
||||
|
||||
@@ -498,15 +498,24 @@ R"html(
|
||||
</locatebar>
|
||||
<globalsearchbar id="global_search_bar" lw="mp" lh="wc">
|
||||
<hbox lw="mp" lh="wc">
|
||||
<TextView text='@string(search_for, "Search for:")' margin-right="4dp" />
|
||||
<vbox lw="wc" lh="wc">
|
||||
<TextView lh="18dp" text='@string(search_for, "Search for:")' margin-right="4dp" margin-bottom="2dp" />
|
||||
<TextView lh="18dp" text='@string(path_filters, "Path Filters:")' margin-right="4dp" />
|
||||
</vbox>
|
||||
<vbox lw="0" lw8="1" lh="wc">
|
||||
<TextInput id="global_search_find" lw="mp" lh="wc" lh="18dp" padding="0" margin-bottom="2dp" />
|
||||
<hbox lw="mp" lh="wc" margin-bottom="4dp">
|
||||
<CheckBox id="case_sensitive" text='@string(case_sensitive, "Case sensitive")' selected="true" />
|
||||
<CheckBox id="whole_word" text='@string(match_whole_word, "Match Whole Word")' selected="false" margin-left="8dp" />
|
||||
<CheckBox id="lua_pattern" text='@string(lua_pattern, "Lua Pattern")' selected="false" margin-left="8dp" />
|
||||
<CheckBox id="escape_sequence" text='@string(use_escape_sequences, "Use escape sequences")' margin-left="8dp" selected="false" tooltip='@string(escape_sequence_tooltip, "Replace \\, \t, \n, \r and \uXXXX (Unicode characters) with the corresponding control")' />
|
||||
<TextInput id="global_search_where" lw="0" lw8="1" lh="wc" lh="18dp" padding="0" margin-right="4dp"
|
||||
hint='@string(search_where_example, "e.g. *.ts, src/**/include, -src/**/exclude")' hint-display="focus" />
|
||||
<PushButton lw="wc" lh="mp" text="..." />
|
||||
</hbox>
|
||||
<StackLayout lw="mp" lh="wc" margin-bottom="4dp">
|
||||
<CheckBox id="case_sensitive" text='@string(case_sensitive, "Case sensitive")' selected="true" margin-right="8dp" />
|
||||
<CheckBox id="whole_word" text='@string(match_whole_word, "Match Whole Word")' selected="false" margin-right="8dp" />
|
||||
<CheckBox id="lua_pattern" text='@string(lua_pattern, "Lua Pattern")' selected="false" margin-right="8dp" />
|
||||
<CheckBox id="escape_sequence" text='@string(use_escape_sequences, "Use escape sequences")' margin-right="8dp" selected="false"
|
||||
tooltip='@string(escape_sequence_tooltip, "Replace \\, \t, \n, \r and \uXXXX (Unicode characters) with the corresponding control")' />
|
||||
</StackLayout>
|
||||
<hbox lw="mp" lh="wc">
|
||||
<TextView text='@string(history, "History:")' margin-right="4dp" lh="18dp" />
|
||||
<DropDownList id="global_search_history" lw="0" lh="18dp" lw8="1" margin-right="4dp" />
|
||||
|
||||
@@ -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<const MouseEvent*>( event );
|
||||
if ( mouseEvent->getFlags() & EE_BUTTON_LMASK )
|
||||
mGlobalSearchBarLayout->execute( cmd );
|
||||
@@ -165,33 +165,34 @@ void GlobalSearchController::initGlobalSearchBar(
|
||||
escapeSequenceChk->setChecked( globalSearchBarConfig.escapeSequence );
|
||||
|
||||
mGlobalSearchInput = mGlobalSearchBarLayout->find<UITextInput>( "global_search_find" );
|
||||
mGlobalSearchWhereInput = mGlobalSearchBarLayout->find<UITextInput>( "global_search_where" );
|
||||
|
||||
mGlobalSearchHistoryList =
|
||||
mGlobalSearchBarLayout->find<UIDropDownList>( "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<const KeyEvent*>( 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<const KeyEvent*>( 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<const KeyEvent*>( 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<UITextView>( "search_str" )->setText( "" );
|
||||
updateGlobalSearchBar();
|
||||
@@ -494,18 +498,18 @@ void GlobalSearchController::updateGlobalSearchBarResults(
|
||||
|
||||
void GlobalSearchController::updateGlobalSearchHistory(
|
||||
std::shared_ptr<ProjectSearch::ResultModel> 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<String> 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<GlobMatch> GlobalSearchController::parseGlobMatches( const String& str ) {
|
||||
std::vector<GlobMatch> 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<const ModelEvent*>( 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 );
|
||||
} );
|
||||
|
||||
|
||||
@@ -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<ProjectSearch::ResultModel> 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<std::pair<std::string, std::shared_ptr<ProjectSearch::ResultModel>>>
|
||||
mGlobalSearchHistory;
|
||||
struct SearchHistoryItem {
|
||||
std::string search;
|
||||
std::string filter;
|
||||
std::shared_ptr<ProjectSearch::ResultModel> result;
|
||||
};
|
||||
std::deque<SearchHistoryItem> mGlobalSearchHistory;
|
||||
|
||||
void onLoadDone( const Variant& lineNum, const Variant& colNum );
|
||||
|
||||
PluginRequestHandle processMessage( const PluginMessage& msg );
|
||||
|
||||
void updateGlobalSearchHistory( std::shared_ptr<ProjectSearch::ResultModel> 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<GlobMatch> parseGlobMatches( const String& str );
|
||||
};
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
@@ -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<std::string>& 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;
|
||||
|
||||
@@ -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<std::string>& globs,
|
||||
bool caseInsensitive = false );
|
||||
|
||||
protected:
|
||||
std::string mPath;
|
||||
bool mMatcherReady{ false };
|
||||
|
||||
@@ -144,13 +144,25 @@ searchInFileLuaPattern( const std::string& file, const std::string& text, const
|
||||
|
||||
void ProjectSearch::find( const std::vector<std::string> files, const std::string& string,
|
||||
ResultCb result, bool caseSensitive, bool wholeWord,
|
||||
const TextDocument::FindReplaceType& type ) {
|
||||
const TextDocument::FindReplaceType& type,
|
||||
const std::vector<GlobMatch>& pathFilters ) {
|
||||
Result res;
|
||||
const auto occ =
|
||||
type == TextDocument::FindReplaceType::Normal
|
||||
? String::BMH::createOccTable( (const unsigned char*)string.c_str(), string.size() )
|
||||
: std::vector<size_t>();
|
||||
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<std::string> files, std::string string,
|
||||
std::shared_ptr<ThreadPool> pool, ResultCb result, bool caseSensitive,
|
||||
bool wholeWord, const TextDocument::FindReplaceType& type ) {
|
||||
bool wholeWord, const TextDocument::FindReplaceType& type,
|
||||
const std::vector<GlobMatch>& pathFilters ) {
|
||||
if ( files.empty() )
|
||||
result( {} );
|
||||
FindData* findData = eeNew( FindData, () );
|
||||
@@ -180,7 +193,43 @@ void ProjectSearch::find( const std::vector<std::string> files, std::string stri
|
||||
type == TextDocument::FindReplaceType::Normal
|
||||
? String::BMH::createOccTable( (const unsigned char*)string.c_str(), string.size() )
|
||||
: std::vector<size_t>();
|
||||
std::vector<bool> 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 =
|
||||
|
||||
@@ -16,6 +16,9 @@ using namespace EE::UI::Models;
|
||||
|
||||
namespace ecode {
|
||||
|
||||
using GlobMatch = std::pair<std::string, bool>; // 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<std::string> 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<GlobMatch>& pathFilters = {} );
|
||||
|
||||
static void
|
||||
find( const std::vector<std::string> files, std::string string,
|
||||
std::shared_ptr<ThreadPool> 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<GlobMatch>& pathFilters = {} );
|
||||
};
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
Reference in New Issue
Block a user