diff --git a/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp b/bin/unit_tests/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp similarity index 100% rename from bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp rename to bin/unit_tests/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp diff --git a/bin/assets/fontrendering/eepp-subpixel-text.webp b/bin/unit_tests/assets/fontrendering/eepp-subpixel-text.webp similarity index 100% rename from bin/assets/fontrendering/eepp-subpixel-text.webp rename to bin/unit_tests/assets/fontrendering/eepp-subpixel-text.webp diff --git a/include/eepp/ui/doc/textdocumentline.hpp b/include/eepp/ui/doc/textdocumentline.hpp index 00ce6fbbe..5c43a20ef 100644 --- a/include/eepp/ui/doc/textdocumentline.hpp +++ b/include/eepp/ui/doc/textdocumentline.hpp @@ -1,6 +1,7 @@ #ifndef EE_UI_DOC_TEXTDOCUMENTLINE_HPP #define EE_UI_DOC_TEXTDOCUMENTLINE_HPP +#include #include #include #include @@ -14,25 +15,54 @@ class EE_API TextDocumentLine { public: TextDocumentLine( const String& text, std::shared_ptr docMutex ) : mText( text ), mDocMutex( docMutex ) { - updateState(); + updateTextHints(); } TextDocumentLine( String&& text, std::shared_ptr docMutex ) : mText( std::move( text ) ), mDocMutex( std::move( docMutex ) ) { - updateState(); + updateTextHints(); } - TextDocumentLine( const TextDocumentLine& ) = default; + TextDocumentLine( const TextDocumentLine& other ) : mDocMutex( other.mDocMutex ) { + ConditionalLock lock( mDocMutex != nullptr, mDocMutex.get() ); + mText = other.mText; + mHash.store( other.mHash.load( std::memory_order_relaxed ), std::memory_order_relaxed ); + mFlags.store( other.mFlags.load( std::memory_order_acquire ), std::memory_order_relaxed ); + } TextDocumentLine( TextDocumentLine&& other ) noexcept : mDocMutex( other.mDocMutex ) { ConditionalLock lock( mDocMutex != nullptr, mDocMutex.get() ); mText = std::move( other.mText ); - mHash = other.mHash; - mFlags = other.mFlags; + mHash.store( other.mHash.load( std::memory_order_relaxed ), std::memory_order_relaxed ); + mFlags.store( other.mFlags.load( std::memory_order_acquire ), std::memory_order_relaxed ); other.mDocMutex.reset(); } - TextDocumentLine& operator=( const TextDocumentLine& ) = default; + TextDocumentLine& operator=( const TextDocumentLine& other ) { + if ( this == &other ) + return *this; + + String text; + String::HashType hash; + Uint32 flags; + auto docMutex = other.mDocMutex; + { + ConditionalLock lock( docMutex != nullptr, docMutex.get() ); + text = other.mText; + hash = other.mHash.load( std::memory_order_relaxed ); + flags = other.mFlags.load( std::memory_order_acquire ); + } + + auto oldDocMutex = mDocMutex; + { + ConditionalLock lock( oldDocMutex != nullptr, oldDocMutex.get() ); + mText = std::move( text ); + mHash.store( hash, std::memory_order_relaxed ); + mFlags.store( flags, std::memory_order_release ); + mDocMutex = std::move( docMutex ); + } + return *this; + } ~TextDocumentLine() { if ( mDocMutex ) { @@ -44,11 +74,13 @@ class EE_API TextDocumentLine { void setText( String&& text ) { if ( mDocMutex ) { Lock lock( *mDocMutex ); + invalidateHash(); mText = std::move( text ); - updateState(); + updateTextHints(); } else { + invalidateHash(); mText = std::move( text ); - updateState(); + updateTextHints(); } } @@ -87,22 +119,26 @@ class EE_API TextDocumentLine { void append( const String& text ) { if ( mDocMutex ) { Lock lock( *mDocMutex ); + invalidateHash(); mText.append( text ); - updateState(); + updateTextHints(); } else { + invalidateHash(); mText.append( text ); - updateState(); + updateTextHints(); } } void insert( std::size_t position, const String& text ) { if ( mDocMutex ) { Lock lock( *mDocMutex ); + invalidateHash(); mText.insert( position, text ); - updateState(); + updateTextHints(); } else { + invalidateHash(); mText.insert( position, text ); - updateState(); + updateTextHints(); } } @@ -130,30 +166,40 @@ class EE_API TextDocumentLine { return mText.size(); } - String::HashType getHash() const { return mHash; } + String::HashType getHash() const { + Uint32 flags = mFlags.load( std::memory_order_acquire ); + if ( flags & HashValid ) + return mHash.load( std::memory_order_relaxed ); - bool isAscii() const { return ( mFlags & TextHints::AllAscii ) != 0; } - - bool isLatin1() const { return ( mFlags & TextHints::AllLatin1 ) != 0; } - - Uint32 getTextHints() const { - if ( mDocMutex ) { - Lock lock( *mDocMutex ); - return mFlags; + ConditionalLock lock( mDocMutex != nullptr, mDocMutex.get() ); + flags = mFlags.load( std::memory_order_acquire ); + if ( !( flags & HashValid ) ) { + mHash.store( mText.getHash(), std::memory_order_relaxed ); + mFlags.store( flags | HashValid, std::memory_order_release ); } - return mFlags; + return mHash.load( std::memory_order_relaxed ); } + bool isAscii() const { + return ( mFlags.load( std::memory_order_acquire ) & TextHints::AllAscii ) != 0; + } + + bool isLatin1() const { + return ( mFlags.load( std::memory_order_acquire ) & TextHints::AllLatin1 ) != 0; + } + + Uint32 getTextHints() const { return mFlags.load( std::memory_order_acquire ) & ~HashValid; } + protected: + static constexpr Uint32 HashValid = 1u << 31; String mText; - String::HashType mHash{ 0 }; - Uint32 mFlags{ 0 }; + mutable std::atomic mHash{ 0 }; + mutable std::atomic mFlags{ 0 }; std::shared_ptr mDocMutex; - void updateState() { - mHash = mText.getHash(); - mFlags = mText.getTextHints(); - } + void invalidateHash() { mFlags.fetch_and( ~HashValid, std::memory_order_release ); } + + void updateTextHints() { mFlags.store( mText.getTextHints(), std::memory_order_release ); } }; }}} // namespace EE::UI::Doc diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp index 5126368e8..3c2854966 100644 --- a/include/eepp/ui/tools/uidiffview.hpp +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -185,7 +185,9 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { void updateButtonsVisibility(); - void createImageViewers(); + UIImageViewer* createImageViewer(); + + void resetImageViewers(); bool loadImageDiffFromPaths( const std::string& oldFilePath, const std::string& newFilePath ); diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index cb9feb963..c64398f4d 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -533,6 +533,8 @@ Uint32 Node::forceTextInput( const TextInputEvent& event ) { } const Vector2f& Node::getScreenPos() const { + if ( mNodeFlags & NODE_FLAG_POSITION_DIRTY ) + const_cast( this )->updateScreenPos(); return mScreenPos; } @@ -1195,6 +1197,12 @@ void Node::updateScreenPos() { if ( !( mNodeFlags & NODE_FLAG_POSITION_DIRTY ) ) return; + // Keep the dirty-tree invariant intact when a descendant is queried before its ancestors are + // drawn. Once this node becomes position-clean, every ancestor must also be position-clean; + // otherwise a later ancestor move can early-out in setDirty() without reaching this node. + if ( mParentNode && ( mParentNode->mNodeFlags & NODE_FLAG_POSITION_DIRTY ) ) + mParentNode->updateScreenPos(); + Vector2f Pos( mPosition ); nodeToWorldTranslation( Pos ); diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 03244cf49..c4e5c1697 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1572,6 +1572,7 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio position = sanitizePosition( position ); size_t lineCount = linesCount(); Int64 linesAdd = 0; + Int64 lastInsertedLineLength = 0; bool multiline = text.find( '\n' ) != String::InvalidPos; { @@ -1596,6 +1597,8 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio for ( Int64 i = 0; i <= linesAdd; ++i ) { size_t newLine = textView.find( '\n', lineStart ); size_t lineEnd = newLine == String::InvalidPos ? textView.size() : newLine + 1; + if ( i == linesAdd ) + lastInsertedLineLength = static_cast( lineEnd - lineStart ); String line( textView.substr( lineStart, lineEnd - lineStart ) ); if ( i == 0 ) line.insert( 0, before ); @@ -1616,7 +1619,12 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio } } - TextPosition cursor = positionOffset( position, text.size() ); + // The multiline construction above already knows the final line and column. Start from that + // line instead of walking every inserted line again, while still letting positionOffset() move + // the endpoint to a grapheme boundary when the inserted text joins the existing suffix. + TextPosition cursor = + multiline ? positionOffset( { position.line() + linesAdd, 0 }, lastInsertedLineLength ) + : positionOffset( position, text.size() ); mUndoStack.pushSelection( undoStack, cursorIdx, mSelection, time ); mUndoStack.pushRemove( undoStack, cursorIdx, { position, cursor }, time ); @@ -4878,6 +4886,9 @@ void TextDocument::clearIndentation() { } void TextDocument::initializeCommands() { + // The built-in document commands and editor-specific commands share this table. Reserve their + // known steady-state capacity so every new editor does not repeatedly grow and rehash it. + mCommands.reserve( 128 ); mCommands["reset-document"] = [this] { reset(); }; mCommands["save-doc"] = [this] { save(); }; mCommands["delete-to-previous-word"] = [this] { deleteToPreviousWord(); }; diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index c9457994a..6dec25f1e 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -494,7 +494,6 @@ UIDiffView::UIDiffView() : createEditor( mEditor, mPlugin ); createEditor( mLeftEditor, mLeftPlugin ); createEditor( mRightEditor, mRightPlugin ); - createImageViewers(); mEditor->on( Event::OnFontChanged, [this]( auto ) { mPlugin->registerUpdate( mEditor ); } ); mLeftEditor->on( Event::OnFontChanged, [this]( auto ) { @@ -507,6 +506,9 @@ UIDiffView::UIDiffView() : mLeftEditor->setFontSize( mRightEditor->getFontSize() ); mLeftPlugin->registerUpdate( mLeftEditor ); } ); + mRightEditor->getVScrollBar()->on( Event::OnSizeChange, [this] ( auto ) { + updateModeButton(); + } ); for ( auto* editor : { mEditor, mLeftEditor, mRightEditor } ) { editor->on( Event::OnSizeChange, [this]( auto ) { onAutoSize(); } ); @@ -552,7 +554,6 @@ UIDiffView::UIDiffView() : mCompleteViewToggle->on( Event::OnSizeChange, [this]( auto ) { updateModeButton(); } ); updateButtonsText(); - updateImagesPosAndSize(); } UIDiffView::~UIDiffView() { @@ -589,33 +590,31 @@ void UIDiffView::createEditor( UICodeEditor*& editor, editor->registerPlugin( plugin.get() ); } -void UIDiffView::createImageViewers() { - const auto initImageView = [this] { - auto iv = UIImageViewer::New(); - iv->setParent( this ); - iv->setVisible( false ); - iv->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); - iv->setDisplayOptions( UIImageViewer::DisplayDimensions ); - iv->setUseNativeImageSize( true ); - return iv; - }; - mLeftImageViewer = initImageView(); - mRightImageViewer = initImageView(); - mDiffImageViewer = initImageView(); +UIImageViewer* UIDiffView::createImageViewer() { + auto* imageViewer = UIImageViewer::New(); + imageViewer->setParent( this ); + imageViewer->setVisible( false ); + imageViewer->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + imageViewer->setDisplayOptions( UIImageViewer::DisplayDimensions ); + imageViewer->setUseNativeImageSize( true ); + return imageViewer; +} + +void UIDiffView::resetImageViewers() { + for ( auto* imageViewer : { mLeftImageViewer, mRightImageViewer, mDiffImageViewer } ) { + if ( imageViewer ) { + imageViewer->reset(); + imageViewer->setVisible( false ); + } + } + mSprite = nullptr; } void UIDiffView::resetToTextDiffView() { mIsImageDiff = false; mImageDiffOldPath.clear(); mImageDiffNewPath.clear(); - if ( mLeftImageViewer ) { - mLeftImageViewer->reset(); - mLeftImageViewer->setVisible( false ); - } - if ( mRightImageViewer ) { - mRightImageViewer->reset(); - mRightImageViewer->setVisible( false ); - } + resetImageViewers(); mEditor->setVisible( mViewMode == ViewMode::Unified ); mLeftEditor->setVisible( mViewMode == ViewMode::SideBySide ); mRightEditor->setVisible( mViewMode == ViewMode::SideBySide ); @@ -628,8 +627,8 @@ void UIDiffView::setViewMode( ViewMode mode ) { mViewMode = mode; if ( mIsImageDiff ) { - onSizeChange(); updateImageDiffView(); + onSizeChange(); updateButtonsText(); return; } @@ -668,6 +667,7 @@ void UIDiffView::setCompleteView( bool complete ) { updateButtonsText(); if ( mIsImageDiff ) { updateImageDiffView(); + onSizeChange(); return; } updateEditorsText(); @@ -696,7 +696,8 @@ void UIDiffView::updateModeButton() { auto vmargin = mHeadersVisible ? emptySpace * 0.5f : margin; Float currentX = getPixelsSize().getWidth() - margin; - currentX -= mRightEditor->getVScrollBar()->getPixelsSize().getWidth(); + Float vScrollWidth = mRightEditor->getVScrollBar()->getPixelsSize().getWidth(); + currentX -= vScrollWidth; if ( mViewModeToggleVisible && mModeToggle ) { currentX -= mModeToggle->getPixelsSize().getWidth(); @@ -734,7 +735,7 @@ void UIDiffView::onAutoSize() { : mLeftEditor->getPixelsSize().getHeight() ) ); } - if ( mIsImageDiff && mLeftImageViewer && mRightImageViewer && mDiffImageViewer ) { + if ( mIsImageDiff ) { bool displayDiffImage; bool displayLeftImage; imageDisplayState( displayDiffImage, displayLeftImage ); @@ -745,39 +746,47 @@ void UIDiffView::onAutoSize() { return 0; }; - height = std::max( height, viewImageHeight( mLeftImageViewer ) ); - height = std::max( height, viewImageHeight( mRightImageViewer ) ); - - if ( displayDiffImage ) + if ( displayDiffImage ) { height = std::ceil( std::max( height, viewImageHeight( mDiffImageViewer ) ) ); + } else { + if ( displayLeftImage ) + height = std::max( height, viewImageHeight( mLeftImageViewer ) ); + height = std::max( height, viewImageHeight( mRightImageViewer ) ); + } setPixelsSize( getPixelsSize().getWidth(), height ); } } void UIDiffView::updateImagesPosAndSize() { + if ( !mIsImageDiff ) + return; + const Sizef size( getPixelsSize() ); bool displayDiffImage; bool displayLeftImage; imageDisplayState( displayDiffImage, displayLeftImage ); - mLeftImageViewer->setVisible( true ); - mLeftImageViewer->setPixelsPosition( 0, 0 ); - mLeftImageViewer->setPixelsSize( { size.getWidth() * 0.5f, size.getHeight() } ); - setImageViewerImageSize( mLeftImageViewer ); + if ( mLeftImageViewer ) { + mLeftImageViewer->setPixelsPosition( 0, 0 ); + mLeftImageViewer->setPixelsSize( { size.getWidth() * 0.5f, size.getHeight() } ); + setImageViewerImageSize( mLeftImageViewer ); + } - mRightImageViewer->setVisible( true ); - mRightImageViewer->setPixelsSize( - displayLeftImage ? Sizef{ size.getWidth() * 0.5f, size.getHeight() } : size ); - mRightImageViewer->setPixelsPosition( - displayLeftImage ? std::floor( size.getWidth() * 0.5f ) : 0.f, 0.f ); - setImageViewerImageSize( mRightImageViewer ); + if ( mRightImageViewer ) { + mRightImageViewer->setPixelsSize( + displayLeftImage ? Sizef{ size.getWidth() * 0.5f, size.getHeight() } : size ); + mRightImageViewer->setPixelsPosition( + displayLeftImage ? std::floor( size.getWidth() * 0.5f ) : 0.f, 0.f ); + setImageViewerImageSize( mRightImageViewer ); + } - mDiffImageViewer->setVisible( true ); - mDiffImageViewer->setPixelsPosition( 0, 0 ); - mDiffImageViewer->setPixelsSize( size ); - setImageViewerImageSize( mDiffImageViewer ); + if ( mDiffImageViewer ) { + mDiffImageViewer->setPixelsPosition( 0, 0 ); + mDiffImageViewer->setPixelsSize( size ); + setImageViewerImageSize( mDiffImageViewer ); + } onAutoSize(); updateModeButton(); @@ -879,6 +888,7 @@ bool UIDiffView::loadImageDiffFromPaths( const std::string& oldFilePath, if ( !hasOldImage && !hasNewImage ) return false; + resetImageViewers(); mLines.clear(); mViewLines.clear(); mSyntaxDef.reset(); @@ -902,8 +912,8 @@ bool UIDiffView::loadImageDiffFromPaths( const std::string& oldFilePath, setCompleteViewToggleVisible( !mImageDiffOldPath.empty() && !mImageDiffNewPath.empty() ); updateButtonsText(); - onSizeChange(); updateImageDiffView(); + onSizeChange(); return true; } @@ -933,18 +943,23 @@ void UIDiffView::updateImageDiffView() { mLeftEditor->setVisible( false ); mRightEditor->setVisible( false ); - mDiffImageViewer->setVisible( false ); + if ( mDiffImageViewer ) + mDiffImageViewer->setVisible( false ); if ( displayLeftImage ) { + if ( !mLeftImageViewer ) + mLeftImageViewer = createImageViewer(); mLeftImageViewer->setVisible( true ); if ( !mLeftImageViewer->hasImage() ) mLeftImageViewer->loadImageAsync( mImageDiffOldPath, false, false ); - } else { + } else if ( mLeftImageViewer ) { mLeftImageViewer->reset(); mLeftImageViewer->setVisible( false ); } if ( displayDiffImage ) { + if ( !mDiffImageViewer ) + mDiffImageViewer = createImageViewer(); if ( nullptr == mSprite ) { Image oldImage( mImageDiffOldPath, 4 ); Image newImage( mImageDiffNewPath, 4 ); @@ -955,8 +970,10 @@ void UIDiffView::updateImageDiffView() { } } - mLeftImageViewer->setVisible( false ); - mRightImageViewer->setVisible( false ); + if ( mLeftImageViewer ) + mLeftImageViewer->setVisible( false ); + if ( mRightImageViewer ) + mRightImageViewer->setVisible( false ); mDiffImageViewer->setVisible( true ); return; } @@ -965,10 +982,12 @@ void UIDiffView::updateImageDiffView() { mImageDiffNewPath.empty() ? mImageDiffOldPath : mImageDiffNewPath; if ( !displayPath.empty() ) { + if ( !mRightImageViewer ) + mRightImageViewer = createImageViewer(); mRightImageViewer->setVisible( true ); if ( !mRightImageViewer->hasImage() ) mRightImageViewer->loadImageAsync( displayPath, false, false ); - } else { + } else if ( mRightImageViewer ) { mRightImageViewer->reset(); mRightImageViewer->setVisible( false ); } diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index f16fbca86..31fc8227f 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -49,7 +49,9 @@ UICodeEditor* UICodeEditor::NewOpt( const bool& autoRegisterBaseCommands, return eeNew( UICodeEditor, ( autoRegisterBaseCommands, autoRegisterBaseKeybindings ) ); } -const std::map UICodeEditor::getDefaultKeybindings() { +using CodeEditorKeyBindingMap = std::map; + +static CodeEditorKeyBindingMap createDefaultCodeEditorKeybindings() { return { { { KEY_BACKSPACE, KeyMod::getDefaultModifier() }, "delete-to-previous-word" }, { { KEY_BACKSPACE, KEYMOD_SHIFT }, "delete-to-previous-char" }, @@ -130,6 +132,32 @@ const std::map UICodeEditor::getDefaultKeybi }; } +static std::shared_ptr getCachedDefaultCodeEditorKeybindings() { + struct Cache { + Mutex mutex; + Uint32 defaultModifier{ 0 }; + Uint32 secondaryModifier{ 0 }; + std::shared_ptr bindings; + }; + static Cache cache; + + const Uint32 defaultModifier = KeyMod::getDefaultModifier(); + const Uint32 secondaryModifier = KeyMod::getDefaultSecondaryModifier(); + Lock lock( cache.mutex ); + if ( !cache.bindings || cache.defaultModifier != defaultModifier || + cache.secondaryModifier != secondaryModifier ) { + cache.bindings = + std::make_shared( createDefaultCodeEditorKeybindings() ); + cache.defaultModifier = defaultModifier; + cache.secondaryModifier = secondaryModifier; + } + return cache.bindings; +} + +const std::map UICodeEditor::getDefaultKeybindings() { + return *getCachedDefaultCodeEditorKeybindings(); +} + const MouseBindings::ShortcutMap UICodeEditor::getDefaultMousebindings() { return { { { MouseAction::Down, EE_BUTTON_LMASK, KeyMod::getDefaultModifier() }, "add-cursor-at-mouse-position" }, @@ -2311,10 +2339,41 @@ void UICodeEditor::onDocumentTextChanged( const DocumentContentChange& change ) mDocView.updateCache( change.range.start().line(), change.range.start().line(), 0 ); if ( !change.text.empty() && !mDocView.isWrapEnabled() ) { - auto range = findLongestLineInRange( change.range ); - if ( range.second > mLongestLineWidth ) { - mLongestLineIndex = range.first; - mLongestLineWidth = range.second; + static constexpr Int64 MAX_SYNCHRONOUS_LONGEST_LINE_SCAN = 64; + static constexpr std::size_t MAX_SYNCHRONOUS_LONGEST_LINE_LENGTH = 4096; + bool largeChange = change.text.size() > MAX_SYNCHRONOUS_LONGEST_LINE_LENGTH || + change.range.end().line() - change.range.start().line() + 1 > + MAX_SYNCHRONOUS_LONGEST_LINE_SCAN; + if ( !largeChange ) { + for ( Int64 line = change.range.start().line(); line <= change.range.end().line(); + ++line ) { + if ( mDoc->getLineLength( line ) > MAX_SYNCHRONOUS_LONGEST_LINE_LENGTH ) { + largeChange = true; + break; + } + } + } + if ( !largeChange ) { + Int64 insertedLines = 1; + for ( auto chr : change.text ) { + if ( chr == '\n' && ++insertedLines > MAX_SYNCHRONOUS_LONGEST_LINE_SCAN ) { + largeChange = true; + break; + } + } + } + if ( !largeChange ) { + auto range = findLongestLineInRange( change.range ); + if ( range.second > mLongestLineWidth ) { + mLongestLineIndex = range.first; + mLongestLineWidth = range.second; + } + } else { + // Unbounded synchronous measurement makes large pastes and document loads block on + // too many lines or one extremely long line, including in editors that are currently + // hidden. Reuse the existing debounced full-document update; visible editors will + // measure once after the change. + invalidateLongestLineWidth(); } } else { invalidateLongestLineWidth(); @@ -4795,6 +4854,7 @@ void UICodeEditor::drawLineEndings( const DocumentLineRange& lineRange, const Ve } void UICodeEditor::registerCommands() { + mUnlockedCmd.reserve( 8 ); mDoc->setCommand( "move-to-previous-line", []( Client* client ) { static_cast( client )->moveToPreviousLine(); } ); @@ -4947,7 +5007,9 @@ Tools::UIDocFindReplace* UICodeEditor::getFindReplace() { } void UICodeEditor::registerKeybindings() { - mKeyBindings.addKeybinds( getDefaultKeybindings() ); + // Editors keep mutable bindings, but the immutable defaults only need to be constructed once + // for each configured default-modifier pair. + mKeyBindings.addKeybinds( *getCachedDefaultCodeEditorKeybindings() ); mMouseBindings.addMousebinds( getDefaultMousebindings() ); } diff --git a/src/tests/unit_tests/compareimages.hpp b/src/tests/unit_tests/compareimages.hpp index cf4f7e407..6195fc2b6 100644 --- a/src/tests/unit_tests/compareimages.hpp +++ b/src/tests/unit_tests/compareimages.hpp @@ -22,7 +22,8 @@ static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Wi int allowedNumDifferentPixels = 0 ) { auto saveType = Image::SaveType::WEBP; auto saveExt( Image::saveTypeToExtension( saveType ) ); - std::string expectedImagePath( "assets/" + imagesFolder + "/" + imageName + "." + saveExt ); + std::string expectedImagePath( Sys::getProcessPath() + "assets/" + imagesFolder + "/" + + imageName + "." + saveExt ); Image::FormatConfiguration fconf; fconf.webpSaveLossless( true ); diff --git a/src/tests/unit_tests/gitconflict_tests.cpp b/src/tests/unit_tests/gitconflict_tests.cpp index eb6f3d3c6..4d73ab5b9 100644 --- a/src/tests/unit_tests/gitconflict_tests.cpp +++ b/src/tests/unit_tests/gitconflict_tests.cpp @@ -317,3 +317,42 @@ UTEST( GitHistory, ListsChangedFilesAndLoadsFirstParentDiff ) { EXPECT_NE( std::string::npos, diff.result.find( "-before" ) ); EXPECT_NE( std::string::npos, diff.result.find( "+after" ) ); } + +UTEST( GitStatus, PreservesSuffixOfRenamedDirectoryNumstatPaths ) { + const std::string gitPath = Sys::which( "git" ); + if ( gitPath.empty() ) + UTEST_SKIP( "Git is not installed" ); + GitTempDirectory temp; + Git git( temp.path.string(), gitPath ); + std::string output; + auto run = [&]( std::vector args ) { + output.clear(); + return git.git( args, temp.path.string(), output ); + }; + ASSERT_EQ( EXIT_SUCCESS, run( { "init", "-b", "main" } ) ); + ASSERT_EQ( EXIT_SUCCESS, run( { "config", "user.name", "Status Tester" } ) ); + ASSERT_EQ( EXIT_SUCCESS, run( { "config", "user.email", "status@example.invalid" } ) ); + ASSERT_TRUE( FileSystem::makeDir( ( temp.path / "bin/assets/fontrendering" ).string(), true ) ); + ASSERT_TRUE( FileSystem::fileWrite( + ( temp.path / "bin/assets/fontrendering/image.webp" ).string(), "image contents" ) ); + ASSERT_EQ( EXIT_SUCCESS, run( { "add", "bin/assets/fontrendering/image.webp" } ) ); + ASSERT_EQ( EXIT_SUCCESS, run( { "commit", "-m", "base" } ) ); + ASSERT_TRUE( FileSystem::makeDir( ( temp.path / "bin/unit_tests" ).string(), true ) ); + ASSERT_EQ( EXIT_SUCCESS, run( { "mv", "bin/assets", "bin/unit_tests/assets" } ) ); + + auto status = git.status( false, temp.path.string() ); + size_t staged = 0; + size_t untracked = 0; + for ( const auto& [_, files] : status.files ) { + for ( const auto& file : files ) { + if ( file.report.type == Git::GitStatusType::Staged ) { + ++staged; + EXPECT_STREQ( "bin/unit_tests/assets/fontrendering/image.webp", file.file.c_str() ); + } else if ( file.report.type == Git::GitStatusType::Untracked ) { + ++untracked; + } + } + } + EXPECT_EQ( 1u, staged ); + EXPECT_EQ( 0u, untracked ); +} diff --git a/src/tests/unit_tests/textdocument_tests.cpp b/src/tests/unit_tests/textdocument_tests.cpp index 6e00ed5ad..8656117b4 100644 --- a/src/tests/unit_tests/textdocument_tests.cpp +++ b/src/tests/unit_tests/textdocument_tests.cpp @@ -148,6 +148,60 @@ UTEST( TextDocument, insertLargeMultilineBlock ) { EXPECT_STRINGEQ( "tail\n", doc.line( insertedLineCount + 2 ).getText() ); } +UTEST( TextDocument, multilineInsertCursorEndsBeforeExistingSuffix ) { + TextDocument doc; + doc.insert( 0, { 0, 0 }, "prefix-suffix" ); + + TextPosition cursor = + doc.insert( 0, { 0, 7 }, String::fromUtf8( std::string_view{ "alpha\nβeta\n" } ) ); + + EXPECT_EQ( 2, cursor.line() ); + EXPECT_EQ( 0, cursor.column() ); + EXPECT_STRINGEQ( "prefix-alpha\nβeta\nsuffix", doc.getText() ); + + cursor = doc.insert( 0, cursor, String::fromUtf8( std::string_view{ "γ\nδ" } ) ); + EXPECT_EQ( 3, cursor.line() ); + EXPECT_EQ( 1, cursor.column() ); + EXPECT_STRINGEQ( "prefix-alpha\nβeta\nγ\nδsuffix", doc.getText() ); + + TextDocument graphemeDoc; + graphemeDoc.insert( 0, { 0, 0 }, + String::fromUtf8( std::string_view{ "prefix-\u0301suffix" } ) ); + cursor = graphemeDoc.insert( 0, { 0, 7 }, "line\nA" ); + EXPECT_EQ( 1, cursor.line() ); + EXPECT_EQ( 2, cursor.column() ); + EXPECT_STRINGEQ( "prefix-line\nA\u0301suffix", graphemeDoc.getText() ); +} + +UTEST( TextDocument, lineHashRemainsStableAndTracksMutations ) { + TextDocumentLine emptyLine( "", nullptr ); + EXPECT_EQ( String( "" ).getHash(), emptyLine.getHash() ); + + TextDocument doc; + doc.insert( 0, { 0, 0 }, String::fromUtf8( std::string_view{ "alpha\nβeta" } ) ); + + String firstLine( "alpha\n" ); + String secondLine( String::fromUtf8( std::string_view{ "βeta\n" } ) ); + EXPECT_EQ( firstLine.getHash(), doc.getLineHash( 0 ) ); + EXPECT_EQ( secondLine.getHash(), doc.getLineHash( 1 ) ); + EXPECT_EQ( secondLine.getHash(), doc.getLineHash( 1 ) ); + + doc.insert( 0, { 1, 1 }, "!" ); + secondLine.insert( 1, "!" ); + EXPECT_EQ( secondLine.getHash(), doc.getLineHash( 1 ) ); + + auto lines = doc.getLines(); + ASSERT_EQ( size_t{ 2 }, lines.size() ); + EXPECT_EQ( doc.getLineHash( 0 ), lines[0].getHash() ); + EXPECT_EQ( doc.getLineHash( 1 ), lines[1].getHash() ); + TextDocumentLine assigned( "", nullptr ); + assigned = lines[0]; + EXPECT_EQ( firstLine.getHash(), assigned.getHash() ); + + TextDocumentLine moved( std::move( lines[1] ) ); + EXPECT_EQ( secondLine.getHash(), moved.getHash() ); +} + UTEST( TextDocument, insertEmptyTextDoesNothing ) { TextDocument doc; doc.insert( 0, { 0, 0 }, "content" ); diff --git a/src/tests/unit_tests/uicodeeditor_tests.cpp b/src/tests/unit_tests/uicodeeditor_tests.cpp index 7b9e20286..4c3601438 100644 --- a/src/tests/unit_tests/uicodeeditor_tests.cpp +++ b/src/tests/unit_tests/uicodeeditor_tests.cpp @@ -16,6 +16,15 @@ using namespace EE::UI::Doc; using namespace EE::Scene; using namespace EE::System; +class TestableCodeEditor : public UICodeEditor { + public: + TestableCodeEditor() : UICodeEditor() {} + + bool isLongestLineWidthDirtyForTest() const { return mLongestLineWidthDirty; } + + void clearLongestLineWidthDirtyForTest() { mLongestLineWidthDirty = false; } +}; + UTEST( MainThreadLifetime, InvalidatedCallbacksDoNotRun ) { UIApplication app( WindowSettings{ 320, 240, "eepp - main thread lifetime test" } ); int owner = 42; @@ -53,6 +62,67 @@ UTEST( MainThreadLifetime, DispatcherCanBeAttachedAfterConstruction ) { EXPECT_TRUE( called ); } +UTEST( UICodeEditor, DefersLongestLineMeasurementForLargeChanges ) { + UIApplication app( WindowSettings{ 320, 240, "eepp - deferred longest line test" } ); + auto* editor = eeNew( TestableCodeEditor, () ); + editor->setPixelsSize( 160, 80 ); + editor->setParent( app.getUI()->getRoot() ); + editor->setFindLongestLineWidthUpdateFrequency( Time::Zero ); + app.getUI()->flushDirtyStyleAndLayout(); + editor->setLineWrapMode( LineWrapMode::NoWrap ); + EXPECT_EQ( LineWrapMode::NoWrap, editor->getLineWrapMode() ); + editor->clearLongestLineWidthDirtyForTest(); + + String text; + for ( size_t i = 0; i < 64; ++i ) + text += i == 32 ? String( 256, 'x' ) + "\n" : "short\n"; + editor->getDocument().textInput( text ); + + EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); + + editor->clearLongestLineWidthDirtyForTest(); + editor->getDocument().insert( 0, { 0, 0 }, "!" ); + EXPECT_FALSE( editor->isLongestLineWidthDirtyForTest() ); + + editor->getDocument().reset(); + editor->clearLongestLineWidthDirtyForTest(); + editor->getDocument().textInput( String( 4097, 'x' ) ); + EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); + + editor->clearLongestLineWidthDirtyForTest(); + editor->getDocument().insert( 0, { 0, 0 }, "!" ); + EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); + + eeDelete( editor ); +} + +UTEST( UICodeEditor, DefaultKeybindingCacheTracksConfiguredModifiers ) { + const Uint32 originalDefaultModifier = KeyMod::getDefaultModifier(); + const Uint32 originalSecondaryModifier = KeyMod::getDefaultSecondaryModifier(); + + auto defaultBindings = UICodeEditor::getDefaultKeybindings(); + auto copy = defaultBindings.find( { KEY_C, originalDefaultModifier } ); + EXPECT_TRUE( copy != defaultBindings.end() ); + if ( copy != defaultBindings.end() ) + EXPECT_STREQ( "copy", copy->second.c_str() ); + + KeyMod::setDefaultModifier( KEYMOD_LALT ); + KeyMod::setDefaultSecondaryModifier( KEYMOD_META ); + auto reconfiguredBindings = UICodeEditor::getDefaultKeybindings(); + copy = reconfiguredBindings.find( { KEY_C, KEYMOD_LALT } ); + EXPECT_TRUE( copy != reconfiguredBindings.end() ); + if ( copy != reconfiguredBindings.end() ) + EXPECT_STREQ( "copy", copy->second.c_str() ); + + KeyMod::setDefaultModifier( originalDefaultModifier ); + KeyMod::setDefaultSecondaryModifier( originalSecondaryModifier ); + auto restoredBindings = UICodeEditor::getDefaultKeybindings(); + copy = restoredBindings.find( { KEY_C, originalDefaultModifier } ); + EXPECT_TRUE( copy != restoredBindings.end() ); + if ( copy != restoredBindings.end() ) + EXPECT_STREQ( "copy", copy->second.c_str() ); +} + static const std::string userCode = R"objcpp(#import "common.h" #import #import diff --git a/src/tests/unit_tests/uidiffview_tests.cpp b/src/tests/unit_tests/uidiffview_tests.cpp index ff4a1ad64..7a5da1e36 100644 --- a/src/tests/unit_tests/uidiffview_tests.cpp +++ b/src/tests/unit_tests/uidiffview_tests.cpp @@ -16,11 +16,13 @@ using namespace EE::UI::Tools; UTEST( UIDiffView, LoadFromStringsAndVerifyDiffLines ) { UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } ); UIDiffView* diffView = UIDiffView::New(); + EXPECT_TRUE( diffView->findAllByType( UI_TYPE_IMAGE_VIEWER ).empty() ); std::string oldText = "line 1\nline 2\nline 3\nline 4"; std::string newText = "line 1\nline 2 changed\nline 3\nline 4 added\nline 5"; diffView->loadFromStrings( oldText, newText ); + EXPECT_TRUE( diffView->findAllByType( UI_TYPE_IMAGE_VIEWER ).empty() ); const auto& lines = diffView->getDiffLines(); @@ -158,6 +160,7 @@ UTEST( UIDiffView, MultiFileViewerHandlesLargePatches ) { auto* viewer = UIDiffView::NewMultiFileDiffViewer( patchText ); EXPECT_EQ( fileCount, viewer->findAllByType( UI_TYPE_DIFF_VIEW ).size() ); + EXPECT_TRUE( viewer->findAllByType( UI_TYPE_IMAGE_VIEWER ).empty() ); EXPECT_FALSE( viewer->getUISceneNode()->isLoading() ); eeDelete( viewer ); @@ -180,6 +183,7 @@ UTEST( UIDiffView, LoadFromFileImageDiffUsesImageViewers ) { EXPECT_TRUE( diffView->isImageDiff() ); EXPECT_TRUE( diffView->getLeftImageViewer()->isVisible() ); EXPECT_TRUE( diffView->getRightImageViewer()->isVisible() ); + EXPECT_EQ( size_t{ 2 }, diffView->findAllByType( UI_TYPE_IMAGE_VIEWER ).size() ); EXPECT_FALSE( diffView->getEditor()->isVisible() ); EXPECT_FALSE( diffView->getLeftEditor()->isVisible() ); EXPECT_FALSE( diffView->getRightEditor()->isVisible() ); diff --git a/src/tests/unit_tests/uiscenenode_tests.cpp b/src/tests/unit_tests/uiscenenode_tests.cpp index 40d737ad1..b253cc3ff 100644 --- a/src/tests/unit_tests/uiscenenode_tests.cpp +++ b/src/tests/unit_tests/uiscenenode_tests.cpp @@ -29,6 +29,24 @@ UTEST( UISceneNode, CssPointerCursorUsesHandCursor ) { EXPECT_STREQ( Cursor::toName( Cursor::Arrow ), "arrow" ); } +UTEST( Node, DescendantWorldBoundsRefreshAfterDirtyAncestorMoves ) { + Node* parent = Node::New(); + Node* child = Node::New(); + Node* descendant = Node::New(); + child->setParent( parent ); + descendant->setParent( child ); + child->setPosition( 10.f, 0.f ); + descendant->setPosition( 5.f, 0.f ); + descendant->setSize( 10.f, 10.f ); + + EXPECT_EQ( 15.f, descendant->getWorldBounds().Left ); + child->setPosition( 20.f, 0.f ); + EXPECT_EQ( 25.f, descendant->getScreenRect().Left ); + EXPECT_EQ( 25.f, descendant->getWorldBounds().Left ); + + eeDelete( parent ); +} + static void init_test_scene_node( UISceneNode* sceneNode ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get(); diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index f4917a4cb..bbfd0b67d 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -1424,11 +1424,12 @@ Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir ) } if ( isBinary || ( inserts || deletes ) ) { - std::string rptrn( "(.*)%{.*%s->%s(.*)%}" ); + std::string rptrn( "(.*)%{.*%s->%s(.*)%}(.*)" ); LuaPattern pattern( rptrn ); if ( pattern.matches( file.data(), 0, matches, file.size() ) ) { file = file.substr( matches[1].start, matches[1].end - matches[1].start ) + - file.substr( matches[2].start, matches[2].end - matches[2].start ); + file.substr( matches[2].start, matches[2].end - matches[2].start ) + + file.substr( matches[3].start, matches[3].end - matches[3].start ); } auto filePath = subModulePath + file; diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index bb9178eee..b76522198 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -255,6 +256,12 @@ GitPlugin::~GitPlugin() { Sys::sleep( Milliseconds( 1.f ) ); } +void GitPlugin::onSaveState( IniFile* state ) { + std::string commitMessage; + Base64::encode( mLastCommitMsg.toUtf8(), commitMessage ); + state->setValue( "git", "commit_message", commitMessage ); +} + void GitPlugin::runAsyncTask( std::function task ) { auto runningTasks = mRunningAsyncTasks; ++*runningTasks; @@ -348,6 +355,12 @@ void GitPlugin::load( PluginManager* pluginManager ) { } } + std::string commitMessage; + Base64::decode( + getPluginContext()->getConfig().iniState.getValue( "git", "commit_message", "" ), + commitMessage ); + mLastCommitMsg = String::fromUtf8( commitMessage ); + if ( mKeyBindings.empty() ) { mKeyBindings["git-blame"] = "alt+shift+b"; } diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 2168ed86f..b551ce7a2 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -87,6 +87,8 @@ class GitPlugin : public PluginBase { void registerSettings( SettingsPage& page ) override; + void onSaveState( IniFile* state ) override; + void onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) override; FileSystemListenerOptions getFileSystemListenerOptions() const override;