Optimize bulk editor setup, deferred UI resources and Git plugin fixes

- Lazily compute document line hashes, avoid redundant multiline insertion
walks, and defer longest-line measurement for large or exceptionally long
text changes.

- Cache default code editor keybindings, reserve common command storage, and
create UIDiffView image viewers only when an image diff requires them.

- Keep cached screen positions coherent when descendants are queried before
dirty ancestors, and add regression coverage for ancestor movement.

- Relocate font-rendering golden images into the unit-test asset directory
and resolve image fixtures independently of the current working directory.

- Fix spurious status entries for files moved between directories. Preserve the suffix after Git's abbreviated brace notation when parsing renamed paths from numstat output. This prevents directory moves such as `bin/{ => unit_tests}/assets/...` from producing truncated synthetic untracked entries like `unit_tests`. Add regression coverage for staged files moved into a different directory.

- Git Plugin now remembers between sessions the uncommited commit message.
This commit is contained in:
Martín Lucas Golini
2026-08-30 01:05:24 -03:00
parent caba2d4462
commit 3afcb3f2d2
17 changed files with 439 additions and 89 deletions

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -1,6 +1,7 @@
#ifndef EE_UI_DOC_TEXTDOCUMENTLINE_HPP
#define EE_UI_DOC_TEXTDOCUMENTLINE_HPP
#include <atomic>
#include <eepp/core/string.hpp>
#include <eepp/system/lock.hpp>
#include <eepp/system/mutex.hpp>
@@ -14,25 +15,54 @@ class EE_API TextDocumentLine {
public:
TextDocumentLine( const String& text, std::shared_ptr<Mutex> docMutex ) :
mText( text ), mDocMutex( docMutex ) {
updateState();
updateTextHints();
}
TextDocumentLine( String&& text, std::shared_ptr<Mutex> 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<String::HashType> mHash{ 0 };
mutable std::atomic<Uint32> mFlags{ 0 };
std::shared_ptr<Mutex> 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

View File

@@ -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 );

View File

@@ -533,6 +533,8 @@ Uint32 Node::forceTextInput( const TextInputEvent& event ) {
}
const Vector2f& Node::getScreenPos() const {
if ( mNodeFlags & NODE_FLAG_POSITION_DIRTY )
const_cast<Node*>( 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 );

View File

@@ -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<Int64>( 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(); };

View File

@@ -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 );
}

View File

@@ -49,7 +49,9 @@ UICodeEditor* UICodeEditor::NewOpt( const bool& autoRegisterBaseCommands,
return eeNew( UICodeEditor, ( autoRegisterBaseCommands, autoRegisterBaseKeybindings ) );
}
const std::map<KeyBindings::Shortcut, std::string> UICodeEditor::getDefaultKeybindings() {
using CodeEditorKeyBindingMap = std::map<KeyBindings::Shortcut, std::string>;
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<KeyBindings::Shortcut, std::string> UICodeEditor::getDefaultKeybi
};
}
static std::shared_ptr<const CodeEditorKeyBindingMap> getCachedDefaultCodeEditorKeybindings() {
struct Cache {
Mutex mutex;
Uint32 defaultModifier{ 0 };
Uint32 secondaryModifier{ 0 };
std::shared_ptr<const CodeEditorKeyBindingMap> 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<const CodeEditorKeyBindingMap>( createDefaultCodeEditorKeybindings() );
cache.defaultModifier = defaultModifier;
cache.secondaryModifier = secondaryModifier;
}
return cache.bindings;
}
const std::map<KeyBindings::Shortcut, std::string> 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<UICodeEditor*>( 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() );
}

View File

@@ -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 );

View File

@@ -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<std::string> 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 );
}

View File

@@ -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" );

View File

@@ -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 <cmath>
#import <gdiplus.h>

View File

@@ -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<UIImageViewer>( 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<UIImageViewer>( 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<UIDiffView>( UI_TYPE_DIFF_VIEW ).size() );
EXPECT_TRUE( viewer->findAllByType<UIImageViewer>( 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<UIImageViewer>( UI_TYPE_IMAGE_VIEWER ).size() );
EXPECT_FALSE( diffView->getEditor()->isVisible() );
EXPECT_FALSE( diffView->getLeftEditor()->isVisible() );
EXPECT_FALSE( diffView->getRightEditor()->isVisible() );

View File

@@ -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();

View File

@@ -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;

View File

@@ -8,6 +8,7 @@
#include <eepp/graphics/image.hpp>
#include <eepp/graphics/primitives.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/base64.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/luapattern.hpp>
#include <eepp/system/scopedop.hpp>
@@ -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<void()> 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";
}

View File

@@ -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;