mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-08-13 04:42:12 +03:00
Allow to add a filtered file to the project file tree by adding it in .ecode/.prjallowed (SpartanJ/ecode#592).
Allow canceling the global search with escape.
This commit is contained in:
@@ -541,7 +541,7 @@ class EE_API String {
|
||||
/** glob matches a string against a glob
|
||||
** @return True if matches
|
||||
*/
|
||||
static bool globMatch( const std::string_view& text, const std::string_view& glob,
|
||||
static bool globMatch( std::string_view text, std::string_view glob,
|
||||
bool caseInsensitive = false );
|
||||
|
||||
/** glob matches a string against a set of globs
|
||||
|
||||
@@ -227,8 +227,7 @@ const std::size_t String::InvalidPos = StringType::npos;
|
||||
#define PATHSEP '/'
|
||||
#define CASE( c, caseInsensitive ) ( caseInsensitive ? std::tolower( c ) : ( c ) )
|
||||
|
||||
bool String::globMatch( const std::string_view& text, const std::string_view& glob,
|
||||
bool caseInsensitive ) {
|
||||
bool String::globMatch( std::string_view text, std::string_view glob, bool caseInsensitive ) {
|
||||
size_t i = 0;
|
||||
size_t j = 0;
|
||||
size_t n = text.size();
|
||||
|
||||
@@ -551,9 +551,6 @@ R"html(
|
||||
<TextView id="image_close" lw="wc" lh="wc" text="" lg="top|right" enabled="false" />
|
||||
<Loader id="image_loader" lw="64dp" lh="64dp" outline-thickness="6dp" lg="center" visible="false" />
|
||||
</RelativeLayout>
|
||||
<vbox id="notification_center" lw="256dp" lh="wc"
|
||||
lg="right|bottom" margin-right="22dp" margin-bottom="56dp">
|
||||
</vbox>
|
||||
</RelativeLayout>
|
||||
</Splitter>
|
||||
<searchbar id="search_bar" lw="mp" lh="wc">
|
||||
@@ -643,4 +640,7 @@ R"html(
|
||||
tooltip='@string(menu_hold_shift_hint_desc, "Keeping \"Shift\" clicked while changing any options it will keep the menu open.")' />
|
||||
</MainLayout>
|
||||
</vbox>
|
||||
<vbox id="notification_center" lw="256dp" lh="wc"
|
||||
lg="right|bottom" margin-right="22dp" margin-bottom="56dp">
|
||||
</vbox>
|
||||
)html"
|
||||
|
||||
@@ -593,7 +593,7 @@ void GlobalSearchController::showGlobalSearch( bool searchReplace ) {
|
||||
mGlobalSearchInput->setText( text );
|
||||
}
|
||||
mGlobalSearchInput->getDocument().selectAll();
|
||||
auto* loader = mGlobalSearchTree->getParent()->find( "loader" );
|
||||
auto* loader = mGlobalSearchLayout->getParent()->find( "loader" );
|
||||
if ( loader )
|
||||
loader->setVisible( true );
|
||||
if ( !searchReplace ) {
|
||||
@@ -671,10 +671,16 @@ void GlobalSearchController::updateGlobalSearchBar() {
|
||||
void GlobalSearchController::hideGlobalSearchBar() {
|
||||
mGlobalSearchBarLayout->setEnabled( false )->setVisible( false );
|
||||
mGlobalSearchLayout->setVisible( false );
|
||||
auto* loader = mGlobalSearchTree->getParent()->find( "loader" );
|
||||
if ( loader )
|
||||
auto* loader = mGlobalSearchLayout->getParent()->find( "loader" );
|
||||
if ( loader ) {
|
||||
loader->setVisible( false );
|
||||
loader->close();
|
||||
}
|
||||
mApp->getStatusBar()->updateState();
|
||||
if ( mCurSearch ) {
|
||||
mCurSearch->active = false;
|
||||
mApp->getThreadPool()->removeWithTag( mCurSearch->taskTag );
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchController::toggleGlobalSearchBar() {
|
||||
@@ -762,67 +768,78 @@ void GlobalSearchController::doGlobalSearch( String text, String filter, bool ca
|
||||
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 );
|
||||
mGlobalSearchTreeReplace->setVisible( searchReplace );
|
||||
mGlobalSearchLayout->findByClass( "status_box" )->setVisible( true );
|
||||
mGlobalSearchLayout->findByClass( "replace_box" )->setVisible( false );
|
||||
mGlobalSearchLayout->findByClass<UITextView>( "search_str" )->setText( text );
|
||||
UILoader* loader = UILoader::New();
|
||||
loader->setId( "loader" );
|
||||
loader->setRadius( 48 );
|
||||
loader->setOutlineThickness( 6 );
|
||||
loader->setParent( mGlobalSearchLayout->getParent() );
|
||||
loader->setPosition( mGlobalSearchLayout->getPosition() +
|
||||
mGlobalSearchLayout->getSize() * 0.5f - loader->getSize() * 0.5f );
|
||||
Clock clock;
|
||||
if ( escapeSequence )
|
||||
text.unescape();
|
||||
std::string search( text.toUtf8() );
|
||||
if ( nullptr == mApp->getDirTree() || !mApp->getDirTree()->isReady() ) {
|
||||
mApp->getNotificationCenter()->addNotification( mApp->i18n(
|
||||
"project_still_indexing", "Project is still being indexed, please wait a moment." ) );
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<TextDocument>> openDocs;
|
||||
mSplitter->forEachDocSharedPtr(
|
||||
[&openDocs]( auto doc ) { openDocs.emplace_back( std::move( doc ) ); } );
|
||||
auto filters = parseGlobMatches( filter );
|
||||
auto imageExts = Image::getImageExtensionsSupported();
|
||||
imageExts.erase( std::remove_if( imageExts.begin(), imageExts.end(),
|
||||
[]( const std::string& ext ) { return ext == "svg"; } ),
|
||||
imageExts.end() );
|
||||
filters.reserve( filters.size() + imageExts.size() );
|
||||
for ( const auto& ext : imageExts ) {
|
||||
// If user explicitly requested to filter some extension, respect it and do not add the
|
||||
// ignore for it.
|
||||
std::string extGlob( "*." + ext );
|
||||
if ( std::find_if( filters.begin(), filters.end(),
|
||||
[&extGlob]( const std::pair<std::string, bool>& filter ) {
|
||||
return filter.first == extGlob;
|
||||
} ) != filters.end() )
|
||||
continue;
|
||||
filters.emplace_back( extGlob, true );
|
||||
}
|
||||
if ( mApp->getDirTree()->getFilesCount() == 0 || text.empty() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectSearch::find(
|
||||
mApp->getDirTree()->getFiles(), search, mApp->getThreadPool(),
|
||||
[this, clock, search, loader, searchReplace, searchAgain, escapeSequence, searchType,
|
||||
filter]( const ProjectSearch::ConsolidatedResult& res ) {
|
||||
Log::info( "Global search for \"%s\" took %s", search.c_str(),
|
||||
clock.getElapsedTime().toString() );
|
||||
mUISceneNode->runOnMainThread( [this, loader, res = std::move( res ), search,
|
||||
searchReplace, searchAgain, escapeSequence,
|
||||
searchType, filter] {
|
||||
mLastSearchConfig = std::move( res.first );
|
||||
auto model = ProjectSearch::asModel( res.second );
|
||||
model->setOpType( searchType );
|
||||
updateGlobalSearchHistory( model, search, filter, searchReplace, searchAgain,
|
||||
escapeSequence );
|
||||
updateGlobalSearchBarResults( search, model, searchReplace, escapeSequence );
|
||||
mGlobalSearchTree = searchReplace ? mGlobalSearchTreeReplace : mGlobalSearchTreeSearch;
|
||||
mGlobalSearchTreeSearch->setVisible( !searchReplace );
|
||||
mGlobalSearchTreeReplace->setVisible( searchReplace );
|
||||
mGlobalSearchLayout->findByClass( "status_box" )->setVisible( true );
|
||||
mGlobalSearchLayout->findByClass( "replace_box" )->setVisible( false );
|
||||
mGlobalSearchLayout->findByClass<UITextView>( "search_str" )->setText( text );
|
||||
UILoader* loader = UILoader::New();
|
||||
loader->setId( "loader" );
|
||||
loader->setRadius( 48 );
|
||||
loader->setOutlineThickness( 6 );
|
||||
loader->setParent( mGlobalSearchLayout->getParent() );
|
||||
loader->setPosition( mGlobalSearchLayout->getPosition() +
|
||||
mGlobalSearchLayout->getSize() * 0.5f - loader->getSize() * 0.5f );
|
||||
Clock clock;
|
||||
if ( escapeSequence )
|
||||
text.unescape();
|
||||
std::string search( text.toUtf8() );
|
||||
|
||||
std::vector<std::shared_ptr<TextDocument>> openDocs;
|
||||
mSplitter->forEachDocSharedPtr(
|
||||
[&openDocs]( auto doc ) { openDocs.emplace_back( std::move( doc ) ); } );
|
||||
auto filters = parseGlobMatches( filter );
|
||||
auto imageExts = Image::getImageExtensionsSupported();
|
||||
imageExts.erase( std::remove_if( imageExts.begin(), imageExts.end(),
|
||||
[]( const std::string& ext ) { return ext == "svg"; } ),
|
||||
imageExts.end() );
|
||||
filters.reserve( filters.size() + imageExts.size() );
|
||||
for ( const auto& ext : imageExts ) {
|
||||
// If user explicitly requested to filter some extension, respect it and do not add the
|
||||
// ignore for it.
|
||||
std::string extGlob( "*." + ext );
|
||||
if ( std::find_if( filters.begin(), filters.end(),
|
||||
[&extGlob]( const std::pair<std::string, bool>& filter ) {
|
||||
return filter.first == extGlob;
|
||||
} ) != filters.end() )
|
||||
continue;
|
||||
filters.emplace_back( extGlob, true );
|
||||
}
|
||||
|
||||
mCurSearch = ProjectSearch::find(
|
||||
mApp->getDirTree()->getFiles(), search, mApp->getThreadPool(),
|
||||
[this, clock, search, searchReplace, searchAgain, escapeSequence, searchType,
|
||||
filter]( const ProjectSearch::ConsolidatedResult& res ) {
|
||||
Log::info( "Global search for \"%s\" took %s", search.c_str(),
|
||||
clock.getElapsedTime().toString() );
|
||||
mUISceneNode->runOnMainThread( [this, res = std::move( res ), search, searchReplace,
|
||||
searchAgain, escapeSequence, searchType, filter] {
|
||||
mLastSearchConfig = std::move( res.first );
|
||||
auto model = ProjectSearch::asModel( res.second );
|
||||
model->setOpType( searchType );
|
||||
updateGlobalSearchHistory( model, search, filter, searchReplace, searchAgain,
|
||||
escapeSequence );
|
||||
updateGlobalSearchBarResults( search, model, searchReplace, escapeSequence );
|
||||
auto* loader = mGlobalSearchLayout->getParent()->find( "loader" );
|
||||
if ( loader ) {
|
||||
loader->setVisible( false );
|
||||
loader->close();
|
||||
} );
|
||||
},
|
||||
caseSensitive, wholeWord, searchType, filters, mApp->getCurrentProject(), openDocs );
|
||||
}
|
||||
}
|
||||
mCurSearch = nullptr;
|
||||
} );
|
||||
},
|
||||
caseSensitive, wholeWord, searchType, filters, mApp->getCurrentProject(), openDocs );
|
||||
}
|
||||
|
||||
void GlobalSearchController::onLoadDone( const Variant& lineNum, const Variant& colNum ) {
|
||||
|
||||
@@ -87,6 +87,7 @@ class GlobalSearchController {
|
||||
std::deque<SearchHistoryItem> mGlobalSearchHistory;
|
||||
bool mValueChanging{ false };
|
||||
ProjectSearch::SearchConfig mLastSearchConfig;
|
||||
ProjectSearch::FindData* mCurSearch{ nullptr };
|
||||
|
||||
void onLoadDone( const Variant& lineNum, const Variant& colNum );
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ bool GitIgnoreMatcher::parse() {
|
||||
return !mPatterns.empty();
|
||||
}
|
||||
|
||||
bool GitIgnoreMatcher::match( const std::string& value ) const {
|
||||
bool GitIgnoreMatcher::match( std::string_view value ) const {
|
||||
if ( mPatterns.empty() )
|
||||
return false;
|
||||
bool match = false;
|
||||
|
||||
@@ -18,7 +18,7 @@ class IgnoreMatcher {
|
||||
|
||||
virtual bool canMatch() = 0;
|
||||
|
||||
virtual bool match( const std::string& value ) const = 0;
|
||||
virtual bool match( std::string_view value ) const = 0;
|
||||
|
||||
virtual std::string findRepositoryRootPath() const = 0;
|
||||
|
||||
@@ -47,7 +47,7 @@ class GitIgnoreMatcher : public IgnoreMatcher {
|
||||
|
||||
const std::string& getIgnoreFilePath() const override;
|
||||
|
||||
bool match( const std::string& value ) const override;
|
||||
bool match( std::string_view value ) const override;
|
||||
|
||||
std::string findRepositoryRootPath() const override;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ void NotificationCenter::addNotification( const String& text, const Time& delay,
|
||||
tv->setText( text );
|
||||
tv->addClass( "notification" );
|
||||
tv->setTextSelection( allowCopy );
|
||||
mLayout->toFront();
|
||||
Action* sequence = Actions::Sequence::New(
|
||||
{ Actions::FadeIn::New( Seconds( 0.125 ) ), Actions::Delay::New( delay ),
|
||||
Actions::FadeOut::New( Seconds( 0.125 ) ), Actions::Close::New() } );
|
||||
@@ -81,6 +82,7 @@ void NotificationCenter::addShowRequest( const String& uri, const String& action
|
||||
Action* sequence = Actions::Sequence::New(
|
||||
{ Actions::FadeIn::New( Seconds( 0.125 ) ), Actions::Delay::New( delay ),
|
||||
Actions::FadeOut::New( Seconds( 0.125 ) ), Actions::Close::New() } );
|
||||
mLayout->toFront();
|
||||
lay->runAction( sequence );
|
||||
};
|
||||
|
||||
@@ -114,14 +116,14 @@ void NotificationCenter::addInteractiveNotification( String text, String actionT
|
||||
} );
|
||||
UIPushButton* pb = lay->findByType( UI_TYPE_PUSHBUTTON )->asType<UIPushButton>();
|
||||
pb->setText( actionText );
|
||||
pb->onClick(
|
||||
[actionText, onInteraction = std::move( onInteraction )]( const MouseEvent* event ) {
|
||||
if ( onInteraction )
|
||||
onInteraction();
|
||||
} );
|
||||
pb->onClick( [actionText, onInteraction = std::move( onInteraction )]( const MouseEvent* ) {
|
||||
if ( onInteraction )
|
||||
onInteraction();
|
||||
} );
|
||||
Action* sequence = Actions::Sequence::New(
|
||||
{ Actions::FadeIn::New( Seconds( 0.125 ) ), Actions::Delay::New( delay ),
|
||||
Actions::FadeOut::New( Seconds( 0.125 ) ), Actions::Close::New() } );
|
||||
mLayout->toFront();
|
||||
lay->runAction( sequence );
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,18 @@ void ProjectDirectoryTree::scan( const ProjectDirectoryTree::ScanCompleteEvent&
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !found && mAllowedMatcher ) {
|
||||
std::string_view file{ files[i] };
|
||||
if ( String::startsWith( file, mAllowedMatcher->getPath() ) ) {
|
||||
std::string_view localPath( std::string_view{ file }.substr(
|
||||
mAllowedMatcher->getPath().size() ) );
|
||||
if ( mAllowedMatcher->match( localPath ) ) {
|
||||
found = true;
|
||||
}
|
||||
} else if ( mAllowedMatcher->match( file ) ) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if ( found ) {
|
||||
mFiles.emplace_back( std::move( files[i] ) );
|
||||
mNames.emplace_back( std::move( names[i] ) );
|
||||
@@ -339,16 +351,18 @@ void ProjectDirectoryTree::getDirectoryFiles(
|
||||
if ( ignoreMatcher.foundMatch() && ignoreMatcher.match( directory, file ) ) {
|
||||
if ( !allowedMatcher || !allowedMatcher->hasPatterns() )
|
||||
continue;
|
||||
std::string localPath;
|
||||
std::string_view localPath( fullpath );
|
||||
if ( String::startsWith( directory, allowedMatcher->getPath() ) )
|
||||
localPath = directory.substr( allowedMatcher->getPath().size() );
|
||||
if ( !allowedMatcher->match( localPath + file ) )
|
||||
localPath = std::string_view{ fullpath }.substr( allowedMatcher->getPath().size() );
|
||||
if ( !allowedMatcher->match( localPath ) )
|
||||
continue;
|
||||
} else if ( disallowedMatcher && disallowedMatcher->hasPatterns() ) {
|
||||
std::string localPath;
|
||||
if ( String::startsWith( directory, disallowedMatcher->getPath() ) )
|
||||
localPath = directory.substr( disallowedMatcher->getPath().size() );
|
||||
if ( disallowedMatcher->match( localPath + file ) )
|
||||
std::string_view localPath( fullpath );
|
||||
if ( String::startsWith( directory, disallowedMatcher->getPath() ) ) {
|
||||
localPath =
|
||||
std::string_view{ fullpath }.substr( disallowedMatcher->getPath().size() );
|
||||
}
|
||||
if ( disallowedMatcher->match( localPath ) )
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,8 @@ class ProjectDirectoryTree {
|
||||
|
||||
bool isRunning() const { return mRunning; }
|
||||
|
||||
bool isReady() const { return mIsReady; }
|
||||
|
||||
protected:
|
||||
std::string mPath;
|
||||
std::shared_ptr<ThreadPool> mPool;
|
||||
|
||||
@@ -159,13 +159,6 @@ static std::vector<ProjectSearch::ResultData::Result> searchInFileRegEx( const s
|
||||
return searchInFilePatternMatch( file, pattern, caseSensitive, wholeWord );
|
||||
}
|
||||
|
||||
struct FindData {
|
||||
Mutex resMutex;
|
||||
Mutex countMutex;
|
||||
int resCount{ 0 };
|
||||
ProjectSearch::Result res;
|
||||
};
|
||||
|
||||
std::vector<ProjectSearch::ResultData::Result>
|
||||
ProjectSearch::fileResFromDoc( const std::string& string, bool caseSensitive, bool wholeWord,
|
||||
TextDocument::FindReplaceType type,
|
||||
@@ -192,20 +185,29 @@ ProjectSearch::fileResFromDoc( const std::string& string, bool caseSensitive, bo
|
||||
return fileRes;
|
||||
}
|
||||
|
||||
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,
|
||||
const std::vector<GlobMatch>& pathFilters, std::string basePath,
|
||||
std::vector<std::shared_ptr<TextDocument>> openDocs ) {
|
||||
if ( files.empty() )
|
||||
ProjectSearch::FindData*
|
||||
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,
|
||||
const std::vector<GlobMatch>& pathFilters, std::string basePath,
|
||||
std::vector<std::shared_ptr<TextDocument>> openDocs ) {
|
||||
static const std::string_view PROJECT_SEARCH_TASK_TAG = "ProjectSearchFindTag";
|
||||
static const Uint64 PROJECT_SEARCH_TASK_TAG_HASH =
|
||||
std::hash<std::string_view>()( PROJECT_SEARCH_TASK_TAG );
|
||||
|
||||
if ( files.empty() ) {
|
||||
result( {} );
|
||||
return nullptr;
|
||||
}
|
||||
FindData* findData = eeNew( FindData, () );
|
||||
findData->taskTag = PROJECT_SEARCH_TASK_TAG_HASH;
|
||||
|
||||
FileSystem::dirAddSlashAtEnd( basePath );
|
||||
pool->run( [files = std::move( files ), string = std::move( string ), pool = std::move( pool ),
|
||||
result = std::move( result ), caseSensitive, wholeWord, type,
|
||||
pathFilters = std::move( pathFilters ), basePath = std::move( basePath ),
|
||||
pool->run( [findData, files = std::move( files ), string = std::move( string ),
|
||||
pool = std::move( pool ), result = std::move( result ), caseSensitive, wholeWord,
|
||||
type, pathFilters = std::move( pathFilters ), basePath = std::move( basePath ),
|
||||
openDocs = std::move( openDocs )]() mutable {
|
||||
SearchConfig searchConfig( string, caseSensitive, wholeWord, type );
|
||||
FindData* findData = eeNew( FindData, () );
|
||||
findData->resCount = files.size();
|
||||
if ( !caseSensitive )
|
||||
String::toLowerInPlace( string );
|
||||
@@ -244,6 +246,12 @@ void ProjectSearch::find( const std::vector<std::string> files, std::string stri
|
||||
}
|
||||
}
|
||||
|
||||
if ( !findData->active ) {
|
||||
result( {} );
|
||||
eeDelete( findData );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( skip ) {
|
||||
search[pos++] = false;
|
||||
continue;
|
||||
@@ -255,7 +263,7 @@ void ProjectSearch::find( const std::vector<std::string> files, std::string stri
|
||||
|
||||
findData->resCount = count;
|
||||
|
||||
if ( count == 0 ) {
|
||||
if ( count == 0 || !findData->active ) {
|
||||
result( { searchConfig, findData->res } );
|
||||
eeDelete( findData );
|
||||
return;
|
||||
@@ -310,7 +318,7 @@ void ProjectSearch::find( const std::vector<std::string> files, std::string stri
|
||||
doc );
|
||||
}
|
||||
},
|
||||
onSearchEnd );
|
||||
onSearchEnd, PROJECT_SEARCH_TASK_TAG_HASH );
|
||||
} else {
|
||||
pool->run(
|
||||
[findData, file, string, caseSensitive, wholeWord, occ, type]() mutable {
|
||||
@@ -327,10 +335,11 @@ void ProjectSearch::find( const std::vector<std::string> files, std::string stri
|
||||
findData->res.emplace_back( std::string( file ), std::move( fileRes ) );
|
||||
}
|
||||
},
|
||||
onSearchEnd );
|
||||
onSearchEnd, PROJECT_SEARCH_TASK_TAG_HASH );
|
||||
}
|
||||
}
|
||||
} );
|
||||
return findData;
|
||||
}
|
||||
|
||||
void ProjectSearch::ResultModel::removeLastNewLineCharacter() {
|
||||
|
||||
@@ -243,7 +243,16 @@ class ProjectSearch {
|
||||
fileResFromDoc( const std::string& string, bool caseSensitive, bool wholeWord,
|
||||
TextDocument::FindReplaceType type, std::shared_ptr<TextDocument> doc );
|
||||
|
||||
static void
|
||||
struct FindData {
|
||||
Mutex resMutex;
|
||||
Mutex countMutex;
|
||||
int resCount{ 0 };
|
||||
ProjectSearch::Result res;
|
||||
bool active{ true };
|
||||
Uint64 taskTag{ 0 };
|
||||
};
|
||||
|
||||
static FindData*
|
||||
find( const std::vector<std::string> files, std::string string,
|
||||
std::shared_ptr<ThreadPool> pool, ResultCb result, bool caseSensitive,
|
||||
bool wholeWord = false,
|
||||
|
||||
Reference in New Issue
Block a user