Added multi-file diff / patch visualization support.

This commit is contained in:
Martín Lucas Golini
2026-03-25 00:54:48 -03:00
parent abbd2625ad
commit d0093a7801
11 changed files with 286 additions and 24 deletions

View File

@@ -1231,10 +1231,15 @@ TextInput.table_cell_edit {
DiffView > SelectButton {
border-color: transparent;
padding: 2dp 4dp 2dp 4dp;
padding: 0dp 4dp 0dp 4dp;
font-size: 10dp;
}
DiffView > SelectButton:hover,
DiffView > SelectButton:focus {
border-color: var(--primary);
}
@media (prefers-color-scheme: light) {
:root {

View File

@@ -1040,7 +1040,7 @@ class EE_API String {
static bool isLatin1( String::View str );
Uint32 getTextHints();
Uint32 getTextHints() const;
static Uint32 getTextHints( String::View str );

View File

@@ -5,7 +5,11 @@
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uiselectbutton.hpp>
namespace EE { namespace UI { namespace Tools {
namespace EE { namespace UI {
class UIScrollView;
namespace Tools {
class UIDiffEditorPlugin;
@@ -16,6 +20,12 @@ class EE_API UIDiffView : public UIWidget {
static UIDiffView* New();
static UIScrollView* NewMultiFileDiffViewer( const std::string& patchText );
static std::vector<std::string> splitDiff( const std::string& multiFileDiff );
static bool isMultiFileDiff( const std::string& diff );
virtual ~UIDiffView();
virtual Uint32 getType() const override;
@@ -56,6 +66,14 @@ class EE_API UIDiffView : public UIWidget {
void setSubLineDiffAlgorithm( SubLineDiffAlgorithm algo );
SubLineDiffAlgorithm getSubLineDiffAlgorithm() const { return mSubLineDiffAlgorithm; }
void setSyntaxColorScheme( const SyntaxColorScheme& colorScheme );
void setHeadersVisible( bool visible );
bool areHeadersVisible() const { return mHeadersVisible; }
const String& getFileName() const { return mFileName; }
protected:
UICodeEditor* mEditor{ nullptr };
UICodeEditor* mLeftEditor{ nullptr };
@@ -72,10 +90,16 @@ class EE_API UIDiffView : public UIWidget {
bool mViewModeToggleVisible{ true };
bool mShowCompleteView{ false };
bool mCompleteViewToggleVisible{ true };
bool mHeadersVisible{ false };
std::shared_ptr<SyntaxDefinition> mSyntaxDef;
String mFileName;
UIDiffView();
virtual void onSizePolicyChange() override;
virtual void onAutoSize() override;
virtual void onSizeChange() override;
void createEditor( UICodeEditor*& editor, std::unique_ptr<UIDiffEditorPlugin>& plugin );

View File

@@ -1140,6 +1140,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
bool gutterSpaceExists( UICodeEditorPlugin* plugin ) const;
Float getTotalTopSpace() const;
bool topSpaceExists( UICodeEditorPlugin* plugin ) const;
bool createContextMenu();

View File

@@ -1480,7 +1480,7 @@ bool String::isLatin1( String::View str ) {
return isAsciiTpl<String::View, 255>( str );
}
Uint32 String::getTextHints() {
Uint32 String::getTextHints() const {
if ( isAscii() )
return TextHints::AllAscii | TextHints::AllLatin1;
if ( isLatin1() )

View File

@@ -2,6 +2,7 @@
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/tools/uicodeeditorsplitter.hpp>
#include <eepp/ui/tools/uidiffview.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eepp/window/displaymanager.hpp>
#include <eepp/window/engine.hpp>
@@ -812,7 +813,10 @@ UICodeEditor* UICodeEditorSplitter::findEditorFromPath( const std::string& path
void UICodeEditorSplitter::applyColorScheme( const SyntaxColorScheme& colorScheme ) {
forEachEditor(
[colorScheme]( UICodeEditor* editor ) { editor->setColorScheme( colorScheme ); } );
[&colorScheme]( UICodeEditor* editor ) { editor->setColorScheme( colorScheme ); } );
forEachWidgetType( UI_TYPE_DIFF_VIEW, [&colorScheme]( UIWidget* widget ) {
widget->asType<UIDiffView>()->setSyntaxColorScheme( colorScheme );
} );
mClient->onColorSchemeChanged( mCurrentColorScheme );
}

View File

@@ -8,11 +8,32 @@
#include <eepp/ui/tools/uidiffview.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uiscrollbar.hpp>
#include <eepp/ui/uiscrollview.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <dtl/dtl.hpp>
namespace EE { namespace UI { namespace Tools {
UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText ) {
auto scrollView = UIScrollView::New();
auto vbox = UILinearLayout::NewVertical();
vbox->setParent( scrollView );
vbox->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
auto diffs = UIDiffView::splitDiff( patchText );
for ( const auto& diff : diffs ) {
auto* diffView = UIDiffView::New();
diffView->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
diffView->setParent( vbox );
diffView->setHeadersVisible( true );
diffView->loadFromPatch( diff );
}
return scrollView;
}
class UIDiffEditorPlugin : public UICodeEditorPlugin {
public:
UIDiffEditorPlugin( UIDiffView* view ) : mView( view ) {}
@@ -45,16 +66,56 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin {
Float glyphWidth = editor->getGlyphWidth();
Float totalChars = mView->getViewMode() == UIDiffView::ViewMode::Unified ? 10 : 5;
mGutterWidth = PixelDensity::dpToPx( glyphWidth * totalChars );
mPluginTopSpace = PixelDensity::dpToPxI( 20 );
editor->registerGutterSpace( this, mGutterWidth, 0 );
if ( mView->areHeadersVisible() ) {
editor->registerTopSpace( this, mPluginTopSpace, 0 );
}
}
void onUnregister( UICodeEditor* editor ) override { editor->unregisterGutterSpace( this ); }
Float getPluginTopSpace() const { return mPluginTopSpace; }
void onUnregister( UICodeEditor* editor ) override {
editor->unregisterGutterSpace( this );
editor->unregisterTopSpace( this );
}
void registerUpdate( UICodeEditor* editor ) {
onUnregister( editor );
onRegister( editor );
}
void drawTop( UICodeEditor* editor, const Vector2f& screenStart, const Sizef& size,
const Float& /*fontSize*/ ) override {
Float width = editor->getTopAreaWidth();
Primitives p;
Color backColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::Background ) );
p.setColor( backColor );
p.drawRectangle( Rectf( screenStart, Sizef( width, mPluginTopSpace ) ) );
Color lineColor(
editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineBreakColumn ) );
p.setColor( lineColor );
Float lineHeight = eefloor( PixelDensity::dpToPxI( 1 ) );
p.drawRectangle( { { screenStart.x, screenStart.y + size.getHeight() - lineHeight },
Sizef( width, lineHeight ) } );
Font* font = mView->getUISceneNode()->getUIThemeManager()->getDefaultFont();
if ( !font || mView->getFileName().empty() )
return;
Float fontSize = editor->getUISceneNode()->getUIThemeManager()->getDefaultFontSize();
Float textOffsetY =
eefloor( ( size.getHeight() - font->getLineSpacing( fontSize ) ) * 0.5f );
Color textColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineNumber2 ) );
Vector2f pos( screenStart.x + eefloor( PixelDensity::dpToPx( 8 ) ),
screenStart.y + textOffsetY );
Text::draw( mView->getFileName(), pos, font, fontSize, textColor, 0, 0.f, Color::Black,
Color::Black, { 1, 1 }, 4, mView->getFileName().getTextHints() );
}
void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position,
const Float& /*fontSize*/, const Float& lineHeight ) override {
const auto& viewLines = mView->getViewLines();
@@ -177,12 +238,62 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin {
protected:
UIDiffView* mView;
Float mGutterWidth{ 0 };
Float mPluginTopSpace{ 0 };
};
UIDiffView* UIDiffView::New() {
return eeNew( UIDiffView, () );
}
bool UIDiffView::isMultiFileDiff( const std::string& diff ) {
size_t startPos = 0;
if ( diff.substr( 0, 5 ) != "diff " ) {
size_t firstDiff = diff.find( "\ndiff " );
if ( firstDiff != std::string::npos ) {
startPos = firstDiff + 1; // Skip the preamble and the newline
}
}
size_t count = 0;
while ( startPos < diff.size() && count < 2 ) {
size_t nextPos = diff.find( "\ndiff ", startPos );
if ( nextPos == std::string::npos ) {
count++;
break;
}
count++;
startPos = nextPos + 1;
}
return count >= 2;
}
std::vector<std::string> UIDiffView::splitDiff( const std::string& multiFileDiff ) {
std::vector<std::string> diffs;
if ( multiFileDiff.empty() )
return diffs;
size_t startPos = 0;
// Skip any preamble (e.g. commit message from git show)
if ( multiFileDiff.substr( 0, 5 ) != "diff " ) {
size_t firstDiff = multiFileDiff.find( "\ndiff " );
if ( firstDiff != std::string::npos ) {
startPos = firstDiff + 1; // Skip the preamble and the newline
}
}
while ( startPos < multiFileDiff.size() ) {
size_t nextPos = multiFileDiff.find( "\ndiff ", startPos );
if ( nextPos == std::string::npos ) {
diffs.push_back( multiFileDiff.substr( startPos ) );
break;
}
diffs.push_back( multiFileDiff.substr( startPos, nextPos + 1 - startPos ) );
startPos = nextPos + 1;
}
return diffs;
}
UIDiffView::UIDiffView() : UIWidget( "diffview" ) {
setFlags( UI_AUTO_SIZE );
createEditor( mEditor, mPlugin );
@@ -328,33 +439,65 @@ void UIDiffView::syncScroll( UICodeEditor* source, UICodeEditor* target, bool em
void UIDiffView::updateModeButton() {
auto margin = PixelDensity::dpToPx( 4 );
Float emptySpace =
mPlugin->getPluginTopSpace() - std::max( mModeToggle->getPixelsSize().getHeight(),
mCompleteViewToggle->getPixelsSize().getHeight() );
auto vmargin = mHeadersVisible ? emptySpace * 0.5f : margin;
Float currentX = getPixelsSize().getWidth() - margin;
currentX -= mRightEditor->getVScrollBar()->getPixelsSize().getWidth();
if ( mViewModeToggleVisible && mModeToggle ) {
currentX -= mModeToggle->getPixelsSize().getWidth();
mModeToggle->setPixelsPosition( currentX, margin );
mModeToggle->setPixelsPosition( currentX, vmargin );
currentX -= margin;
}
if ( mCompleteViewToggleVisible && mCompleteViewToggle ) {
currentX -= mCompleteViewToggle->getPixelsSize().getWidth();
mCompleteViewToggle->setPixelsPosition( currentX, margin );
mCompleteViewToggle->setPixelsPosition( currentX, vmargin );
}
}
void UIDiffView::onSizePolicyChange() {
if ( mHeightPolicy == SizePolicy::WrapContent && mEditor && mLeftEditor ) {
mEditor->setLayoutHeightPolicy( mHeightPolicy );
mLeftEditor->setLayoutHeightPolicy( mHeightPolicy );
mRightEditor->setLayoutHeightPolicy( mHeightPolicy );
onAutoSize();
}
}
void UIDiffView::onAutoSize() {
if ( mHeightPolicy == SizePolicy::WrapContent && mEditor && mLeftEditor ) {
setPixelsSize( getPixelsSize().getWidth(), mViewMode == ViewMode::Unified
? mEditor->getPixelsSize().getHeight()
: mLeftEditor->getPixelsSize().getHeight() );
}
}
void UIDiffView::onSizeChange() {
if ( mViewMode == ViewMode::Unified ) {
mEditor->setPixelsSize( getPixelsSize() );
mEditor->setPixelsSize(
{ getPixelsSize().getWidth(), mHeightPolicy == SizePolicy::WrapContent
? mEditor->getPixelsSize().getHeight()
: getPixelsSize().getHeight() } );
} else {
mLeftEditor->setPixelsSize( std::ceil( getPixelsSize().getWidth() * 0.5f ),
getPixelsSize().getHeight() );
mHeightPolicy == SizePolicy::WrapContent
? mLeftEditor->getPixelsSize().getHeight()
: getPixelsSize().getHeight() );
mRightEditor->setPixelsSize( std::floor( getPixelsSize().getWidth() * 0.5f ),
getPixelsSize().getHeight() );
mHeightPolicy == SizePolicy::WrapContent
? mRightEditor->getPixelsSize().getHeight()
: getPixelsSize().getHeight() );
mRightEditor->setPixelsPosition( std::floor( getPixelsSize().getWidth() * 0.5f ), 0.f );
}
onAutoSize();
updateModeButton();
}
@@ -639,10 +782,12 @@ void UIDiffView::loadFromPatch( const std::string& patchText,
auto def = SyntaxDefinitionManager::instance()->getByExtension( filename );
mSyntaxDef =
SyntaxDefinitionManager::instance()->getLanguageDefinition( def.getLanguageIndex() );
mFileName = std::move( filename );
}
updateEditorsText();
updateButtonsText();
onSizeChange();
}
void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& newText ) {
@@ -686,6 +831,7 @@ void UIDiffView::loadFromStrings( const std::string& oldText, const std::string&
mShowCompleteView = false;
mCompleteViewToggle->setText( i18n( "diffview_complete", "Complete" ) );
updateEditorsText();
onSizeChange();
}
void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ) {
@@ -699,6 +845,7 @@ void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string
mEditor->getDocument().setSyntaxDefinition( def );
mLeftEditor->getDocument().setSyntaxDefinition( def );
mRightEditor->getDocument().setSyntaxDefinition( def );
mFileName = FileSystem::fileNameFromPath( newFilePath );
loadFromStrings( oldText, newText );
}
@@ -710,4 +857,20 @@ void UIDiffView::updateButtonsText() {
mCompleteViewToggle->setSelected( !mShowCompleteView );
}
void UIDiffView::setSyntaxColorScheme( const SyntaxColorScheme& colorScheme ) {
mEditor->setColorScheme( colorScheme );
mLeftEditor->setColorScheme( colorScheme );
mRightEditor->setColorScheme( colorScheme );
}
void UIDiffView::setHeadersVisible( bool visible ) {
if ( visible == mHeadersVisible )
return;
mHeadersVisible = visible;
mPlugin->registerUpdate( mEditor );
mLeftPlugin->registerUpdate( mLeftEditor );
mRightPlugin->registerUpdate( mRightEditor );
updateModeButton();
}
}}} // namespace EE::UI::Tools

View File

@@ -2040,6 +2040,7 @@ void UICodeEditor::drawCursor( const Vector2f& startScroll, const Float& lineHei
}
void UICodeEditor::onSizeChange() {
onAutoSize();
invalidateEditor( false );
invalidateLineWrapMaxWidth( false );
if ( !mDocView.isWrapEnabled() )
@@ -3352,6 +3353,16 @@ Float UICodeEditor::getPluginsGutterSpace() const {
return mPluginsGutterSpace;
}
Float UICodeEditor::getTotalTopSpace() const {
Float space = 0.f;
if ( mPluginsTopSpace > 0 ) {
for ( auto& plugin : mPluginTopSpaces ) {
space += plugin.space;
}
}
return space;
}
bool UICodeEditor::topSpaceExists( UICodeEditorPlugin* plugin ) const {
for ( const auto& space : mPluginTopSpaces ) {
if ( space.plugin == plugin )
@@ -5574,8 +5585,8 @@ void UICodeEditor::onAutoSize() {
if ( mHeightPolicy == SizePolicy::WrapContent ) {
auto visibleLineCount = getDocumentView().getVisibleLinesCount();
Float lineHeight = getLineHeight();
Float height =
lineHeight * visibleLineCount + getPixelsPadding().Top + getPixelsPadding().Bottom;
Float height = lineHeight * visibleLineCount + getPixelsPadding().Top +
getPixelsPadding().Bottom + getTotalTopSpace();
setPixelsSize( getPixelsSize().getWidth(), height );
}
}

View File

@@ -2608,6 +2608,28 @@ void App::loadAudioFromPath( const std::string& path, bool autoPlay ) {
}
void App::loadDiffFromMemory( const std::string& content, const std::string& originalFilePath ) {
if ( UIDiffView::isMultiFileDiff( content ) ) {
auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ) + ": " + originalFilePath;
UIIcon* icon = getUISceneNode()->findIcon( "filetype-diff" );
if ( !icon )
icon = getUISceneNode()->findIcon( "file" );
auto scrollView = UIDiffView::NewMultiFileDiffViewer( content );
auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle );
if ( icon )
tab->setIcon( icon->getSize( getMenuIconSize() ) );
tab->setText( diffViewTitle );
auto diffView = scrollView->getFirstChild()->asType<UILinearLayout>()->getFirstChild();
while ( diffView ) {
if ( diffView->isType( UI_TYPE_DIFF_VIEW ) )
diffView->asType<UIDiffView>()->setSyntaxColorScheme( *getCurrentColorScheme() );
diffView = diffView->getNextNode();
}
return;
}
auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" );
auto* diffView = Tools::UIDiffView::New();
auto [tab, iv] = getSplitter()->createWidget( diffView, diffViewTitle );
@@ -2623,10 +2645,39 @@ void App::loadDiffFromMemory( const std::string& content, const std::string& ori
icon = getUISceneNode()->findIcon( "file" );
if ( icon )
tab->setIcon( icon->getSize( getMenuIconSize() ) );
diffView->setHeadersVisible( true );
diffView->loadFromPatch( content, originalFilePath );
diffView->setSyntaxColorScheme( *getCurrentColorScheme() );
}
void App::loadDiffFromPath( const std::string& path ) {
std::string content;
if ( !FileSystem::fileGet( path, content ) )
return;
if ( UIDiffView::isMultiFileDiff( content ) ) {
auto diffViewTitle =
i18n( "diff_viewer", "Diff Viewer" ) + ": " + FileSystem::fileNameFromPath( path );
UIIcon* icon = getUISceneNode()->findIcon( "filetype-diff" );
if ( !icon )
icon = getUISceneNode()->findIcon( "file" );
auto scrollView = UIDiffView::NewMultiFileDiffViewer( content );
auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle );
if ( icon )
tab->setIcon( icon->getSize( getMenuIconSize() ) );
tab->setText( diffViewTitle );
auto diffView = scrollView->getFirstChild()->asType<UILinearLayout>()->getFirstChild();
while ( diffView ) {
if ( diffView->isType( UI_TYPE_DIFF_VIEW ) )
diffView->asType<UIDiffView>()->setSyntaxColorScheme( *getCurrentColorScheme() );
diffView = diffView->getNextNode();
}
return;
}
auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" );
auto* diffView = Tools::UIDiffView::New();
auto [tab, iv] = mSplitter->createWidget( diffView, i18n( "diff_viewer", "Diff Viewer" ) );
@@ -2639,9 +2690,10 @@ void App::loadDiffFromPath( const std::string& path ) {
}
auto icon = findIcon( "filetype-diff" );
tab->setIcon( icon ? icon : findIcon( "file" ) );
std::string text;
if ( FileSystem::fileGet( path, text ) )
diffView->loadFromPatch( text, path );
diffView->setHeadersVisible( true );
diffView->loadFromPatch( content, path );
diffView->setSyntaxColorScheme( *getCurrentColorScheme() );
}
void App::openFileFromPath( const std::string& path ) {

View File

@@ -14,6 +14,7 @@
#include <eepp/ui/uiloader.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiradiobutton.hpp>
#include <eepp/ui/uiscrollview.hpp>
#include <eepp/ui/uistackwidget.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextedit.hpp>
@@ -1087,8 +1088,6 @@ void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) {
std::string repoName = this->repoName( repoPath );
getUISceneNode()->runOnMainThread( [this, mode, res, repoName] {
auto ret = mManager->getSplitter()->createEditorInNewTab();
auto doc = ret.second->getDocumentRef();
std::string modeName;
switch ( mode ) {
case Git::DiffHead: {
@@ -1099,12 +1098,8 @@ void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) {
modeName = "staged";
break;
}
doc->setDefaultFileName( repoName + "-" + modeName + ".diff" );
ret.second->setSyntaxDefinition(
SyntaxDefinitionManager::instance()->getByLSPName( "diff" ) );
doc->textInput( res.result, false );
doc->moveToStartOfDoc();
doc->resetUndoRedo();
getPluginContext()->loadDiffFromMemory(
res.result, UIDiffView::isMultiFileDiff( res.result ) ? modeName : "" );
} );
} );
}

View File

@@ -23,6 +23,10 @@ class UITabWidget;
class UISceneNode;
class UICodeEditor;
namespace Doc {
class SyntaxColorScheme;
}
namespace Tools {
class UICodeEditorSplitter;
}
@@ -159,6 +163,8 @@ class PluginContextProvider {
virtual bool projectIsOpen() const = 0;
virtual size_t getMenuIconSize() const = 0;
virtual const SyntaxColorScheme* getCurrentColorScheme() const = 0;
};
} // namespace ecode