From 534c052722a0880f06f0d0fe13c92e7892e8dda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 24 Mar 2026 13:34:56 -0300 Subject: [PATCH] Added side by side view in the diff viewer, allow to change view mode with a switch. Performance improvements and more. --- include/eepp/ui/tools/uidiffview.hpp | 33 ++- include/eepp/ui/uicodeeditor.hpp | 3 + src/eepp/ui/tools/uidiffview.cpp | 231 ++++++++++++++---- src/eepp/ui/uicodeeditor.cpp | 2 +- src/tools/ecode/ecode.cpp | 40 ++- src/tools/ecode/ecode.hpp | 2 + src/tools/ecode/plugins/git/gitplugin.cpp | 16 +- .../ecode/plugins/plugincontextprovider.hpp | 3 + 8 files changed, 256 insertions(+), 74 deletions(-) diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp index a4507423a..e0658b9fa 100644 --- a/include/eepp/ui/tools/uidiffview.hpp +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace EE { namespace UI { namespace Tools { @@ -10,9 +11,10 @@ class UIDiffEditorPlugin; class EE_API UIDiffView : public UIWidget { public: + enum class ViewMode { Unified, SideBySide }; + static UIDiffView* New(); - UIDiffView(); virtual ~UIDiffView(); virtual Uint32 getType() const override; @@ -23,6 +25,8 @@ class EE_API UIDiffView : public UIWidget { void loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ); UICodeEditor* getEditor() const { return mEditor; } + UICodeEditor* getLeftEditor() const { return mLeftEditor; } + UICodeEditor* getRightEditor() const { return mRightEditor; } enum class DiffLineType { Common, Added, Removed, Header }; struct DiffLine { @@ -34,14 +38,31 @@ class EE_API UIDiffView : public UIWidget { const std::vector& getDiffLines() const { return mLines; } + void setViewMode( ViewMode mode ); + ViewMode getViewMode() const { return mViewMode; } + + void setViewModeToggleVisible( bool visible ); + bool isViewModeToggleVisible() const { return mViewModeToggleVisible; } + protected: + UICodeEditor* mEditor{ nullptr }; + UICodeEditor* mLeftEditor{ nullptr }; + UICodeEditor* mRightEditor{ nullptr }; + UIPushButton* mModeToggle{ nullptr }; + std::unique_ptr mPlugin; + std::unique_ptr mLeftPlugin; + std::unique_ptr mRightPlugin; + std::vector mLines; + ViewMode mViewMode{ ViewMode::Unified }; + bool mViewModeToggleVisible{ true }; + + UIDiffView(); + virtual void onSizeChange() override; - UICodeEditor* mEditor{ nullptr }; - std::shared_ptr mPlugin; - std::vector mLines; - - void buildEditor(); + void createEditor( UICodeEditor*& editor, std::unique_ptr& plugin ); + void syncScroll( UICodeEditor* source, UICodeEditor* target, bool emitEvent = false ); + void updateModeButton(); }; }}} // namespace EE::UI::Tools diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index cd8eeac5f..9f33e8731 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -836,6 +836,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void disableEditorFeatures( bool useDefaultStyle = true ); + void setDisableScrollInvalidation( bool disable ) { mDisableScrollInvalidation = disable; } + protected: struct LastXOffset { TextPosition position{ 0, 0 }; @@ -883,6 +885,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { bool mAllowSelectingTextFromGutter{ true }; bool mTabStops{ false }; bool mKerningEnabled{ false }; + bool mDisableScrollInvalidation{ false }; DocumentView mDocView; Clock mBlinkTimer; Time mBlinkTime; diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index 873166af8..43bc4cc9d 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace EE { namespace UI { namespace Tools { @@ -15,7 +17,7 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { UIDiffEditorPlugin( UIDiffView* view ) : mView( view ) {} inline Color getBackgroundColor( UIDiffView::DiffLineType type ) { - switch( type ) { + switch ( type ) { case UIDiffView::DiffLineType::Added: return Color( 0, 150, 32, 30 ); case UIDiffView::DiffLineType::Removed: @@ -28,13 +30,20 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { return Color::Transparent; } - std::string getId() override { return "UIDiffEditorPlugin"; } - std::string getTitle() override { return "UIDiffEditorPlugin"; } + std::string getId() override { return "DiffEditorPlugin"; } + + std::string getTitle() override { return "DiffEditorPlugin"; } + std::string getDescription() override { return "Highlights diff added/removed lines."; } + bool isReady() const override { return true; } void onRegister( UICodeEditor* editor ) override { - editor->registerGutterSpace( this, PixelDensity::dpToPx( 80 ), 0 ); + if ( mView->getViewMode() == UIDiffView::ViewMode::Unified ) { + editor->registerGutterSpace( this, PixelDensity::dpToPx( 80 ), 0 ); + } else { + editor->registerGutterSpace( this, PixelDensity::dpToPx( 40 ), 0 ); + } } void onUnregister( UICodeEditor* editor ) override { editor->unregisterGutterSpace( this ); } @@ -65,27 +74,48 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { const auto& lines = mView->getDiffLines(); const auto& line = lines[index]; - String text; - if ( line.type == UIDiffView::DiffLineType::Added ) { - text = String::format( "%5s %5lld", "", (long long)line.newLineNum ); - } else if ( line.type == UIDiffView::DiffLineType::Removed ) { - text = String::format( "%5lld %5s", (long long)line.oldLineNum, "" ); - } else if ( line.type == UIDiffView::DiffLineType::Common ) { - text = String::format( "%5lld %5lld", (long long)line.oldLineNum, - (long long)line.newLineNum ); + static constexpr auto bufSize = 16; + String::StringBaseType buf[bufSize] = {}; + if ( mView->getViewMode() == UIDiffView::ViewMode::Unified ) { + switch ( line.type ) { + case UIDiffView::DiffLineType::Added: + String::formatBuffer( buf, bufSize, "%5s %5lld", "", + (long long)line.newLineNum ); + break; + case UIDiffView::DiffLineType::Removed: + String::formatBuffer( buf, bufSize, "%5lld %5s", (long long)line.oldLineNum, + "" ); + break; + case UIDiffView::DiffLineType::Header: + String::formatBuffer( buf, bufSize, "%5lld %5lld", (long long)line.oldLineNum, + (long long)line.newLineNum ); + break; + case UIDiffView::DiffLineType::Common: + return; + } } else { - return; + if ( editor == mView->getLeftEditor() ) { + if ( line.oldLineNum > 0 ) + String::formatBuffer( buf, bufSize, "%5lld", (long long)line.oldLineNum ); + } else { + if ( line.newLineNum > 0 ) + String::formatBuffer( buf, bufSize, "%5lld", (long long)line.newLineNum ); + } } + String::View text{ buf, bufSize }; + if ( text.empty() ) + return; + FontStyleConfig config = editor->getFontStyleConfig(); config.FontColor = editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineNumber ); Float textWidth = Text::getTextWidth( text, config, 4, TextHints::AllAscii ); - Vector2f pos( screenStart.x + ( gutterWidth - textWidth ) * 0.5f, - screenStart.y + - ( lineHeight - config.Font->getLineSpacing( config.CharacterSize ) ) * - 0.5f ); + Vector2f pos( screenStart.x + std::floor( ( gutterWidth - textWidth ) * 0.5f ), + screenStart.y + std::floor( ( lineHeight - config.Font->getLineSpacing( + config.CharacterSize ) ) * + 0.5f ) ); Text::draw( text, pos, config, 4, TextHints::AllAscii ); } @@ -100,21 +130,49 @@ UIDiffView* UIDiffView::New() { UIDiffView::UIDiffView() : UIWidget( "diffview" ) { setFlags( UI_AUTO_SIZE ); - mEditor = UICodeEditor::New(); - mEditor->setParent( this ); - mEditor->setDocument( std::make_shared() ); - mEditor->setLocked( true ); - mEditor->setShowLineNumber( false ); - mEditor->setShowFoldingRegion( false ); + createEditor( mEditor, mPlugin ); + createEditor( mLeftEditor, mLeftPlugin ); + createEditor( mRightEditor, mRightPlugin ); - mPlugin = std::make_shared( this ); - mEditor->registerPlugin( mPlugin.get() ); + mLeftEditor->setVisible( false ); + mRightEditor->setVisible( false ); + mLeftEditor->setVerticalScrollBarEnabled( false ); + + mLeftEditor->on( Event::OnScrollChange, [this]( const Event* ) { + syncScroll( mLeftEditor, mRightEditor ); + + mRightEditor->getVScrollBar()->setValue( + mLeftEditor->getScroll().y / mLeftEditor->getMaxScroll().y, false ); + } ); + + mRightEditor->on( Event::OnScrollChange, + [this]( const Event* ) { syncScroll( mRightEditor, mLeftEditor ); } ); + + mLeftEditor->getVScrollBar()->on( + Event::OnValueChange, [this]( const Event* ) { syncScroll( mLeftEditor, mRightEditor ); } ); + + mRightEditor->getVScrollBar()->on( + Event::OnValueChange, [this]( const Event* ) { syncScroll( mRightEditor, mLeftEditor ); } ); + + mModeToggle = UIPushButton::New(); + mModeToggle->setParent( this ); + mModeToggle->setText( "Unified" ); + mModeToggle->onClick( [this]( const Event* event ) { + setViewMode( mViewMode == ViewMode::Unified ? ViewMode::SideBySide : ViewMode::Unified ); + } ); + + mModeToggle->on( Event::OnSizeChange, [this]( auto ) { updateModeButton(); } ); } UIDiffView::~UIDiffView() { - if ( mEditor && mPlugin ) { + if ( mEditor && mPlugin ) mEditor->unregisterPlugin( mPlugin.get() ); - } + + if ( mLeftEditor && mLeftPlugin ) + mLeftEditor->unregisterPlugin( mLeftPlugin.get() ); + + if ( mRightEditor && mRightPlugin ) + mRightEditor->unregisterPlugin( mRightPlugin.get() ); } Uint32 UIDiffView::getType() const { @@ -125,16 +183,79 @@ bool UIDiffView::isType( const Uint32& type ) const { return UIDiffView::getType() == type ? true : UIWidget::isType( type ); } +void UIDiffView::createEditor( UICodeEditor*& editor, + std::unique_ptr& plugin ) { + editor = UICodeEditor::New(); + editor->setParent( this ); + editor->setLocked( true ); + editor->setShowLineNumber( false ); + editor->setShowFoldingRegion( false ); + editor->setDisableScrollInvalidation( true ); + plugin = std::make_unique( this ); + editor->registerPlugin( plugin.get() ); +} + +void UIDiffView::setViewMode( ViewMode mode ) { + if ( mViewMode == mode ) + return; + + mViewMode = mode; + + if ( mViewMode == ViewMode::Unified ) { + mEditor->setVisible( true ); + mLeftEditor->setVisible( false ); + mRightEditor->setVisible( false ); + mModeToggle->setText( i18n( "diffview_unified", "Unified" ) ); + onSizeChange(); + syncScroll( mRightEditor, mEditor, true ); + } else { + mEditor->setVisible( false ); + mLeftEditor->setVisible( true ); + mRightEditor->setVisible( true ); + mModeToggle->setText( i18n( "diffview_split", "Split" ) ); + onSizeChange(); + syncScroll( mEditor, mRightEditor, true ); + syncScroll( mEditor, mLeftEditor, true ); + } +} + +void UIDiffView::setViewModeToggleVisible( bool visible ) { + mViewModeToggleVisible = visible; + mModeToggle->setVisible( visible ); +} + +void UIDiffView::syncScroll( UICodeEditor* source, UICodeEditor* target, bool emitEvent ) { + target->setScrollY( source->getScroll().y, emitEvent ); + target->setScrollX( source->getScroll().x, emitEvent ); +} + +void UIDiffView::updateModeButton() { + mModeToggle->setPixelsPosition( + getPixelsSize().getWidth() - mModeToggle->getPixelsSize().getWidth() - + mRightEditor->getVScrollBar()->getPixelsSize().getWidth() - PixelDensity::dpToPx( 8 ), + PixelDensity::dpToPx( 8 ) ); +} + void UIDiffView::onSizeChange() { - if ( mEditor ) + if ( mViewMode == ViewMode::Unified ) { mEditor->setPixelsSize( getPixelsSize() ); - UIWidget::onSizeChange(); + } else { + mLeftEditor->setPixelsSize( std::ceil( getPixelsSize().getWidth() * 0.5f ), + getPixelsSize().getHeight() ); + mRightEditor->setPixelsSize( std::floor( getPixelsSize().getWidth() * 0.5f ), + getPixelsSize().getHeight() ); + mRightEditor->setPixelsPosition( std::floor( getPixelsSize().getWidth() * 0.5f ), 0.f ); + } + + updateModeButton(); } void UIDiffView::loadFromPatch( const std::string& patchText ) { mLines.clear(); auto lines = String::split( patchText, '\n' ); - std::string cleanText; + String cleanText; + String leftText; + String rightText; Int64 oldLineNum = 0; Int64 newLineNum = 0; @@ -186,19 +307,27 @@ void UIDiffView::loadFromPatch( const std::string& patchText ) { dline.type = DiffLineType::Added; dline.text = line.substr( 1 ); dline.newLineNum = ++newLineNum; + leftText += "\n"; + rightText += dline.text + "\n"; } else if ( String::startsWith( line, "-" ) ) { dline.type = DiffLineType::Removed; dline.text = line.substr( 1 ); dline.oldLineNum = ++oldLineNum; + leftText += dline.text + "\n"; + rightText += "\n"; } else if ( String::startsWith( line, " " ) ) { dline.type = DiffLineType::Common; dline.text = line.substr( 1 ); dline.oldLineNum = ++oldLineNum; dline.newLineNum = ++newLineNum; + leftText += dline.text + "\n"; + rightText += dline.text + "\n"; } else { dline.type = DiffLineType::Common; dline.oldLineNum = ++oldLineNum; dline.newLineNum = ++newLineNum; + leftText += dline.text + "\n"; + rightText += dline.text + "\n"; } cleanText += dline.text + "\n"; @@ -207,11 +336,19 @@ void UIDiffView::loadFromPatch( const std::string& patchText ) { mEditor->getDocument().reset(); mEditor->getDocument().textInput( cleanText ); + mLeftEditor->getDocument().reset(); + mLeftEditor->getDocument().textInput( leftText ); + mRightEditor->getDocument().reset(); + mRightEditor->getDocument().textInput( rightText ); + mEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); + mLeftEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); + mRightEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); if ( !filename.empty() ) { auto def = Doc::SyntaxDefinitionManager::instance()->getByExtension( filename ); - if ( def.getLanguageIndex() > 1 ) - mEditor->getDocument().setSyntaxDefinition( def ); + mEditor->getDocument().setSyntaxDefinition( def ); + mLeftEditor->getDocument().setSyntaxDefinition( def ); + mRightEditor->getDocument().setSyntaxDefinition( def ); } } @@ -227,7 +364,9 @@ void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& size_t leftIndex = 0; size_t rightIndex = 0; - std::string cleanText; + String cleanText; + String leftText; + String rightText; for ( auto& pair : ranges ) { DiffLine dline; @@ -237,33 +376,35 @@ void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& dline.type = DiffLineType::Common; dline.oldLineNum = ++leftIndex; dline.newLineNum = ++rightIndex; + leftText += dline.text + "\n"; + rightText += dline.text + "\n"; break; case dtl::SES_ADD: dline.type = DiffLineType::Added; dline.newLineNum = ++rightIndex; + leftText += "\n"; + rightText += dline.text + "\n"; break; case dtl::SES_DELETE: dline.type = DiffLineType::Removed; dline.oldLineNum = ++leftIndex; + leftText += dline.text + "\n"; + rightText += "\n"; break; } - mLines.push_back( dline ); cleanText += dline.text + "\n"; + mLines.push_back( dline ); } mEditor->getDocument().reset(); mEditor->getDocument().textInput( cleanText ); -} - -void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ) { - std::string oldText, newText; - FileSystem::fileGet( oldFilePath, oldText ); - FileSystem::fileGet( newFilePath, newText ); - loadFromStrings( oldText, newText ); - - auto def = Doc::SyntaxDefinitionManager::instance()->getByExtension( oldFilePath ); - if ( def.getLanguageIndex() > 1 ) - mEditor->getDocument().setSyntaxDefinition( def ); + mLeftEditor->getDocument().reset(); + mLeftEditor->getDocument().textInput( leftText ); + mRightEditor->getDocument().reset(); + mRightEditor->getDocument().textInput( rightText ); + mEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); + mLeftEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); + mRightEditor->getDocument().setSelection( TextPosition( 0, 0 ) ); } }}} // namespace EE::UI::Tools diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 77d98662e..ba7125964 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -2251,7 +2251,7 @@ void UICodeEditor::scrollToCursor( bool centered ) { void UICodeEditor::updateEditor() { mDoc->setPageSize( getViewPortLineCount().y ); - if ( mDirtyScroll && mDoc->getActiveClient() == this ) + if ( !mDisableScrollInvalidation && mDirtyScroll && mDoc->getActiveClient() == this ) scrollTo( mDoc->getSelection().start() ); updateScrollBar(); diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index ea7bc37b5..a46761ce1 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -11,7 +11,6 @@ #include "uitreeviewfs.hpp" #include "uiwelcomescreen.hpp" #include "version.hpp" -#include #include #include #include @@ -19,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -2607,17 +2607,41 @@ void App::loadAudioFromPath( const std::string& path, bool autoPlay ) { audioPlayer->loadFromPath( path, autoPlay ); } +void App::loadDiffFromMemory( const std::string& content, const std::string& originalFilePath ) { + auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ); + auto* diffView = Tools::UIDiffView::New(); + auto [tab, iv] = getSplitter()->createWidget( diffView, diffViewTitle ); + if ( !originalFilePath.empty() ) { + std::string fileName = FileSystem::fileNameFromPath( originalFilePath ); + tab->setText( diffViewTitle + ": " + fileName ); + tab->setTooltipText( originalFilePath ); + } else { + tab->setText( diffViewTitle ); + } + UIIcon* icon = getUISceneNode()->findIcon( "filetype-diff" ); + if ( !icon ) + icon = getUISceneNode()->findIcon( "file" ); + if ( icon ) + tab->setIcon( icon->getSize( getMenuIconSize() ) ); + diffView->loadFromPatch( content ); +} + void App::loadDiffFromPath( const std::string& path ) { + auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ); auto* diffView = Tools::UIDiffView::New(); auto [tab, iv] = mSplitter->createWidget( diffView, i18n( "diff_viewer", "Diff Viewer" ) ); - tab->setText( FileSystem::fileNameFromPath( path ) )->setTooltipText( path ); - auto icon = findIcon( "filetype-patch" ); - tab->setIcon( icon ? icon : findIcon( "file" ) ); - - std::string text; - if ( FileSystem::fileGet( path, text ) ) { - diffView->loadFromPatch( text ); + if ( !path.empty() ) { + std::string fileName = FileSystem::fileNameFromPath( path ); + tab->setText( diffViewTitle + ": " + fileName ); + tab->setTooltipText( path ); + } else { + tab->setText( diffViewTitle ); } + auto icon = findIcon( "filetype-diff" ); + tab->setIcon( icon ? icon : findIcon( "file" ) ); + std::string text; + if ( FileSystem::fileGet( path, text ) ) + diffView->loadFromPatch( text ); } void App::openFileFromPath( const std::string& path ) { diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 63e37dfab..ec4046bef 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -558,6 +558,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void loadDiffFromPath( const std::string& path ); + void loadDiffFromMemory( const std::string& content, const std::string& originalFilePath = "" ); + void createAndShowRecentFolderPopUpMenu( Node* recentFoldersBut ); void createAndShowRecentFilesPopUpMenu( Node* recentFilesBut ); diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index ab4d8db00..28866a4d1 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1115,20 +1115,8 @@ void GitPlugin::diff( const std::string& file, bool isStaged ) { if ( res.fail() ) return; - getUISceneNode()->runOnMainThread( [this, file, res] { - auto* diffView = Tools::UIDiffView::New(); - auto [tab, iv] = getPluginContext()->getSplitter()->createWidget( - diffView, i18n( "diff_viewer", "Diff Viewer" ) ); - std::string fileName = FileSystem::fileNameFromPath( file ); - tab->setText( fileName )->setTooltipText( file ); - UIIcon* icon = getUISceneNode()->findIcon( - UIIconThemeManager::getIconNameFromFileName( fileName ) ); - if ( !icon ) - icon = getUISceneNode()->findIcon( "file" ); - if ( icon ) - tab->setIcon( icon->getSize( getPluginContext()->getMenuIconSize() ) ); - diffView->loadFromPatch( res.result ); - } ); + getUISceneNode()->runOnMainThread( + [this, file, res] { getPluginContext()->loadDiffFromMemory( res.result, file ); } ); } ); } diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index 9467e9051..60d152642 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -146,6 +146,9 @@ class PluginContextProvider { virtual void loadDiffFromPath( const std::string& path ) = 0; + virtual void loadDiffFromMemory( const std::string& content, + const std::string& originalFilePath = "" ) = 0; + virtual void loadFolder( std::string path, bool forceNewWindow = false ) = 0; virtual void showGlobalSearch( bool searchAndReplace,