mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-29 17:46:29 +03:00
465 lines
18 KiB
C++
465 lines
18 KiB
C++
#include "docsearchcontroller.hpp"
|
|
#include "ecode.hpp"
|
|
|
|
namespace ecode {
|
|
|
|
DocSearchController::DocSearchController( UICodeEditorSplitter* editorSplitter, App* app ) :
|
|
mSplitter( editorSplitter ), mApp( app ) {}
|
|
|
|
DocSearchController::~DocSearchController() {
|
|
Clock clock;
|
|
while ( mSearchingCount && clock.getElapsedTime() < Seconds( 1 ) )
|
|
Sys::sleep( Milliseconds( 10 ) );
|
|
}
|
|
|
|
void DocSearchController::refreshHighlight() {
|
|
if ( mSearchState.editor && mSplitter->editorExists( mSearchState.editor ) ) {
|
|
mSearchState.text = mFindInput->getText();
|
|
mSearchState.editor->setHighlightWord( mSearchState.toTextSearchParams() );
|
|
if ( !mSearchState.text.empty() ) {
|
|
findNextText( mSearchState, true );
|
|
} else {
|
|
mFindInput->removeClass( "error" );
|
|
mSearchState.editor->getDocument().setSelection(
|
|
mSearchState.editor->getDocument().getSelection().start() );
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocSearchController::initSearchBar(
|
|
UISearchBar* searchBar, const SearchBarConfig& searchBarConfig,
|
|
std::unordered_map<std::string, std::string> keybindings ) {
|
|
mSearchBarLayout = searchBar;
|
|
mSearchBarLayout->setVisible( false )->setEnabled( false );
|
|
auto addClickListener = [this]( UIWidget* widget, std::string cmd ) {
|
|
widget->setTooltipText( mSearchBarLayout->getKeyBindings().getCommandKeybindString( cmd ) );
|
|
widget->on( Event::MouseClick, [this, cmd]( const Event* event ) {
|
|
const MouseEvent* mouseEvent = static_cast<const MouseEvent*>( event );
|
|
if ( mouseEvent->getFlags() & EE_BUTTON_LMASK )
|
|
mSearchBarLayout->execute( cmd );
|
|
} );
|
|
};
|
|
auto addReturnListener = [this]( UIWidget* widget, std::string cmd ) {
|
|
widget->on( Event::OnPressEnter,
|
|
[this, cmd]( const Event* ) { mSearchBarLayout->execute( cmd ); } );
|
|
};
|
|
|
|
auto& kbind = mSearchBarLayout->getKeyBindings();
|
|
kbind.addKeybindsString(
|
|
{ { mApp->getKeybind( "repeat-find" ), "repeat-find" },
|
|
{ mApp->getKeybind( "find-prev" ), "find-prev" },
|
|
{ mApp->getKeybind( "open-global-search" ), "open-global-search" },
|
|
{ mApp->getKeybind( "select-all-results" ), "select-all-results" } } );
|
|
kbind.addKeybindsStringUnordered( keybindings );
|
|
|
|
mFindInput = mSearchBarLayout->find<UITextInput>( "search_find" );
|
|
mFindInput->setEscapePastedText( true );
|
|
mReplaceInput = mSearchBarLayout->find<UITextInput>( "search_replace" );
|
|
mCaseSensitiveChk = mSearchBarLayout->find<UICheckBox>( "case_sensitive" );
|
|
mEscapeSequenceChk = mSearchBarLayout->find<UICheckBox>( "escape_sequence" );
|
|
mWholeWordChk = mSearchBarLayout->find<UICheckBox>( "whole_word" );
|
|
mLuaPatternChk = mSearchBarLayout->find<UICheckBox>( "lua_pattern" );
|
|
mRegExChk = mSearchBarLayout->find<UICheckBox>( "regex" );
|
|
UIPushButton* replaceAllButton = mSearchBarLayout->find<UIPushButton>( "replace_all" );
|
|
UIPushButton* findPrevButton = mSearchBarLayout->find<UIPushButton>( "find_prev" );
|
|
UIPushButton* findNextButton = mSearchBarLayout->find<UIPushButton>( "find_next" );
|
|
UIPushButton* replaceButton = mSearchBarLayout->find<UIPushButton>( "replace" );
|
|
UIPushButton* findReplaceButton = mSearchBarLayout->find<UIPushButton>( "replace_find" );
|
|
UIPushButton* selectAllButton = mSearchBarLayout->find<UIPushButton>( "select_all" );
|
|
UIWidget* closeButton = mSearchBarLayout->find<UIWidget>( "searchbar_close" );
|
|
mCaseSensitiveChk->setChecked( searchBarConfig.caseSensitive );
|
|
mLuaPatternChk->setChecked( searchBarConfig.luaPattern );
|
|
mRegExChk->setChecked( searchBarConfig.regex );
|
|
mWholeWordChk->setChecked( searchBarConfig.wholeWord );
|
|
mEscapeSequenceChk->setChecked( searchBarConfig.escapeSequence );
|
|
|
|
mLuaPatternChk->setTooltipText( kbind.getCommandKeybindString( "toggle-lua-pattern" ) );
|
|
mRegExChk->setTooltipText( kbind.getCommandKeybindString( "toggle-regex" ) );
|
|
mCaseSensitiveChk->setTooltipText( kbind.getCommandKeybindString( "change-case" ) );
|
|
mWholeWordChk->setTooltipText( kbind.getCommandKeybindString( "change-whole-word" ) );
|
|
std::string kbindEscape = kbind.getCommandKeybindString( "change-escape-sequence" );
|
|
if ( !kbindEscape.empty() )
|
|
mEscapeSequenceChk->setTooltipText( mEscapeSequenceChk->getTooltipText() + " (" +
|
|
kbindEscape + ")" );
|
|
|
|
mCaseSensitiveChk->on( Event::OnValueChange, [this]( const Event* ) {
|
|
mSearchState.caseSensitive = mCaseSensitiveChk->isChecked();
|
|
refreshHighlight();
|
|
} );
|
|
|
|
mEscapeSequenceChk->on( Event::OnValueChange, [this]( const Event* ) {
|
|
mSearchState.escapeSequences = mEscapeSequenceChk->isChecked();
|
|
refreshHighlight();
|
|
} );
|
|
|
|
mWholeWordChk->on( Event::OnValueChange, [this]( const Event* ) {
|
|
mSearchState.wholeWord = mWholeWordChk->isChecked();
|
|
refreshHighlight();
|
|
} );
|
|
|
|
mLuaPatternChk->on( Event::OnValueChange, [this]( const Event* ) {
|
|
if ( mValueChanging )
|
|
return;
|
|
BoolScopedOp op( mValueChanging, true );
|
|
mSearchState.type = mLuaPatternChk->isChecked() ? TextDocument::FindReplaceType::LuaPattern
|
|
: TextDocument::FindReplaceType::Normal;
|
|
if ( mLuaPatternChk->isChecked() && mRegExChk->isChecked() )
|
|
mRegExChk->setChecked( false );
|
|
refreshHighlight();
|
|
} );
|
|
mRegExChk->on( Event::OnValueChange, [this]( const Event* ) {
|
|
if ( mValueChanging )
|
|
return;
|
|
BoolScopedOp op( mValueChanging, true );
|
|
mSearchState.type = mRegExChk->isChecked() ? TextDocument::FindReplaceType::RegEx
|
|
: TextDocument::FindReplaceType::Normal;
|
|
if ( mRegExChk->isChecked() && mLuaPatternChk->isChecked() )
|
|
mLuaPatternChk->setChecked( false );
|
|
refreshHighlight();
|
|
} );
|
|
|
|
mFindInput->on( Event::OnTextChanged, [this]( const Event* ) { refreshHighlight(); } );
|
|
mFindInput->on( Event::OnTextPasted, [this]( const Event* ) {
|
|
if ( mFindInput->getUISceneNode()->getWindow()->getClipboard()->getText().find( '\n' ) !=
|
|
String::InvalidPos ) {
|
|
if ( !mEscapeSequenceChk->isChecked() )
|
|
mEscapeSequenceChk->setChecked( true );
|
|
}
|
|
} );
|
|
mSearchBarLayout->setCommand( "close-searchbar", [this] {
|
|
hideSearchBar();
|
|
if ( mSplitter->getCurWidget() )
|
|
mSplitter->getCurWidget()->setFocus();
|
|
if ( mSearchState.editor ) {
|
|
if ( mSplitter->editorExists( mSearchState.editor ) ) {
|
|
mSearchState.editor->setHighlightWord( { "" } );
|
|
mSearchState.editor->setHighlightTextRange( TextRange() );
|
|
}
|
|
}
|
|
} );
|
|
mSearchBarLayout->setCommand( "repeat-find", [this] { findNextText( mSearchState ); } );
|
|
mSearchBarLayout->setCommand( "replace-all", [this] {
|
|
size_t count = replaceAll( mSearchState, mReplaceInput->getText() );
|
|
mApp->getNotificationCenter()->addNotification(
|
|
String::format( "Replaced %zu occurrences.", count ) );
|
|
mReplaceInput->setFocus();
|
|
} );
|
|
mSearchBarLayout->setCommand( "select-all-results", [this] { selectAll( mSearchState ); } );
|
|
mSearchBarLayout->setCommand(
|
|
"find-and-replace", [this] { findAndReplace( mSearchState, mReplaceInput->getText() ); } );
|
|
mSearchBarLayout->setCommand( "find-prev", [this] { findPrevText( mSearchState ); } );
|
|
mSearchBarLayout->setCommand( "replace-selection", [this] {
|
|
replaceSelection( mSearchState, mReplaceInput->getText() );
|
|
} );
|
|
mSearchBarLayout->setCommand( "change-case", [this] {
|
|
mCaseSensitiveChk->setChecked( !mCaseSensitiveChk->isChecked() );
|
|
} );
|
|
mSearchBarLayout->setCommand(
|
|
"change-whole-word", [this] { mWholeWordChk->setChecked( !mWholeWordChk->isChecked() ); } );
|
|
mSearchBarLayout->setCommand( "change-escape-sequence", [this] {
|
|
mEscapeSequenceChk->setChecked( !mEscapeSequenceChk->isChecked() );
|
|
} );
|
|
mSearchBarLayout->setCommand( "toggle-lua-pattern", [this] {
|
|
mRegExChk->setChecked( false );
|
|
mLuaPatternChk->setChecked( !mLuaPatternChk->isChecked() );
|
|
} );
|
|
mSearchBarLayout->setCommand( "toggle-regex", [this] {
|
|
mLuaPatternChk->setChecked( false );
|
|
mRegExChk->setChecked( !mRegExChk->isChecked() );
|
|
} );
|
|
mSearchBarLayout->setCommand( "open-global-search",
|
|
[this] { mApp->showGlobalSearch( false ); } );
|
|
|
|
addReturnListener( mFindInput, "repeat-find" );
|
|
addReturnListener( mReplaceInput, "find-and-replace" );
|
|
addClickListener( findPrevButton, "find-prev" );
|
|
addClickListener( findNextButton, "repeat-find" );
|
|
addClickListener( selectAllButton, "select-all-results" );
|
|
addClickListener( replaceButton, "replace-selection" );
|
|
addClickListener( findReplaceButton, "find-and-replace" );
|
|
addClickListener( replaceAllButton, "replace-all" );
|
|
addClickListener( closeButton, "close-searchbar" );
|
|
|
|
mFindInput->setSelectAllDocOnTabNavigate( false );
|
|
mReplaceInput->setSelectAllDocOnTabNavigate( false );
|
|
mReplaceInput->on( Event::OnTabNavigate, [this]( const Event* ) { mFindInput->setFocus(); } );
|
|
}
|
|
|
|
void DocSearchController::showFindView() {
|
|
mApp->hideLocateBar();
|
|
mApp->hideGlobalSearchBar();
|
|
mApp->getStatusBar()->hideAllElements();
|
|
|
|
if ( !mSplitter->curEditorExistsAndFocused() )
|
|
return;
|
|
|
|
UICodeEditor* editor = mSplitter->getCurEditor();
|
|
mSearchState.editor = editor;
|
|
mSearchState.range = TextRange();
|
|
mSearchState.caseSensitive = mCaseSensitiveChk->isChecked();
|
|
mSearchState.wholeWord = mWholeWordChk->isChecked();
|
|
mSearchState.escapeSequences = mEscapeSequenceChk->isChecked();
|
|
mSearchState.type = mLuaPatternChk->isChecked()
|
|
? TextDocument::FindReplaceType::LuaPattern
|
|
: ( mRegExChk->isChecked() ? TextDocument::FindReplaceType::RegEx
|
|
: TextDocument::FindReplaceType::Normal );
|
|
mSearchBarLayout->setEnabled( true )->setVisible( true );
|
|
|
|
mFindInput->getDocument().selectAll();
|
|
mFindInput->setFocus();
|
|
|
|
const TextDocument& doc = editor->getDocument();
|
|
|
|
if ( doc.getSelection().hasSelection() ) {
|
|
String text = doc.getSelectedText();
|
|
|
|
if ( !doc.getSelection().inSameLine() )
|
|
mSearchState.range = doc.getSelection( true );
|
|
|
|
if ( !text.empty() && doc.getSelection().inSameLine() ) {
|
|
mFindInput->setText( text );
|
|
mFindInput->getDocument().selectAll();
|
|
} else if ( !mFindInput->getText().empty() ) {
|
|
mFindInput->getDocument().selectAll();
|
|
}
|
|
}
|
|
mSearchState.text = mFindInput->getText();
|
|
editor->setHighlightTextRange( mSearchState.range );
|
|
editor->setHighlightWord( mSearchState.toTextSearchParams() );
|
|
editor->getDocument().setActiveClient( editor );
|
|
}
|
|
|
|
void DocSearchController::findPrevText( SearchState& search ) {
|
|
if ( search.text.empty() )
|
|
search.text = mLastSearch;
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) || search.text.empty() )
|
|
return;
|
|
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
mLastSearch = search.text;
|
|
|
|
Uint64 tag = String::hash( "DocSearchController::findPrevText-" +
|
|
search.editor->getDocument().getHashHexString() );
|
|
mApp->getThreadPool()->removeWithTag( tag );
|
|
mApp->getThreadPool()->run( [this, search] {
|
|
ScopedOp op( [this] { mSearchingCount++; }, [this] { mSearchingCount--; } );
|
|
TextDocument& doc = search.editor->getDocument();
|
|
TextRange range = doc.getDocRange();
|
|
TextPosition from = doc.getSelection( true ).start();
|
|
if ( search.range.isValid() ) {
|
|
range = doc.sanitizeRange( search.range ).normalized();
|
|
from = from < range.start() ? range.start() : from;
|
|
}
|
|
|
|
String txt( search.text );
|
|
if ( search.escapeSequences )
|
|
txt.unescape();
|
|
|
|
TextRange found = doc.findLast( txt, from, search.caseSensitive, search.wholeWord,
|
|
search.type, search.range )
|
|
.result;
|
|
if ( found.isValid() ) {
|
|
doc.setSelection( found );
|
|
mFindInput->runOnMainThread( [this, search] {
|
|
mSplitter->addEditorPositionToNavigationHistory( search.editor );
|
|
mFindInput->removeClass( "error" );
|
|
} );
|
|
return;
|
|
} else {
|
|
found = doc.findLast( txt, range.end(), search.caseSensitive, search.wholeWord,
|
|
search.type, range )
|
|
.result;
|
|
if ( found.isValid() ) {
|
|
doc.setSelection( found );
|
|
mFindInput->runOnMainThread( [this, search] {
|
|
mSplitter->addEditorPositionToNavigationHistory( search.editor );
|
|
mFindInput->removeClass( "error" );
|
|
} );
|
|
return;
|
|
}
|
|
}
|
|
mFindInput->runOnMainThread( [this] { mFindInput->addClass( "error" ); } );
|
|
} );
|
|
}
|
|
|
|
void DocSearchController::findNextText( SearchState& search, bool resetSelection ) {
|
|
if ( search.text.empty() )
|
|
search.text = mLastSearch;
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) || search.text.empty() )
|
|
return;
|
|
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
mLastSearch = search.text;
|
|
|
|
Uint64 tag = String::hash( "DocSearchController::findNextText-" +
|
|
search.editor->getDocument().getHashHexString() );
|
|
|
|
mApp->getThreadPool()->removeWithTag( tag );
|
|
mApp->getThreadPool()->run(
|
|
[this, search, resetSelection] {
|
|
ScopedOp op( [this] { mSearchingCount++; }, [this] { mSearchingCount--; } );
|
|
TextDocument& doc = search.editor->getDocument();
|
|
TextRange range = doc.getDocRange();
|
|
TextPosition from =
|
|
resetSelection ? TextPosition( 0, 0 ) : doc.getSelection( true ).end();
|
|
|
|
if ( search.range.isValid() ) {
|
|
range = doc.sanitizeRange( search.range ).normalized();
|
|
from = from < range.start() ? range.start() : from;
|
|
}
|
|
|
|
String txt( search.text );
|
|
if ( search.escapeSequences )
|
|
txt.unescape();
|
|
|
|
TextRange found =
|
|
doc.find( txt, from, search.caseSensitive, search.wholeWord, search.type, range )
|
|
.result;
|
|
|
|
if ( found.isValid() ) {
|
|
doc.setSelection( found.reversed() );
|
|
mFindInput->runOnMainThread( [this, search] {
|
|
mSplitter->addEditorPositionToNavigationHistory( search.editor );
|
|
mFindInput->removeClass( "error" );
|
|
} );
|
|
return;
|
|
} else {
|
|
found = doc.find( txt, range.start(), search.caseSensitive, search.wholeWord,
|
|
search.type, range )
|
|
.result;
|
|
if ( found.isValid() ) {
|
|
doc.setSelection( found.reversed() );
|
|
mFindInput->runOnMainThread( [this, search] {
|
|
mSplitter->addEditorPositionToNavigationHistory( search.editor );
|
|
mFindInput->removeClass( "error" );
|
|
} );
|
|
return;
|
|
}
|
|
}
|
|
|
|
mFindInput->runOnMainThread( [this] { mFindInput->addClass( "error" ); } );
|
|
},
|
|
{}, tag );
|
|
}
|
|
|
|
bool DocSearchController::replaceSelection( SearchState& search, const String& replacement ) {
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) ||
|
|
!search.editor->getDocument().hasSelection() )
|
|
return false;
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
search.editor->getDocument().replace(
|
|
search.text, replacement, search.editor->getDocument().getSelection().normalized().start(),
|
|
search.caseSensitive, search.wholeWord, search.type,
|
|
search.editor->getDocument().getSelection().normalized() );
|
|
return true;
|
|
}
|
|
|
|
void DocSearchController::selectAll( SearchState& search ) {
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) )
|
|
return;
|
|
if ( search.text.empty() )
|
|
search.text = mLastSearch;
|
|
if ( search.text.empty() )
|
|
return;
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
mLastSearch = search.text;
|
|
TextDocument& doc = search.editor->getDocument();
|
|
auto ranges = doc.findAll( search.text, search.caseSensitive, search.wholeWord, search.type,
|
|
search.range );
|
|
for ( const auto& range : ranges )
|
|
doc.addSelection( range.result.reversed() );
|
|
}
|
|
|
|
int DocSearchController::replaceAll( SearchState& search, const String& replace ) {
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) )
|
|
return 0;
|
|
if ( search.text.empty() )
|
|
search.text = mLastSearch;
|
|
if ( search.text.empty() )
|
|
return 0;
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
mLastSearch = search.text;
|
|
TextDocument& doc = search.editor->getDocument();
|
|
TextPosition startedPosition = doc.getSelection().start();
|
|
|
|
String txt( search.text );
|
|
String repl( replace );
|
|
if ( search.escapeSequences ) {
|
|
txt.unescape();
|
|
repl.unescape();
|
|
}
|
|
|
|
int count = doc.replaceAll( txt, repl, search.caseSensitive, search.wholeWord, search.type,
|
|
search.range );
|
|
doc.setSelection( startedPosition );
|
|
return count;
|
|
}
|
|
|
|
void DocSearchController::findAndReplace( SearchState& search, const String& replace ) {
|
|
if ( !search.editor || !mSplitter->editorExists( search.editor ) )
|
|
return;
|
|
if ( search.text.empty() )
|
|
search.text = mLastSearch;
|
|
if ( search.text.empty() )
|
|
return;
|
|
search.editor->getDocument().setActiveClient( search.editor );
|
|
mLastSearch = search.text;
|
|
TextDocument& doc = search.editor->getDocument();
|
|
|
|
String txt( search.text );
|
|
String repl( replace );
|
|
if ( search.escapeSequences ) {
|
|
txt.unescape();
|
|
repl.unescape();
|
|
}
|
|
|
|
if ( doc.hasSelection() &&
|
|
( doc.getSelectedText() == txt ||
|
|
( search.type == TextDocument::FindReplaceType::LuaPattern &&
|
|
LuaPattern::hasMatches( doc.getAllSelectedText().toUtf8(), txt.toUtf8() ) ) ) ) {
|
|
replaceSelection( search, repl );
|
|
} else {
|
|
findNextText( search );
|
|
}
|
|
}
|
|
|
|
void DocSearchController::hideSearchBar() {
|
|
mSearchBarLayout->setEnabled( false )->setVisible( false );
|
|
}
|
|
|
|
void DocSearchController::onCodeEditorFocusChange( UICodeEditor* editor ) {
|
|
if ( mSearchState.editor && mSearchState.editor != editor ) {
|
|
TextSearchParams word;
|
|
if ( mSplitter->editorExists( mSearchState.editor ) ) {
|
|
word = mSearchState.editor->getHighlightWord();
|
|
mSearchState.editor->setHighlightWord( { "" } );
|
|
mSearchState.editor->setHighlightTextRange( TextRange() );
|
|
} else {
|
|
mSearchState.editor = nullptr;
|
|
}
|
|
mSearchState.text = "";
|
|
mSearchState.range = TextRange();
|
|
if ( editor ) {
|
|
mSearchState.editor = editor;
|
|
mSearchState.editor->setHighlightWord( word );
|
|
mSearchState.range = TextRange();
|
|
}
|
|
}
|
|
}
|
|
|
|
SearchState& DocSearchController::getSearchState() {
|
|
return mSearchState;
|
|
}
|
|
|
|
SearchBarConfig DocSearchController::getSearchBarConfig() const {
|
|
SearchBarConfig searchBarConfig;
|
|
searchBarConfig.caseSensitive = mCaseSensitiveChk->isChecked();
|
|
searchBarConfig.luaPattern = mLuaPatternChk->isChecked();
|
|
searchBarConfig.regex = mRegExChk->isChecked();
|
|
searchBarConfig.wholeWord = mWholeWordChk->isChecked();
|
|
searchBarConfig.escapeSequence = mEscapeSequenceChk->isChecked();
|
|
return searchBarConfig;
|
|
}
|
|
|
|
} // namespace ecode
|