#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 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( 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( "search_find" ); mFindInput->setEscapePastedText( true ); mReplaceInput = mSearchBarLayout->find( "search_replace" ); mCaseSensitiveChk = mSearchBarLayout->find( "case_sensitive" ); mEscapeSequenceChk = mSearchBarLayout->find( "escape_sequence" ); mWholeWordChk = mSearchBarLayout->find( "whole_word" ); mLuaPatternChk = mSearchBarLayout->find( "lua_pattern" ); mRegExChk = mSearchBarLayout->find( "regex" ); UIPushButton* replaceAllButton = mSearchBarLayout->find( "replace_all" ); UIPushButton* findPrevButton = mSearchBarLayout->find( "find_prev" ); UIPushButton* findNextButton = mSearchBarLayout->find( "find_next" ); UIPushButton* replaceButton = mSearchBarLayout->find( "replace" ); UIPushButton* findReplaceButton = mSearchBarLayout->find( "replace_find" ); UIPushButton* selectAllButton = mSearchBarLayout->find( "select_all" ); UIWidget* closeButton = mSearchBarLayout->find( "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