From 23cf9084cedd3497771d19d2056ff6d0abb30147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 9 Aug 2026 21:38:17 -0300 Subject: [PATCH] perf: reduce allocations across UI and editor hot paths - reuse formatting, document text, property resolution, and layout storage - use inline containers for interpolations, callbacks, wrapping, and matcher state - optimize single-line insertion by mutating document lines without splitting - iterate PATH entries directly in Sys::which and reuse candidate buffers - avoid temporary clocks and string copies in frequent ecode plugin updates - retain documents during autocomplete cache jobs and prevent duplicate scheduling - fix PatternMatcher copy assignment and MSVC move-only container compilation - expand coverage for matching, text insertion, reusable strings, and Sys::which --- include/eepp/core/string.hpp | 25 +++- include/eepp/math/interpolation1d.hpp | 4 +- include/eepp/math/interpolation2d.hpp | 4 +- include/eepp/system/patternmatcher.hpp | 14 +- include/eepp/system/threadpool.hpp | 10 +- include/eepp/ui/doc/textdocument.hpp | 2 + include/eepp/ui/doc/textdocumentline.hpp | 11 ++ include/eepp/ui/uicodeeditor.hpp | 1 + include/eepp/ui/uistacklayout.hpp | 8 ++ include/eepp/ui/uistyle.hpp | 12 +- src/eepp/core/string.cpp | 10 ++ src/eepp/math/interpolation1d.cpp | 4 +- src/eepp/math/interpolation2d.cpp | 4 +- src/eepp/system/patternmatcher.cpp | 75 +++++----- src/eepp/system/sys.cpp | 83 +++++++---- src/eepp/system/threadpool.cpp | 4 +- src/eepp/ui/doc/documentview.cpp | 2 +- src/eepp/ui/doc/foldrangeservice.cpp | 6 +- src/eepp/ui/doc/textdocument.cpp | 55 +++++--- src/eepp/ui/uicodeeditor.cpp | 7 +- src/eepp/ui/uistacklayout.cpp | 44 +++--- src/eepp/ui/uistyle.cpp | 14 +- .../src/eepp/ui/doc/languages/xit.cpp | 1 - src/tests/unit_tests/regex_tests.cpp | 47 +++++++ .../unit_tests/stringsoperations_tests.cpp | 129 +++++++++++++++++ src/tests/unit_tests/textdocument_tests.cpp | 131 ++++++++++++++++++ src/tools/ecode/ecode.cpp | 19 +-- src/tools/ecode/ecode.hpp | 4 + .../autocomplete/autocompleteplugin.cpp | 43 +++--- .../autocomplete/autocompleteplugin.hpp | 4 +- .../ecode/plugins/linter/linterplugin.cpp | 10 +- .../ecode/plugins/linter/linterplugin.hpp | 2 +- .../spellchecker/spellcheckerplugin.cpp | 14 +- .../spellchecker/spellcheckerplugin.hpp | 2 +- .../ecode/plugins/xmltools/xmltoolsplugin.cpp | 2 +- 35 files changed, 618 insertions(+), 189 deletions(-) diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index b286a0402..c26d5cd7f 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -566,6 +566,13 @@ class EE_API String { template static std::string format( std::string_view format, Args&&... args ) { + std::string result; + formatTo( result, format, std::forward( args )... ); + return result; + } + + template + static void formatTo( std::string& result, std::string_view format, Args&&... args ) { #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-security" @@ -578,18 +585,22 @@ class EE_API String { std::snprintf( nullptr, 0, format.data(), FormatArg>::get( std::forward( args ) )... ); - if ( reqSize < 0 ) - return ""; + if ( reqSize < 0 ) { + result.clear(); + return; + } std::size_t bufSize = static_cast( reqSize ) + 1; - std::string result( bufSize, '\0' ); + result.resize( bufSize ); int writtenChars = std::snprintf( &result[0], bufSize, format.data(), FormatArg>::get( std::forward( args ) )... ); - if ( writtenChars < 0 ) - return ""; + if ( writtenChars < 0 ) { + result.clear(); + return; + } if ( static_cast( writtenChars ) < bufSize ) { result.resize( static_cast( writtenChars ) ); @@ -602,7 +613,6 @@ class EE_API String { #elif defined( __GNUC__ ) #pragma GCC diagnostic pop #endif - return result; } /** Format a char buffer */ @@ -993,6 +1003,9 @@ class EE_API String { String& assign( const char* s ); + /** Assigns UTF-8 text while retaining the current UTF-32 storage capacity. */ + String& assignUtf8( std::string_view utf8String ); + String& assign( std::size_t n, StringBaseType c ); template String& assign( InputIterator first, InputIterator last ) { diff --git a/include/eepp/math/interpolation1d.hpp b/include/eepp/math/interpolation1d.hpp index 52b3f7a7c..f5d83b2b4 100644 --- a/include/eepp/math/interpolation1d.hpp +++ b/include/eepp/math/interpolation1d.hpp @@ -111,7 +111,7 @@ class EE_API Interpolation1d { const Time& getDuration() const; /** @return the vector of points */ - const std::vector& getPoints() const; + const SmallVector& getPoints() const; /** @return the vector of points reversed */ std::vector getReversePoints(); @@ -163,7 +163,7 @@ class EE_API Interpolation1d { Float mSpeed; - std::vector mPoints; + SmallVector mPoints; Point1d* mActP; Point1d* mNexP; diff --git a/include/eepp/math/interpolation2d.hpp b/include/eepp/math/interpolation2d.hpp index 12339b844..b8ed33f8c 100644 --- a/include/eepp/math/interpolation2d.hpp +++ b/include/eepp/math/interpolation2d.hpp @@ -112,7 +112,7 @@ class EE_API Interpolation2d { const Uint32& getCurrentPositionIndex() const; /** @return the vector of waypoints */ - const std::vector& getPoints() const; + const SmallVector& getPoints() const; /** @return the vector of waypoints reversed */ std::vector getReversePoints(); @@ -167,7 +167,7 @@ class EE_API Interpolation2d { Point2d* mActP; Point2d* mNexP; - std::vector mPoints; + SmallVector mPoints; OnPathEndCallback mOnPathEndCallback; diff --git a/include/eepp/system/patternmatcher.hpp b/include/eepp/system/patternmatcher.hpp index 9bb74a734..4251761c6 100644 --- a/include/eepp/system/patternmatcher.hpp +++ b/include/eepp/system/patternmatcher.hpp @@ -1,6 +1,7 @@ #ifndef EE_SYSTEM_PATTERNMATCHER_HPP #define EE_SYSTEM_PATTERNMATCHER_HPP +#include #include #include #include @@ -23,16 +24,19 @@ class EE_API PatternMatcher { class EE_API State { public: State( PatternMatcher* pattern, bool ownPattern ); + State( const State& other ); + State& operator=( const State& other ); ~State(); - bool range( int index, int& start, int& end ); + void swap( State& other ) noexcept; + + bool range( int index, int& start, int& end ) const; bool matches( const char* string, size_t length ); PatternMatcher* mPattern; - Range* mRanges; - size_t mRefCount; + std::array mRanges; bool mOwnPattern; }; @@ -87,7 +91,7 @@ class EE_API PatternMatcher { iterator end() { return iterator( nullptr ); } protected: - PatternMatcher::State* mState{ nullptr }; + PatternMatcher::State mState; const char* mString{ nullptr }; size_t mLength{ 0 }; }; @@ -107,7 +111,7 @@ class EE_API PatternMatcher { PatternMatcher::Match gmatch( const std::string& string ) &; bool range( int indexGet, int& startMatch, int& endMatch, - PatternMatcher::Range* returnedMatched ) const; + const PatternMatcher::Range* returnedMatched ) const; bool find( const std::string& s, int& startMatch, int& endMatch, int offset = 0, int returnedMatchIndex = 0 ) const; diff --git a/include/eepp/system/threadpool.hpp b/include/eepp/system/threadpool.hpp index 88c219058..431679997 100644 --- a/include/eepp/system/threadpool.hpp +++ b/include/eepp/system/threadpool.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -28,10 +29,9 @@ class EE_API ThreadPool : NonCopyable { virtual ~ThreadPool(); - Uint64 run( - const std::function& func, - const std::function& doneCallback = []( const Uint64& ) {}, - const Uint64& tag = 0 ); + Uint64 run( SmallFunction<48> func, + const std::function& doneCallback = {}, + const Uint64& tag = 0 ); Uint32 numThreads() const; @@ -50,7 +50,7 @@ class EE_API ThreadPool : NonCopyable { private: struct Work { Uint64 id{ 0 }; - const std::function func; + const SmallFunction<48> func; const std::function callback; Uint64 tag{ 0 }; }; diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index a217b674f..33e778ad5 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -213,6 +213,8 @@ class EE_API TextDocument { String getText( const TextRange& range ) const; + void getTextToBuffer( const TextRange& range, String& buffer ) const; + String getText() const; String getSelectedText() const; diff --git a/include/eepp/ui/doc/textdocumentline.hpp b/include/eepp/ui/doc/textdocumentline.hpp index 5f83288bf..540d39be1 100644 --- a/include/eepp/ui/doc/textdocumentline.hpp +++ b/include/eepp/ui/doc/textdocumentline.hpp @@ -78,6 +78,17 @@ class EE_API TextDocumentLine { } } + void insert( std::size_t position, const String& text ) { + if ( mDocMutex ) { + Lock lock( *mDocMutex ); + mText.insert( position, text ); + updateState(); + } else { + mText.insert( position, text ); + updateState(); + } + } + String substr( std::size_t pos = 0, std::size_t n = String::StringType::npos ) const { if ( mDocMutex ) { Lock lock( *mDocMutex ); diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index c59a66c69..2b1e269a5 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -965,6 +965,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { Mutex mHighlightWordCacheMutex; TextRange mHighlightTextRange; TextRange mPreviewColorRange; + String mMouseOverColorBuffer; std::vector mPlugins; UILoader* mLoader{ nullptr }; Float mGlyphWidth{ 0 }; diff --git a/include/eepp/ui/uistacklayout.hpp b/include/eepp/ui/uistacklayout.hpp index 660ff3d09..807a70a64 100644 --- a/include/eepp/ui/uistacklayout.hpp +++ b/include/eepp/ui/uistacklayout.hpp @@ -1,6 +1,7 @@ #ifndef EE_UI_UISTACKLAYOUT_HPP #define EE_UI_UISTACKLAYOUT_HPP +#include #include namespace EE { namespace UI { @@ -33,7 +34,14 @@ class EE_API UIStackLayout : public UILayout { void setRowValign( const RowValign& rowValign ); protected: + struct NodeLine { + SmallVector nodes; + Float maxY{ 0 }; + Float width{ 0 }; + }; + RowValign mRowValign{ RowValign::Bottom }; + SmallVector mLines; Node* mParentRef{ nullptr }; Uint32 mParentSizeChangeCb{ 0 }; Uint32 mParentCloseCb{ 0 }; diff --git a/include/eepp/ui/uistyle.hpp b/include/eepp/ui/uistyle.hpp index c191a01db..dca3f0949 100644 --- a/include/eepp/ui/uistyle.hpp +++ b/include/eepp/ui/uistyle.hpp @@ -29,6 +29,11 @@ class EE_API UIStyle : public UIState { public: static UIStyle* New( UIWidget* widget ); + UIStyle( const UIStyle& ) = delete; + UIStyle& operator=( const UIStyle& ) = delete; + UIStyle( UIStyle&& ) = delete; + UIStyle& operator=( UIStyle&& ) = delete; + virtual ~UIStyle(); bool stateExists( const Uint32& state ) const; @@ -133,9 +138,10 @@ class EE_API UIStyle : public UIState { Uint32 mPropertyResolutionDepth{ 0 }; Uint64 mLoadedVersion{ 0 }; const CSS::StyleSheet* mLoadedStyleSheet{ nullptr }; - /** Lazily allocated for styles that use substitutions. The pointed-to properties remain stable - * during nested resolutions and retain their string/vector capacities between state changes. */ - SmallVector, 1> mPropertyResolutionSlots; + /** Lazily allocated for styles that use substitutions. The common, non-reentrant resolution + * needs no container allocation; nested slots are retained for later reuse. */ + std::unique_ptr mPropertyResolutionSlot; + std::vector> mNestedPropertyResolutionSlots; bool mChangingState; bool mForceReapplyProperties; bool mDisableAnimations; diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index e11573d61..788fe635c 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -2319,6 +2319,16 @@ String& String::assign( const char* s ) { return *this; } +String& String::assignUtf8( std::string_view utf8String ) { + if ( utf8String.empty() ) { + mString.clear(); + return *this; + } + mString.resize( utf8Length( utf8String ) ); + toUtf32( utf8String, mString.data(), mString.size() ); + return *this; +} + String& String::assign( size_t n, StringBaseType c ) { mString.assign( n, c ); diff --git a/src/eepp/math/interpolation1d.cpp b/src/eepp/math/interpolation1d.cpp index acfd179c4..beed379b8 100644 --- a/src/eepp/math/interpolation1d.cpp +++ b/src/eepp/math/interpolation1d.cpp @@ -37,7 +37,7 @@ Interpolation1d::Interpolation1d( std::vector points ) : mElapsed( Time::Zero ), mDuration( Time::Zero ), mSpeed( 1.3f ), - mPoints( points ), + mPoints( points.begin(), points.end() ), mActP( NULL ), mNexP( NULL ), mOnPathEndCallback(), @@ -352,7 +352,7 @@ const Uint32& Interpolation1d::getCurrentPositionIndex() const { return mCurPoint; } -const std::vector& Interpolation1d::getPoints() const { +const SmallVector& Interpolation1d::getPoints() const { return mPoints; } diff --git a/src/eepp/math/interpolation2d.cpp b/src/eepp/math/interpolation2d.cpp index 6fd665c01..90adc386f 100644 --- a/src/eepp/math/interpolation2d.cpp +++ b/src/eepp/math/interpolation2d.cpp @@ -33,7 +33,7 @@ Interpolation2d::Interpolation2d( std::vector points ) : mElapsed( Time::Zero ), mDuration( Time::Zero ), mSpeed( 1.3f ), - mPoints( points ), + mPoints( points.begin(), points.end() ), mOnPathEndCallback(), mOnStepCallback() {} @@ -346,7 +346,7 @@ const Uint32& Interpolation2d::getCurrentPositionIndex() const { return mCurPoint; } -const std::vector& Interpolation2d::getPoints() const { +const SmallVector& Interpolation2d::getPoints() const { return mPoints; } diff --git a/src/eepp/system/patternmatcher.cpp b/src/eepp/system/patternmatcher.cpp index 9301a7c41..23a25efca 100644 --- a/src/eepp/system/patternmatcher.cpp +++ b/src/eepp/system/patternmatcher.cpp @@ -48,7 +48,7 @@ bool PatternMatcher::find( const std::string& s, int& startMatch, int& endMatch, } bool PatternMatcher::range( int indexGet, int& startMatch, int& endMatch, - PatternMatcher::Range* returnedMatched ) const { + const PatternMatcher::Range* returnedMatched ) const { if ( indexGet == -1 ) indexGet = getNumMatches() > 1 ? 1 : 0; if ( indexGet >= 0 && indexGet < (int)getNumMatches() ) { @@ -59,17 +59,16 @@ bool PatternMatcher::range( int indexGet, int& startMatch, int& endMatch, return false; } -bool PatternMatcher::State::range( int index, int& start, int& end ) { - return mPattern->range( index, start, end, mRanges ); +bool PatternMatcher::State::range( int index, int& start, int& end ) const { + return mPattern->range( index, start, end, mRanges.data() ); } bool PatternMatcher::State::matches( const char* string, size_t length ) { - return mPattern->matches( string, 0, mRanges, length ); + return mPattern->matches( string, 0, mRanges.data(), length ); } PatternMatcher::State::State( PatternMatcher* pattern, bool ownPattern ) : - mRefCount( 1 ), mOwnPattern( ownPattern ) { - mRanges = new Range[10]; + mPattern( pattern ), mOwnPattern( ownPattern ) { if ( ownPattern ) { switch ( pattern->getType() ) { case PatternType::LuaPattern: @@ -82,55 +81,65 @@ PatternMatcher::State::State( PatternMatcher* pattern, bool ownPattern ) : mPattern = new ParserMatcher( pattern->getPattern() ); break; } - } else { - mPattern = pattern; } } +PatternMatcher::State::State( const State& other ) : State( other.mPattern, other.mOwnPattern ) { + mRanges = other.mRanges; +} + +PatternMatcher::State& PatternMatcher::State::operator=( const State& other ) { + if ( this != &other ) { + State copy( other ); + swap( copy ); + } + return *this; +} + PatternMatcher::State::~State() { - delete[] mRanges; if ( mOwnPattern ) delete mPattern; } +void PatternMatcher::State::swap( State& other ) noexcept { + using std::swap; + swap( mPattern, other.mPattern ); + swap( mRanges, other.mRanges ); + swap( mOwnPattern, other.mOwnPattern ); +} + PatternMatcher::Match::Match( PatternMatcher& r, const char* string, bool ownPattern ) : - mString( string ) { + mState( &r, ownPattern ), mString( string ) { mLength = strlen( string ); - mState = new PatternMatcher::State( &r, ownPattern ); } -PatternMatcher::Match::Match( PatternMatcher& r, const std::string& string, bool ownPattern ) { - mState = new PatternMatcher::State( &r, ownPattern ); - mString = string.c_str(); - mLength = string.size(); -} - -PatternMatcher::Match::~Match() { - --mState->mRefCount; - if ( mState->mRefCount == 0 ) - delete mState; -} +PatternMatcher::Match::Match( PatternMatcher& r, const std::string& string, bool ownPattern ) : + mState( &r, ownPattern ), mString( string.c_str() ), mLength( string.size() ) {} PatternMatcher::Match::Match( const PatternMatcher::Match& other ) : - mState( other.mState ), mString( other.mString ), mLength( other.mLength ) { - ++mState->mRefCount; -} + mState( other.mState ), mString( other.mString ), mLength( other.mLength ) {} -PatternMatcher::Match& PatternMatcher::Match::operator=( const Match& ) { - ++mState->mRefCount; +PatternMatcher::Match::~Match() = default; + +PatternMatcher::Match& PatternMatcher::Match::operator=( const Match& other ) { + if ( this != &other ) { + mState = other.mState; + mString = other.mString; + mLength = other.mLength; + } return *this; } void PatternMatcher::Match::next() { int m1 = 0, m2 = 0; - mState->range( 0, m1, m2 ); + mState.range( 0, m1, m2 ); mString += m2; mLength -= m2; } std::string PatternMatcher::Match::group( int idx ) const { int m1, m2; - if ( mState->range( idx, m1, m2 ) ) + if ( mState.range( idx, m1, m2 ) ) return std::string( mString + m1, m2 - m1 ); return ""; } @@ -138,13 +147,13 @@ std::string PatternMatcher::Match::group( int idx ) const { std::string_view PatternMatcher::Match::groupView( int idx ) const { static constexpr auto EMPTY = ""sv; int m1, m2; - if ( mState->range( idx, m1, m2 ) ) + if ( mState.range( idx, m1, m2 ) ) return std::string_view( mString + m1, m2 - m1 ); return EMPTY; } bool PatternMatcher::Match::range( int idx, int& start, int& end ) const { - return mState->range( idx, start, end ); + return mState.range( idx, start, end ); } std::string PatternMatcher::Match::operator[]( int index ) const { @@ -152,7 +161,7 @@ std::string PatternMatcher::Match::operator[]( int index ) const { } bool PatternMatcher::Match::matches() { - return mState->matches( mString, mLength ); + return mState.matches( mString, mLength ); } bool PatternMatcher::Match::subst( std::string& res ) { @@ -161,7 +170,7 @@ bool PatternMatcher::Match::subst( std::string& res ) { return false; } int start = 0, end = 0; - mState->range( 0, start, end ); + mState.range( 0, start, end ); if ( start == 0 ) return true; res.append( mString, start ); diff --git a/src/eepp/system/sys.cpp b/src/eepp/system/sys.cpp index 9bd982bb1..da88b5fd7 100644 --- a/src/eepp/system/sys.cpp +++ b/src/eepp/system/sys.cpp @@ -705,9 +705,8 @@ Int64 Sys::getSystemTime() { #if EE_PLATFORM == EE_PLATFORM_WIN using GetSystemTimePreciseAsFileTimeType = VOID( WINAPI* )( LPFILETIME ); static const auto getSystemTimePreciseAsFileTime = - reinterpret_cast( - GetProcAddress( GetModuleHandleA( "kernel32.dll" ), - "GetSystemTimePreciseAsFileTime" ) ); + reinterpret_cast( GetProcAddress( + GetModuleHandleA( "kernel32.dll" ), "GetSystemTimePreciseAsFileTime" ) ); FILETIME fileTime; if ( getSystemTimePreciseAsFileTime ) @@ -1145,18 +1144,8 @@ std::string Sys::which( const std::string& exeName, FileSystem::fileExists( exeName ) ) return exeName; - std::vector PATHS = getEnvSplit( "PATH" ); #if EE_PLATFORM == EE_PLATFORM_WIN static std::vector PATHEXTS = getEnvSplit( "PATHEXT" ); - std::string exePath; -#endif - - if ( !customSearchPaths.empty() ) { - for ( const auto& searchPath : customSearchPaths ) - PATHS.emplace_back( searchPath ); - } - -#if EE_PLATFORM == EE_PLATFORM_WIN bool hasExtension = false; for ( const auto& pathExt : PATHEXTS ) { if ( String::endsWith( exeName, pathExt ) ) { @@ -1166,25 +1155,59 @@ std::string Sys::which( const std::string& exeName, } #endif - for ( const auto& path : PATHS ) { - std::string fpath( path ); - FileSystem::dirAddSlashAtEnd( fpath ); - fpath += exeName; + std::string foundPath; + std::string candidate; #if EE_PLATFORM == EE_PLATFORM_WIN - if ( hasExtension ) { - if ( FileSystem::fileExists( fpath ) ) - return fpath; - } else { - for ( const auto& pathext : PATHEXTS ) { - exePath = fpath + pathext; - if ( FileSystem::fileExists( exePath ) ) - return exePath; - } - } -#else - if ( FileSystem::fileExists( fpath ) ) - return fpath; + std::string candidateWithExtension; #endif + struct SearchContext { + const std::string* exeName; + std::string* foundPath; + std::string* candidate; +#if EE_PLATFORM == EE_PLATFORM_WIN + const std::vector* pathExts; + std::string* candidateWithExtension; + bool hasExtension; +#endif + }; + SearchContext context{ &exeName, &foundPath, &candidate }; +#if EE_PLATFORM == EE_PLATFORM_WIN + context.pathExts = &PATHEXTS; + context.candidateWithExtension = &candidateWithExtension; + context.hasExtension = hasExtension; +#endif + auto searchPath = [context = &context]( std::string_view path ) { + context->candidate->assign( path ); + FileSystem::dirAddSlashAtEnd( *context->candidate ); + context->candidate->append( *context->exeName ); +#if EE_PLATFORM == EE_PLATFORM_WIN + if ( !context->hasExtension ) { + for ( const auto& pathExt : *context->pathExts ) { + context->candidateWithExtension->assign( *context->candidate ); + context->candidateWithExtension->append( pathExt ); + if ( FileSystem::fileExists( *context->candidateWithExtension ) ) { + *context->foundPath = std::move( *context->candidateWithExtension ); + return false; + } + } + return true; + } +#endif + if ( FileSystem::fileExists( *context->candidate ) ) { + *context->foundPath = std::move( *context->candidate ); + return false; + } + return true; + }; + + const std::string paths = getEnv( "PATH" ); + String::splitCb( searchPath, paths, std::string( 1, PATH_SEP_CHAR ), "", "" ); + if ( !foundPath.empty() ) + return foundPath; + + for ( const auto& path : customSearchPaths ) { + if ( !searchPath( path ) ) + return foundPath; } return ""; } diff --git a/src/eepp/system/threadpool.cpp b/src/eepp/system/threadpool.cpp index ae203a233..a228ee36b 100644 --- a/src/eepp/system/threadpool.cpp +++ b/src/eepp/system/threadpool.cpp @@ -111,7 +111,7 @@ bool ThreadPool::removeWithTag( const Uint64& tag ) { return !ids.empty(); } -Uint64 ThreadPool::run( const std::function& func, +Uint64 ThreadPool::run( SmallFunction<48> func, const std::function& doneCallback, const Uint64& tag ) { Uint64 id = ++mLastWorkId; @@ -121,7 +121,7 @@ Uint64 ThreadPool::run( const std::function& func, if ( mShuttingDown ) return id; - mWork.emplace_back( new Work{ id, func, doneCallback, tag } ); + mWork.emplace_back( new Work{ id, std::move( func ), doneCallback, tag } ); } mWorkAvailable.notify_one(); diff --git a/src/eepp/ui/doc/documentview.cpp b/src/eepp/ui/doc/documentview.cpp index 42ed1a100..7c0f4517a 100644 --- a/src/eepp/ui/doc/documentview.cpp +++ b/src/eepp/ui/doc/documentview.cpp @@ -418,7 +418,7 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { mVisibleLinesOffset.insert( mVisibleLinesOffset.begin() + i, lb.paddingStart ); - std::vector newWraps; + SmallVector newWraps; newWraps.reserve( lb.wraps.size() ); for ( const auto& col : lb.wraps ) newWraps.push_back( { i, col } ); diff --git a/src/eepp/ui/doc/foldrangeservice.cpp b/src/eepp/ui/doc/foldrangeservice.cpp index fea612b90..95309ec0e 100644 --- a/src/eepp/ui/doc/foldrangeservice.cpp +++ b/src/eepp/ui/doc/foldrangeservice.cpp @@ -191,7 +191,7 @@ static std::vector findFoldingRangesMarkdown( TextDocument* doc ) { String::View trimmed = String::trim( lineText.view() ); if ( inCodeBlock ) { - if ( String::startsWith( trimmed, "```" ) ) { + if ( String::startsWith( trimmed, codeFence.view() ) ) { // Ensure there's content to fold between start and end if ( codeBlockStart != -1 && codeBlockStart <= static_cast( lineIdx ) - 1 ) { regions.emplace_back( TextPosition( codeBlockStart, 0 ), // Start at opening ``` @@ -203,7 +203,7 @@ static std::vector findFoldingRangesMarkdown( TextDocument* doc ) { } // Continue to next line if still in code block } else { - if ( String::startsWith( trimmed, "```" ) ) { + if ( String::startsWith( trimmed, codeFence.view() ) ) { // Check if the line contains a closing ``` on the same line. // We search starting from index 3 (after the opening ```). bool isSingleLineBlock = @@ -214,7 +214,7 @@ static std::vector findFoldingRangesMarkdown( TextDocument* doc ) { inCodeBlock = true; codeBlockStart = static_cast( lineIdx ); } - } else if ( String::startsWith( trimmed, "#" ) ) { + } else if ( !trimmed.empty() && trimmed.front() == U'#' ) { // Check if it's a valid heading size_t hashCount = 0; while ( hashCount < trimmed.size() && trimmed[hashCount] == '#' ) diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 38685ab46..b36f60efd 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1372,12 +1372,19 @@ std::string TextDocument::getHashHexString() const { } String TextDocument::getText( const TextRange& range ) const { + String result; + getTextToBuffer( range, result ); + return result; +} + +void TextDocument::getTextToBuffer( const TextRange& range, String& buffer ) const { Lock l( mLinesMutex ); Lock l2( *mDocumentMutex ); TextRange nrange = sanitizeRange( range.normalized() ); + buffer.clear(); if ( !nrange.hasSelection() ) - return String(); + return; Int64 startLine = nrange.start().line(); Int64 endLine = nrange.end().line(); @@ -1395,24 +1402,21 @@ String TextDocument::getText( const TextRange& range ) const { totalSize += endCol; } - String result; - result.reserve( totalSize ); + buffer.reserve( totalSize ); if ( startLine == endLine ) { - result.append( mLines[startLine].getText(), startCol, endCol - startCol ); + buffer.append( mLines[startLine].getText(), startCol, endCol - startCol ); } else { - result.append( mLines[startLine].getText(), startCol, mLines[startLine].size() - startCol ); + buffer.append( mLines[startLine].getText(), startCol, mLines[startLine].size() - startCol ); for ( Int64 i = startLine + 1; i < endLine; ++i ) { - result.append( mLines[i].getText() ); + buffer.append( mLines[i].getText() ); } if ( endCol > 0 ) { - result.append( mLines[endLine].getText(), 0, endCol ); + buffer.append( mLines[endLine].getText(), 0, endCol ); } } - - return result; } String TextDocument::getText() const { @@ -1566,22 +1570,27 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio { Lock l( mLinesMutex ); - String before = mLines[position.line()].substr( 0, position.column() ); - String after = mLines[position.line()].substr( position.column() ); - std::vector lines = text.split( '\n', true ); - linesAdd = eemax( 0, static_cast( lines.size() ) - 1 ); - for ( auto i = 0; i < linesAdd; i++ ) - lines[i] = lines[i] + "\n"; - lines[0] = before + lines[0]; - lines[lines.size() - 1] = lines[lines.size() - 1] + after; + if ( text.find( '\n' ) == String::InvalidPos ) { + mLines[position.line()].insert( position.column(), text ); + notifyLineChanged( position.line() ); + } else { + String before = mLines[position.line()].substr( 0, position.column() ); + String after = mLines[position.line()].substr( position.column() ); + std::vector lines = text.split( '\n', true ); + linesAdd = eemax( 0, static_cast( lines.size() ) - 1 ); + for ( auto i = 0; i < linesAdd; i++ ) + lines[i] = lines[i] + "\n"; + lines[0] = before + lines[0]; + lines[lines.size() - 1] = lines[lines.size() - 1] + after; - mLines[position.line()] = TextDocumentLine( lines[0], mDocumentMutex ); - notifyLineChanged( position.line() ); + mLines[position.line()] = TextDocumentLine( lines[0], mDocumentMutex ); + notifyLineChanged( position.line() ); - for ( Int64 i = 1; i < (Int64)lines.size(); i++ ) { - mLines.insert( mLines.begin() + position.line() + i, - TextDocumentLine( lines[i], mDocumentMutex ) ); - notifyLineChanged( position.line() + i ); + for ( Int64 i = 1; i < (Int64)lines.size(); i++ ) { + mLines.insert( mLines.begin() + position.line() + i, + TextDocumentLine( lines[i], mDocumentMutex ) ); + notifyLineChanged( position.line() + i ); + } } } diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 0e954ed20..e10beab27 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -4941,21 +4941,22 @@ void UICodeEditor::checkMouseOverColor( const Vector2i& position ) { if ( !end.isValid() ) return; TextRange wordPos = { { start.line(), start.column() - 1 }, end }; - String word = mDoc->getText( wordPos ); + mDoc->getTextToBuffer( wordPos, mMouseOverColorBuffer ); + String& word = mMouseOverColorBuffer; bool found = false; if ( word[0] == '#' && ( word.size() == 7 || word.size() == 9 ) ) { if ( checkHexa( word ) ) found = true; } else { wordPos = { start, end }; - word = mDoc->getText( wordPos ); + mDoc->getTextToBuffer( wordPos, word ); if ( end.column() < (Int64)line.size() && line[end.column()] == '(' && ( "rgb" == word || "rgba" == word || "hsl" == word || "hsv" == word || "hsla" == word || "hsva" == word ) ) { const String& text = mDoc->line( start.line() ).getText(); size_t endFun = String::findCloseBracket( text, end.column(), '(', ')' ); if ( endFun != std::string::npos ) { - word = word + text.substr( end.column(), endFun - end.column() + 1 ); + word.append( text, end.column(), endFun - end.column() + 1 ); if ( word.find( "--" ) == String::InvalidPos ) { found = true; wordPos = { wordPos.start(), { wordPos.end().line(), (Int64)endFun + 1 } }; diff --git a/src/eepp/ui/uistacklayout.cpp b/src/eepp/ui/uistacklayout.cpp index da6a4f3d1..3bcfabc55 100644 --- a/src/eepp/ui/uistacklayout.cpp +++ b/src/eepp/ui/uistacklayout.cpp @@ -187,16 +187,17 @@ Uint32 UIStackLayout::onMessage( const NodeMessage* Msg ) { return 0; } -struct NodeLine { - std::vector nodes; - Float maxY{ 0 }; - Float width{ 0 }; -}; - void UIStackLayout::updateLayout() { if ( mPacking ) return; mPacking = true; + for ( auto& line : mLines ) { + line.nodes.clear(); + line.maxY = 0; + line.width = 0; + } + if ( mLines.empty() ) + mLines.emplace_back(); if ( !mVisible ) { setInternalPixelsSize( Sizef::Zero ); @@ -218,14 +219,14 @@ void UIStackLayout::updateLayout() { Float curX = mPaddingPx.Left; Node* child = mChild; - std::vector lines = { {} }; Uint32 curLine = 0; bool addedLine = false; auto addLine = [&]() { curX = mPaddingPx.Left; ++curLine; - lines.push_back( { {} } ); + if ( curLine >= mLines.size() ) + mLines.emplace_back(); addedLine = true; }; @@ -235,7 +236,7 @@ void UIStackLayout::updateLayout() { const Rectf& margin = widget->getLayoutPixelsMargin(); if ( curX + margin.Left + widget->getPixelsSize().getWidth() >= mSize.getWidth() && - !addedLine && !lines[curLine].nodes.empty() ) + !addedLine && !mLines[curLine].nodes.empty() ) addLine(); addedLine = false; @@ -248,13 +249,13 @@ void UIStackLayout::updateLayout() { curX += eeceil( widget->getPixelsSize().getWidth() + margin.Right ); - lines[curLine].nodes.push_back( widget ); - lines[curLine].width = curX; + mLines[curLine].nodes.push_back( widget ); + mLines[curLine].width = curX; if ( widget->getLayoutHeightPolicy() != SizePolicy::MatchParent ) { - lines[curLine].maxY = eeceil( - eemax( lines[curLine].maxY, ( widget->getPixelsSize().getHeight() + - widget->getLayoutPixelsMargin().Top + - widget->getLayoutPixelsMargin().Bottom ) ) ); + mLines[curLine].maxY = eeceil( + eemax( mLines[curLine].maxY, ( widget->getPixelsSize().getHeight() + + widget->getLayoutPixelsMargin().Top + + widget->getLayoutPixelsMargin().Bottom ) ) ); } if ( curX > mSize.getWidth() ) @@ -267,16 +268,19 @@ void UIStackLayout::updateLayout() { Float maxY = mPaddingPx.Top; Float height = 0.f; Float totHeight = maxY; - for ( auto& line : lines ) { + const Uint32 lineCount = curLine + 1; + for ( Uint32 i = 0; i < lineCount; ++i ) { + auto& line = mLines[i]; if ( curLine > 0 && line.maxY == 0 ) - line.maxY = lines[curLine - 1].maxY; + line.maxY = mLines[curLine - 1].maxY; height += line.maxY; totHeight += line.maxY; } totHeight += mPaddingPx.Bottom; curLine = 0; - for ( const auto& line : lines ) { + for ( Uint32 i = 0; i < lineCount; ++i ) { + const auto& line = mLines[i]; Float xDisplacement = 0.f; Float yDisplacement = 0.f; @@ -347,8 +351,8 @@ void UIStackLayout::updateLayout() { } if ( getLayoutWidthPolicy() == SizePolicy::WrapContent && curX < mSize.getWidth() && - ( ( lines.size() == 1 && !lines[0].nodes.empty() ) || - ( lines.size() == 2 && lines[1].nodes.empty() ) ) ) { + ( ( lineCount == 1 && !mLines[0].nodes.empty() ) || + ( lineCount == 2 && mLines[1].nodes.empty() ) ) ) { setInternalPixelsWidth( curX ); notifyLayoutAttrChangeParent( LayoutInvalidation::ParentChildChange ); } diff --git a/src/eepp/ui/uistyle.cpp b/src/eepp/ui/uistyle.cpp index b837adca1..5d253e63c 100644 --- a/src/eepp/ui/uistyle.cpp +++ b/src/eepp/ui/uistyle.cpp @@ -952,9 +952,17 @@ UIStyle::PropertyResolution UIStyle::resolveProperty( const StyleSheetProperty* return PropertyResolution{ nullptr, property }; const Uint32 slotIndex = mPropertyResolutionDepth; - if ( slotIndex >= mPropertyResolutionSlots.size() ) - mPropertyResolutionSlots.emplace_back( std::make_unique() ); - StyleSheetProperty* slot = mPropertyResolutionSlots[slotIndex].get(); + StyleSheetProperty* slot; + if ( 0 == slotIndex ) { + if ( !mPropertyResolutionSlot ) + mPropertyResolutionSlot = std::make_unique(); + slot = mPropertyResolutionSlot.get(); + } else { + const Uint32 nestedSlotIndex = slotIndex - 1; + if ( nestedSlotIndex >= mNestedPropertyResolutionSlots.size() ) + mNestedPropertyResolutionSlots.emplace_back( std::make_unique() ); + slot = mNestedPropertyResolutionSlots[nestedSlotIndex].get(); + } ++mPropertyResolutionDepth; PropertyResolution resolution{ this, slot, slotIndex }; diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/xit.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/xit.cpp index 229bc595f..0c446bd6c 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/xit.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/xit.cpp @@ -54,7 +54,6 @@ SyntaxDefinition& addXit() { {} } ); - sd.setFoldRangeType( FoldRangeType::Markdown ); return sd; } diff --git a/src/tests/unit_tests/regex_tests.cpp b/src/tests/unit_tests/regex_tests.cpp index 21c4abef3..849697b4d 100644 --- a/src/tests/unit_tests/regex_tests.cpp +++ b/src/tests/unit_tests/regex_tests.cpp @@ -57,6 +57,53 @@ UTEST( RegEx, captures ) { RegExCache::destroySingleton(); } +UTEST( PatternMatcher, matchIterationAndGroups ) { + RegEx regex( "(\\d+)" ); + std::string text = "12, 345"; + auto match = regex.gmatch( text ); + + ASSERT_TRUE( match.matches() ); + EXPECT_TRUE( match.group( 0 ) == "12" ); + EXPECT_TRUE( match.group( 1 ) == "12" ); + + match.next(); + ASSERT_TRUE( match.matches() ); + EXPECT_TRUE( match.group( 0 ) == "345" ); + EXPECT_TRUE( match.group( 1 ) == "345" ); + + match.next(); + EXPECT_FALSE( match.matches() ); + RegExCache::destroySingleton(); +} + +UTEST( PatternMatcher, matchCopyAndAssignment ) { + RegEx regex( "(\\d+)" ); + std::string text = "value 42"; + auto match = regex.gmatch( text ); + ASSERT_TRUE( match.matches() ); + + auto copy = match; + EXPECT_TRUE( copy.group( 0 ) == "42" ); + EXPECT_TRUE( copy.group( 1 ) == "42" ); + + std::string otherText = "no match"; + auto assigned = regex.gmatch( otherText ); + assigned = match; + EXPECT_TRUE( assigned.group( 0 ) == "42" ); + EXPECT_TRUE( assigned.group( 1 ) == "42" ); + RegExCache::destroySingleton(); +} + +UTEST( PatternMatcher, matchOwnsTemporaryPattern ) { + std::string text = "value 42"; + auto match = RegEx( "(\\d+)" ).gmatch( text ); + + ASSERT_TRUE( match.matches() ); + EXPECT_TRUE( match.group( 0 ) == "42" ); + EXPECT_TRUE( match.group( 1 ) == "42" ); + RegExCache::destroySingleton(); +} + UTEST( RegEx, TextDocument ) { TextDocument doc; doc.textInput( "This number is 42.\nThe number is 23.\n" ); diff --git a/src/tests/unit_tests/stringsoperations_tests.cpp b/src/tests/unit_tests/stringsoperations_tests.cpp index 67795dc73..23df34ca1 100644 --- a/src/tests/unit_tests/stringsoperations_tests.cpp +++ b/src/tests/unit_tests/stringsoperations_tests.cpp @@ -1,13 +1,71 @@ #include "utest.h" +#include #include #include #include +#include using namespace std::literals; using namespace EE; using namespace EE::System; +namespace { + +class ScopedEnvironmentVariable { + public: + ScopedEnvironmentVariable( const char* name, const std::string& value ) : mName( name ) { + if ( const char* currentValue = std::getenv( name ) ) { + mHadValue = true; + mValue = currentValue; + } + set( value.c_str() ); + } + + ~ScopedEnvironmentVariable() { + if ( mHadValue ) + set( mValue.c_str() ); + else + unset(); + } + + private: + void set( const char* value ) { +#if EE_PLATFORM == EE_PLATFORM_WIN + _putenv_s( mName, value ); +#else + setenv( mName, value, 1 ); +#endif + } + + void unset() { +#if EE_PLATFORM == EE_PLATFORM_WIN + _putenv_s( mName, "" ); +#else + unsetenv( mName ); +#endif + } + + const char* mName; + std::string mValue; + bool mHadValue{ false }; +}; + +class ScopedTestDirectory { + public: + explicit ScopedTestDirectory( std::filesystem::path path ) : mPath( std::move( path ) ) {} + + ~ScopedTestDirectory() { + std::error_code error; + std::filesystem::remove_all( mPath, error ); + } + + private: + std::filesystem::path mPath; +}; + +} // namespace + UTEST( String, countLines ) { EXPECT_EQ( static_cast( 0 ), String::countLines( "" ) ); EXPECT_EQ( static_cast( 1 ), String::countLines( "A" ) ); @@ -30,6 +88,23 @@ UTEST( String, fromStringView ) { EXPECT_EQ( 1234, intValue ); } +UTEST( String, reusableFormattingAndUtf8Assignment ) { + std::string formatted; + formatted.reserve( 128 ); + const char* formattedStorage = formatted.data(); + String::formatTo( formatted, "%s: %d", std::string_view{ "line" }, 42 ); + EXPECT_STREQ( "line: 42", formatted.c_str() ); + EXPECT_EQ( formattedStorage, formatted.data() ); + + String text; + text.reserve( 128 ); + const auto* textStorage = text.getString().data(); + text.assignUtf8( "áβ中" ); + const std::string utf8Text = text.toUtf8(); + EXPECT_STREQ( "áβ中", utf8Text.c_str() ); + EXPECT_EQ( textStorage, text.getString().data() ); +} + UTEST( FileSystem, fileCountLines ) { std::string path = Sys::getTempPath() + "eepp_test_count_lines.txt"; FileSystem::fileWrite( path, "A\nB\nC" ); @@ -57,6 +132,60 @@ UTEST( FileSystem, fileCountLines ) { FileSystem::fileRemove( path ); } +UTEST( Sys, whichUsesPathAndCustomSearchPaths ) { + const std::filesystem::path root = std::filesystem::path( Sys::getTempPath() ) / + ( "eepp-sys-which-" + std::to_string( Sys::getProcessID() ) + + "-" + std::to_string( Sys::getTicks() ) ); + ScopedTestDirectory cleanup( root ); + const std::filesystem::path firstDir = root / "first"; + const std::filesystem::path secondDir = root / "second"; + ASSERT_TRUE( std::filesystem::create_directories( firstDir ) ); + ASSERT_TRUE( std::filesystem::create_directories( secondDir ) ); + +#if EE_PLATFORM == EE_PLATFORM_WIN + static constexpr auto EXECUTABLE_NAME = "eepp-which-probe.EXE"; + static constexpr auto LOOKUP_NAME = "eepp-which-probe"; + static constexpr auto PATH_SEPARATOR = ';'; + ScopedEnvironmentVariable pathExt( "PATHEXT", ".COM;.EXE;.BAT;.CMD" ); +#else + static constexpr auto EXECUTABLE_NAME = "eepp-which-probe"; + static constexpr auto LOOKUP_NAME = EXECUTABLE_NAME; + static constexpr auto PATH_SEPARATOR = ':'; +#endif + + const std::filesystem::path firstExecutable = firstDir / EXECUTABLE_NAME; + const std::filesystem::path secondExecutable = secondDir / EXECUTABLE_NAME; + ASSERT_TRUE( FileSystem::fileWrite( firstExecutable.string(), "first" ) ); + ASSERT_TRUE( FileSystem::fileWrite( secondExecutable.string(), "second" ) ); +#if EE_PLATFORM != EE_PLATFORM_WIN + std::error_code permissionError; + const auto executablePermissions = std::filesystem::perms::owner_exec | + std::filesystem::perms::group_exec | + std::filesystem::perms::others_exec; + std::filesystem::permissions( firstExecutable, executablePermissions, + std::filesystem::perm_options::add, permissionError ); + ASSERT_FALSE( permissionError ); + std::filesystem::permissions( secondExecutable, executablePermissions, + std::filesystem::perm_options::add, permissionError ); + ASSERT_FALSE( permissionError ); +#endif + + const std::string path = firstDir.string() + PATH_SEPARATOR + secondDir.string(); + ScopedEnvironmentVariable scopedPath( "PATH", path ); + + EXPECT_TRUE( Sys::which( LOOKUP_NAME ) == firstExecutable.string() ); + EXPECT_TRUE( Sys::which( firstExecutable.string() ) == firstExecutable.string() ); + EXPECT_TRUE( Sys::which( "eepp-which-missing" ).empty() ); + + FileSystem::fileRemove( firstExecutable.string() ); + EXPECT_TRUE( Sys::which( LOOKUP_NAME ) == secondExecutable.string() ); + + ScopedEnvironmentVariable emptyPath( "PATH", "" ); + EXPECT_TRUE( Sys::which( LOOKUP_NAME ).empty() ); + EXPECT_TRUE( Sys::which( LOOKUP_NAME, { firstDir.string(), secondDir.string() } ) == + secondExecutable.string() ); +} + UTEST( String, isAscii ) { // Empty string EXPECT_TRUE( String::isAscii( String::View( U"" ) ) ); diff --git a/src/tests/unit_tests/textdocument_tests.cpp b/src/tests/unit_tests/textdocument_tests.cpp index a710a89c0..f733eabce 100644 --- a/src/tests/unit_tests/textdocument_tests.cpp +++ b/src/tests/unit_tests/textdocument_tests.cpp @@ -6,6 +6,137 @@ using namespace EE::UI::Doc; using namespace EE::System; +UTEST( TextDocument, insertSingleLineAtDifferentPositions ) { + TextDocument doc; + + TextPosition cursor = doc.insert( 0, { 0, 0 }, "abcd" ); + EXPECT_EQ( 0, cursor.line() ); + EXPECT_EQ( 4, cursor.column() ); + EXPECT_STRINGEQ( "abcd", doc.getText() ); + EXPECT_STRINGEQ( "abcd\n", doc.line( 0 ).getText() ); + EXPECT_EQ( 1u, doc.linesCount() ); + + cursor = doc.insert( 0, { 0, 0 }, "start-" ); + EXPECT_EQ( 0, cursor.line() ); + EXPECT_EQ( 6, cursor.column() ); + EXPECT_STRINGEQ( "start-abcd", doc.getText() ); + + cursor = doc.insert( 0, { 0, 8 }, "middle-" ); + EXPECT_EQ( 0, cursor.line() ); + EXPECT_EQ( 15, cursor.column() ); + EXPECT_STRINGEQ( "start-abmiddle-cd", doc.getText() ); + + cursor = doc.insert( 0, { 0, 17 }, "-end" ); + EXPECT_EQ( 0, cursor.line() ); + EXPECT_EQ( 21, cursor.column() ); + EXPECT_STRINGEQ( "start-abmiddle-cd-end", doc.getText() ); + EXPECT_STRINGEQ( "start-abmiddle-cd-end\n", doc.line( 0 ).getText() ); + EXPECT_EQ( 1u, doc.linesCount() ); +} + +UTEST( TextDocument, insertNewLinesIntoEmptyDocument ) { + { + TextDocument doc; + TextPosition cursor = doc.insert( 0, { 0, 0 }, "alpha\nbeta" ); + EXPECT_EQ( 1, cursor.line() ); + EXPECT_EQ( 4, cursor.column() ); + EXPECT_EQ( 2u, doc.linesCount() ); + EXPECT_STRINGEQ( "alpha\nbeta", doc.getText() ); + EXPECT_STRINGEQ( "alpha\n", doc.line( 0 ).getText() ); + EXPECT_STRINGEQ( "beta\n", doc.line( 1 ).getText() ); + } + + { + TextDocument doc; + TextPosition cursor = doc.insert( 0, { 0, 0 }, "\nleading" ); + EXPECT_EQ( 1, cursor.line() ); + EXPECT_EQ( 7, cursor.column() ); + EXPECT_EQ( 2u, doc.linesCount() ); + EXPECT_STRINGEQ( "\nleading", doc.getText() ); + EXPECT_STRINGEQ( "\n", doc.line( 0 ).getText() ); + EXPECT_STRINGEQ( "leading\n", doc.line( 1 ).getText() ); + } + + { + TextDocument doc; + TextPosition cursor = doc.insert( 0, { 0, 0 }, "trailing\n" ); + EXPECT_EQ( 1, cursor.line() ); + EXPECT_EQ( 0, cursor.column() ); + EXPECT_EQ( 2u, doc.linesCount() ); + EXPECT_STRINGEQ( "trailing\n", doc.getText() ); + EXPECT_STRINGEQ( "trailing\n", doc.line( 0 ).getText() ); + EXPECT_STRINGEQ( "\n", doc.line( 1 ).getText() ); + } + + { + TextDocument doc; + TextPosition cursor = doc.insert( 0, { 0, 0 }, "a\n\nb" ); + EXPECT_EQ( 2, cursor.line() ); + EXPECT_EQ( 1, cursor.column() ); + EXPECT_EQ( 3u, doc.linesCount() ); + EXPECT_STRINGEQ( "a\n\nb", doc.getText() ); + EXPECT_STRINGEQ( "a\n", doc.line( 0 ).getText() ); + EXPECT_STRINGEQ( "\n", doc.line( 1 ).getText() ); + EXPECT_STRINGEQ( "b\n", doc.line( 2 ).getText() ); + } +} + +UTEST( TextDocument, insertIntoExistingLines ) { + TextDocument doc; + doc.insert( 0, { 0, 0 }, "first\nsecond\nthird" ); + + TextPosition cursor = doc.insert( 0, { 1, 3 }, "!" ); + EXPECT_EQ( 1, cursor.line() ); + EXPECT_EQ( 4, cursor.column() ); + EXPECT_EQ( 3u, doc.linesCount() ); + EXPECT_STRINGEQ( "first\nsec!ond\nthird", doc.getText() ); + EXPECT_STRINGEQ( "sec!ond\n", doc.line( 1 ).getText() ); + + cursor = doc.insert( 0, { 1, 4 }, "X\nY" ); + EXPECT_EQ( 2, cursor.line() ); + EXPECT_EQ( 1, cursor.column() ); + EXPECT_EQ( 4u, doc.linesCount() ); + EXPECT_STRINGEQ( "first\nsec!X\nYond\nthird", doc.getText() ); + EXPECT_STRINGEQ( "sec!X\n", doc.line( 1 ).getText() ); + EXPECT_STRINGEQ( "Yond\n", doc.line( 2 ).getText() ); + + cursor = doc.insert( 0, { 2, 0 }, "\n" ); + EXPECT_EQ( 3, cursor.line() ); + EXPECT_EQ( 0, cursor.column() ); + EXPECT_EQ( 5u, doc.linesCount() ); + EXPECT_STRINGEQ( "first\nsec!X\n\nYond\nthird", doc.getText() ); + EXPECT_STRINGEQ( "\n", doc.line( 2 ).getText() ); + EXPECT_STRINGEQ( "Yond\n", doc.line( 3 ).getText() ); +} + +UTEST( TextDocument, insertEmptyTextDoesNothing ) { + TextDocument doc; + doc.insert( 0, { 0, 0 }, "content" ); + + TextPosition cursor = doc.insert( 0, { 0, 3 }, "" ); + EXPECT_EQ( 0, cursor.line() ); + EXPECT_EQ( 3, cursor.column() ); + EXPECT_EQ( 1u, doc.linesCount() ); + EXPECT_STRINGEQ( "content", doc.getText() ); + EXPECT_STRINGEQ( "content\n", doc.line( 0 ).getText() ); +} + +UTEST( TextDocument, getTextToBuffer ) { + TextDocument doc; + doc.insert( 0, { 0, 0 }, "first line\nsecond line\nthird line" ); + String buffer( "previous contents that should be replaced" ); + + doc.getTextToBuffer( { { 0, 6 }, { 0, 10 } }, buffer ); + EXPECT_STRINGEQ( "line", buffer ); + + doc.getTextToBuffer( { { 2, 5 }, { 0, 6 } }, buffer ); + EXPECT_STRINGEQ( "line\nsecond line\nthird", buffer ); + EXPECT_STRINGEQ( doc.getText( { { 0, 6 }, { 2, 5 } } ), buffer ); + + doc.getTextToBuffer( { { 1, 3 }, { 1, 3 } }, buffer ); + EXPECT_TRUE( buffer.empty() ); +} + UTEST( TextDocument, multicursor ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); TextDocument doc; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 7b9707242..f48f033b6 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1568,14 +1568,15 @@ void App::updateDocInfo( TextDocument& doc ) { mDocInfo && mSplitter->curEditorExistsAndFocused() ) { mDocInfo->setVisible( true ); updateDocInfoLocation(); - String infoStr( String::format( - "%s: %lld / %zu %s: %lld %s %s%s %s", i18n( "line_abbr", "line" ).toUtf8(), - doc.getSelection().start().line() + 1, doc.linesCount(), - i18n( "col_abbr", "col" ).toUtf8(), mSplitter->getCurEditor()->getCurrentColumnCount(), - doc.getSyntaxDefinition().getLanguageName(), - TextFormat::encodingToString( doc.getEncoding() ), doc.isBOM() ? " (with BOM)"sv : ""sv, - TextFormat::lineEndingToString( doc.getLineEnding() ) ) ); - mDocInfo->debounce( [this, infoStr] { mDocInfo->setText( infoStr ); }, Time::Zero, + String::formatTo( mDocInfoUtf8Buffer, "%s: %lld / %zu %s: %lld %s %s%s %s", + mDocInfoLineAbbr, doc.getSelection().start().line() + 1, doc.linesCount(), + mDocInfoColAbbr, mSplitter->getCurEditor()->getCurrentColumnCount(), + doc.getSyntaxDefinition().getLanguageName(), + TextFormat::encodingToString( doc.getEncoding() ), + doc.isBOM() ? " (with BOM)"sv : ""sv, + TextFormat::lineEndingToString( doc.getLineEnding() ) ); + mDocInfoText.assignUtf8( mDocInfoUtf8Buffer ); + mDocInfo->debounce( [this] { mDocInfo->setText( mDocInfoText ); }, Time::Zero, String::hash( "ecode::doc_info::update" ) ); } } @@ -4872,6 +4873,8 @@ void App::init( InitParameters& params ) { mUISceneNode->bind( "code_container", mBaseLayout ); mUISceneNode->bind( "image_container", mImageLayout ); mUISceneNode->bind( "doc_info", mDocInfo ); + mDocInfoLineAbbr = i18n( "line_abbr", "line" ).toUtf8(); + mDocInfoColAbbr = i18n( "col_abbr", "col" ).toUtf8(); mUISceneNode->bind( "panel", mSidePanel ); mUISceneNode->bind( "project_splitter", mProjectSplitter ); mUISceneNode->bind( "main_menubar", mMenuBar ); diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 5814d6e7f..7ff7b6352 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -668,6 +668,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UILayout* mBaseLayout{ nullptr }; UILayout* mImageLayout{ nullptr }; UITextView* mDocInfo{ nullptr }; + std::string mDocInfoLineAbbr; + std::string mDocInfoColAbbr; + std::string mDocInfoUtf8Buffer; + String mDocInfoText; std::vector mRecentFiles; std::stack mRecentClosedFiles; std::unordered_map mClosedDocumentState; diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp index abb436e57..7d5fa1746 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp @@ -1041,22 +1041,19 @@ bool AutoCompletePlugin::onTextInput( UICodeEditor* editor, const TextInputEvent return false; } -void AutoCompletePlugin::updateDocCache( TextDocument* doc ) { - ScopedOp op( - [this, doc] { - Lock lu( mDocsUpdatingMutex ); - mDocsUpdating[doc] = true; - }, - [this, doc] { - Lock lu( mDocsUpdatingMutex ); - mDocsUpdating[doc] = false; - } ); +void AutoCompletePlugin::updateDocCache( std::shared_ptr doc ) { + TextDocument* docPtr = doc.get(); + ScopedOp op( [] {}, + [this, docPtr] { + Lock lu( mDocsUpdatingMutex ); + mDocsUpdating[docPtr] = false; + } ); Clock clock; std::unordered_map::iterator docCache; { Lock l( mDocMutex ); - docCache = mDocCache.find( doc ); + docCache = mDocCache.find( docPtr ); if ( docCache == mDocCache.end() || mShuttingDown ) return; } @@ -1066,7 +1063,7 @@ void AutoCompletePlugin::updateDocCache( TextDocument* doc ) { { Lock l( mDocMutex ); - docCache = mDocCache.find( doc ); + docCache = mDocCache.find( docPtr ); if ( docCache == mDocCache.end() || mShuttingDown ) return; auto& cache = docCache->second; @@ -1966,14 +1963,20 @@ void AutoCompletePlugin::update( UICodeEditor* editor ) { Lock l( mDocMutex ); for ( auto& doc : mDocs ) { if ( !doc->isLoading() && mDocCache[doc].changeId != doc->getCurrentChangeId() ) { + auto docRef = getPluginContext()->getSplitter()->getTextDocumentRef( doc ); + if ( !docRef ) + continue; { Lock lu( mDocsUpdatingMutex ); - auto du = mDocsUpdating.find( doc ); - // Dont update the document cache if it's still updating the document - if ( du != mDocsUpdating.end() && du->second == true ) + auto& updating = mDocsUpdating[doc]; + // Don't queue another cache update while one is queued or running. + if ( updating ) continue; + updating = true; } - mThreadPool->run( [this, doc] { updateDocCache( doc ); } ); + mThreadPool->run( [this, doc = std::move( docRef )]() mutable { + updateDocCache( std::move( doc ) ); + } ); } } } @@ -2484,13 +2487,13 @@ void AutoCompletePlugin::resetSignatureHelp() { mSignatureHelpEditor = nullptr; } -AutoCompletePlugin::SymbolsList AutoCompletePlugin::getDocumentSymbols( TextDocument* doc ) { +AutoCompletePlugin::SymbolsList +AutoCompletePlugin::getDocumentSymbols( const std::shared_ptr& docRef ) { static constexpr auto MAX_LINE_COUNT = EE_1KB * 10; AutoCompletePlugin::SymbolsList symbols; - std::shared_ptr docRef = - getPluginContext()->getSplitter()->getTextDocumentRef( doc ); // acquire a doc - if ( docRef == nullptr ) + if ( !docRef ) return symbols; + TextDocument* doc = docRef.get(); LuaPattern pattern( mSymbolPattern ); if ( doc->linesCount() == 0 || doc->isHuge() || mShuttingDown ) return symbols; diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp index 87c97627c..2caa3a0de 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp @@ -266,9 +266,9 @@ class AutoCompletePlugin : public Plugin { void updateSuggestions( const std::string& symbol, UICodeEditor* editor ); - SymbolsList getDocumentSymbols( TextDocument* ); + SymbolsList getDocumentSymbols( const std::shared_ptr& doc ); - void updateDocCache( TextDocument* doc ); + void updateDocCache( std::shared_ptr doc ); std::string getPartialSymbol( TextDocument* doc ); diff --git a/src/tools/ecode/plugins/linter/linterplugin.cpp b/src/tools/ecode/plugins/linter/linterplugin.cpp index 89e7305a7..10ad515c3 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.cpp +++ b/src/tools/ecode/plugins/linter/linterplugin.cpp @@ -746,9 +746,9 @@ void LinterPlugin::onUnregister( UICodeEditor* editor ) { void LinterPlugin::update( UICodeEditor* editor ) { std::shared_ptr doc = editor->getDocumentRef(); auto it = mDirtyDoc.find( doc.get() ); - if ( it != mDirtyDoc.end() && it->second->getElapsedTime() >= mDelayTime ) { + if ( it != mDirtyDoc.end() && it->second.getElapsedTime() >= mDelayTime ) { mDirtyDoc.erase( doc.get() ); - mThreadPool->run( [this, doc] { lintDoc( doc ); } ); + mThreadPool->run( [this, doc = std::move( doc )] { lintDoc( doc ); } ); } } @@ -1469,10 +1469,12 @@ Linter LinterPlugin::supportsLinter( std::shared_ptr doc ) { } void LinterPlugin::setDocDirty( TextDocument* doc ) { - mDirtyDoc[doc] = std::make_unique(); + auto [it, inserted] = mDirtyDoc.try_emplace( doc ); + if ( !inserted ) + it->second.restart(); } void LinterPlugin::setDocDirty( UICodeEditor* editor ) { - mDirtyDoc[editor->getDocumentRef().get()] = std::make_unique(); + setDocDirty( editor->getDocumentRef().get() ); } void LinterPlugin::invalidateEditors( TextDocument* doc ) { diff --git a/src/tools/ecode/plugins/linter/linterplugin.hpp b/src/tools/ecode/plugins/linter/linterplugin.hpp index 7dd8df2f0..c1d9976b3 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.hpp +++ b/src/tools/ecode/plugins/linter/linterplugin.hpp @@ -131,7 +131,7 @@ class LinterPlugin : public Plugin { std::unordered_map> mEditors; std::unordered_set mDocs; std::unordered_map mEditorDocs; - std::unordered_map> mDirtyDoc; + std::unordered_map mDirtyDoc; std::unordered_map>> mMatches; Time mDelayTime{ Seconds( 0.5f ) }; Mutex mDocMutex; diff --git a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp index 75e307857..f1a0f4f2b 100644 --- a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp +++ b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp @@ -230,19 +230,21 @@ void SpellCheckerPlugin::onRegisterListeners( UICodeEditor* editor, } void SpellCheckerPlugin::setDocDirty( TextDocument* doc ) { - mDirtyDoc[doc] = std::make_unique(); + auto [it, inserted] = mDirtyDoc.try_emplace( doc ); + if ( !inserted ) + it->second.restart(); } void SpellCheckerPlugin::setDocDirty( UICodeEditor* editor ) { - mDirtyDoc[editor->getDocumentRef().get()] = std::make_unique(); + setDocDirty( editor->getDocumentRef().get() ); } void SpellCheckerPlugin::update( UICodeEditor* editor ) { std::shared_ptr doc = editor->getDocumentRef(); auto it = mDirtyDoc.find( doc.get() ); - if ( it != mDirtyDoc.end() && it->second->getElapsedTime() >= mDelayTime ) { + if ( it != mDirtyDoc.end() && it->second.getElapsedTime() >= mDelayTime ) { mDirtyDoc.erase( doc.get() ); - mThreadPool->run( [this, doc] { spellCheckDoc( doc ); } ); + mThreadPool->run( [this, doc = std::move( doc )] { spellCheckDoc( doc ); } ); } } @@ -254,11 +256,11 @@ void SpellCheckerPlugin::spellCheckDoc( std::shared_ptr doc ) { return; ScopedOp op( - [this, doc]() { + [this] { std::lock_guard l( mWorkMutex ); mWorkersCount++; }, - [this]() { + [this] { { std::lock_guard l( mWorkMutex ); mWorkersCount--; diff --git a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp index 69cb87af1..e3b005255 100644 --- a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp +++ b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp @@ -57,7 +57,7 @@ class SpellCheckerPlugin : public PluginBase { protected: Time mDelayTime{ Seconds( 0.5f ) }; - std::unordered_map> mDirtyDoc; + std::unordered_map mDirtyDoc; std::unordered_map>> mMatches; std::set mLanguagesDisabled; std::set mLSPLanguagesDisabled; diff --git a/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp index 0373ad78c..39a3450b5 100644 --- a/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp +++ b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp @@ -253,7 +253,7 @@ void XMLToolsPlugin::XMLToolsClient::updateMatch( const TextRange& sel ) { const auto& line = mDoc->line( mDoc->getSelection().start().line() ).getText(); if ( mDoc->getSelection().start().column() >= (Int64)line.size() ) return clearMatch(); - auto def = mDoc->getHighlighter()->getSyntaxDefinitionFromTextPosition( sel.start() ); + const auto& def = mDoc->getHighlighter()->getSyntaxDefinitionFromTextPosition( sel.start() ); if ( !def.getAutoCloseXMLTags() ) // getAutoCloseXMLTags means that it supports XML element tags return clearMatch(); TextRange range = mDoc->getWordRangeInPosition( sel.start(), false );