#include #include #include #include #ifndef _WIN32 #include #include #include #endif /* _WIN32 */ #include "replxx.hxx" #include "history.hxx" using namespace std; namespace replxx { namespace { void delete_ReplxxHistoryScanImpl( Replxx::HistoryScanImpl* impl_ ) { delete impl_; } } static int const REPLXX_DEFAULT_HISTORY_MAX_LEN( 1000 ); Replxx::HistoryScan::HistoryScan( impl_t impl_ ) : _impl( std::move( impl_ ) ) { } bool Replxx::HistoryScan::next( void ) { return ( _impl->next() ); } Replxx::HistoryScanImpl::HistoryScanImpl( History::entries_t const& entries_ ) : _entries( entries_ ) , _it( _entries.end() ) , _utf8Cache() , _entryCache( std::string(), std::string() ) , _cacheValid( false ) { } Replxx::HistoryEntry const& Replxx::HistoryScan::get( void ) const { return ( _impl->get() ); } bool Replxx::HistoryScanImpl::next( void ) { if ( _it == _entries.end() ) { _it = _entries.begin(); } else { ++ _it; } _cacheValid = false; return ( _it != _entries.end() ); } Replxx::HistoryEntry const& Replxx::HistoryScanImpl::get( void ) const { if ( _cacheValid ) { return ( _entryCache ); } _utf8Cache.assign( _it->text() ); _entryCache = Replxx::HistoryEntry( _it->timestamp(), _utf8Cache.get() ); _cacheValid = true; return ( _entryCache ); } Replxx::HistoryScan::impl_t History::scan( void ) const { return ( Replxx::HistoryScan::impl_t( new Replxx::HistoryScanImpl( _entries ), delete_ReplxxHistoryScanImpl ) ); } History::History( void ) : _entries() , _maxSize( REPLXX_DEFAULT_HISTORY_MAX_LEN ) , _current( _entries.begin() ) , _yankPos( _entries.end() ) , _previous( _entries.begin() ) , _recallMostRecent( false ) , _unique( true ) { } void History::add( UnicodeString const& line, std::string const& when ) { if ( _maxSize <= 0 ) { return; } if ( ! _entries.empty() && ( line == _entries.back().text() ) ) { _entries.back() = Entry( now_ms_str(), line ); return; } remove_duplicate( line ); trim_to_max_size(); _entries.emplace_back( when, line ); _locations.insert( make_pair( line, last() ) ); if ( _current == _entries.end() ) { _current = last(); } _yankPos = _entries.end(); } #ifndef _WIN32 class FileLock { std::string _path; int _lockFd; public: FileLock( std::string const& name_ ) : _path( name_ + ".lock" ) , _lockFd( ::open( _path.c_str(), O_CREAT | O_RDWR, 0600 ) ) { static_cast( ::lockf( _lockFd, F_LOCK, 0 ) == 0 ); } ~FileLock( void ) { static_cast( ::lockf( _lockFd, F_ULOCK, 0 ) == 0 ); ::close( _lockFd ); ::unlink( _path.c_str() ); return; } }; #endif bool History::save( std::string const& filename, bool sync_ ) { #ifndef _WIN32 mode_t old_umask = umask( S_IXUSR | S_IRWXG | S_IRWXO ); FileLock fileLock( filename ); #endif entries_t entries; locations_t locations; if ( ! sync_ ) { entries.swap( _entries ); locations.swap( _locations ); _entries = entries; reset_iters(); } do_load( filename ); sort(); remove_duplicates(); trim_to_max_size(); ofstream histFile( filename ); if ( ! histFile ) { return ( false ); } #ifndef _WIN32 umask( old_umask ); chmod( filename.c_str(), S_IRUSR | S_IWUSR ); #endif Utf8String utf8; for ( Entry const& h : _entries ) { if ( ! h.text().is_empty() ) { utf8.assign( h.text() ); histFile << "### " << h.timestamp() << "\n" << utf8.get() << endl; } } if ( ! sync_ ) { _entries = std::move( entries ); _locations = std::move( locations ); } reset_iters(); return ( true ); } namespace { bool is_timestamp( std::string const& s ) { static char const TIMESTAMP_PATTERN[] = "### dddd-dd-dd dd:dd:dd.ddd"; static int const TIMESTAMP_LENGTH( sizeof ( TIMESTAMP_PATTERN ) - 1 ); if ( s.length() != TIMESTAMP_LENGTH ) { return ( false ); } for ( int i( 0 ); i < TIMESTAMP_LENGTH; ++ i ) { if ( TIMESTAMP_PATTERN[i] == 'd' ) { if ( ! isdigit( s[i] ) ) { return ( false ); } } else if ( s[i] != TIMESTAMP_PATTERN[i] ) { return ( false ); } } return ( true ); } } bool History::do_load( std::string const& filename ) { ifstream histFile( filename ); if ( ! histFile ) { return ( false ); } string line; string when( "0000-00-00 00:00:00.000" ); while ( getline( histFile, line ).good() ) { string::size_type eol( line.find_first_of( "\r\n" ) ); if ( eol != string::npos ) { line.erase( eol ); } if ( is_timestamp( line ) ) { when.assign( line, 4, std::string::npos ); continue; } if ( ! line.empty() ) { _entries.emplace_back( when, UnicodeString( line ) ); } } return ( true ); } bool History::load( std::string const& filename ) { clear(); bool success( do_load( filename ) ); sort(); remove_duplicates(); trim_to_max_size(); _previous = _current = last(); _yankPos = _entries.end(); return ( success ); } void History::sort( void ) { typedef std::vector sortable_entries_t; _locations.clear(); sortable_entries_t sortableEntries( _entries.begin(), _entries.end() ); std::stable_sort( sortableEntries.begin(), sortableEntries.end() ); _entries.clear(); _entries.insert( _entries.begin(), sortableEntries.begin(), sortableEntries.end() ); } void History::clear( void ) { _locations.clear(); _entries.clear(); _current = _entries.begin(); _recallMostRecent = false; } void History::set_max_size( int size_ ) { if ( size_ >= 0 ) { _maxSize = size_; trim_to_max_size(); } } void History::reset_yank_iterator( void ) { _yankPos = _entries.end(); } bool History::next_yank_position( void ) { bool resetYankSize( false ); if ( _yankPos == _entries.end() ) { resetYankSize = true; } if ( ( _yankPos != _entries.begin() ) && ( _yankPos != _entries.end() ) ) { -- _yankPos; } else { _yankPos = moved( _entries.end(), -2 ); } return ( resetYankSize ); } bool History::move( bool up_ ) { bool doRecall( _recallMostRecent && ! up_ ); if ( doRecall ) { _current = _previous; // emulate Windows down-arrow } _recallMostRecent = false; return ( doRecall || move( _current, up_ ? -1 : 1 ) ); } void History::jump( bool start_, bool reset_ ) { if ( start_ ) { _current = _entries.begin(); } else { _current = last(); } if ( reset_ ) { _recallMostRecent = false; } } void History::save_pos( void ) { _previous = _current; } void History::restore_pos( void ) { _current = _previous; } bool History::common_prefix_search( UnicodeString const& prefix_, int prefixSize_, bool back_ ) { int step( back_ ? -1 : 1 ); entries_t::const_iterator it( moved( _current, step, true ) ); while ( it != _current ) { if ( it->text().starts_with( prefix_.begin(), prefix_.begin() + prefixSize_ ) ) { _current = it; commit_index(); return ( true ); } move( it, step, true ); } return ( false ); } bool History::move( entries_t::const_iterator& it_, int by_, bool wrapped_ ) const { if ( by_ > 0 ) { for ( int i( 0 ); i < by_; ++ i ) { ++ it_; if ( it_ != _entries.end() ) { } else if ( wrapped_ ) { it_ = _entries.begin(); } else { -- it_; return ( false ); } } } else { for ( int i( 0 ); i > by_; -- i ) { if ( it_ != _entries.begin() ) { -- it_; } else if ( wrapped_ ) { it_ = last(); } else { return ( false ); } } } return ( true ); } History::entries_t::const_iterator History::moved( entries_t::const_iterator it_, int by_, bool wrapped_ ) const { move( it_, by_, wrapped_ ); return ( it_ ); } void History::erase( entries_t::const_iterator it_ ) { bool invalidated( it_ == _current ); _locations.erase( it_->text() ); it_ = _entries.erase( it_ ); if ( invalidated ) { _current = it_; } if ( ( _current == _entries.end() ) && ! _entries.empty() ) { -- _current; } _yankPos = _entries.end(); _previous = _current; } void History::trim_to_max_size( void ) { while ( size() > _maxSize ) { erase( _entries.begin() ); } } void History::remove_duplicate( UnicodeString const& line_ ) { if ( ! _unique ) { return; } locations_t::iterator it( _locations.find( line_ ) ); if ( it == _locations.end() ) { return; } erase( it->second ); } void History::remove_duplicates( void ) { if ( ! _unique ) { return; } _locations.clear(); typedef std::pair locations_insertion_result_t; for ( entries_t::iterator it( _entries.begin() ), end( _entries.end() ); it != end; ++ it ) { locations_insertion_result_t locationsInsertionResult( _locations.insert( make_pair( it->text(), it ) ) ); if ( ! locationsInsertionResult.second ) { _entries.erase( locationsInsertionResult.first->second ); locationsInsertionResult.first->second = it; } } } void History::update_last( UnicodeString const& line_ ) { if ( _unique ) { _locations.erase( _entries.back().text() ); remove_duplicate( line_ ); _locations.insert( make_pair( line_, last() ) ); } _entries.back() = Entry( now_ms_str(), line_ ); } void History::drop_last( void ) { erase( last() ); } bool History::is_last( void ) const { return ( _current == last() ); } History::entries_t::const_iterator History::last( void ) const { return ( moved( _entries.end(), -1 ) ); } void History::reset_iters( void ) { _previous = _current = last(); _yankPos = _entries.end(); } }