mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
Add integrated Git conflict resolution workflow
Introduce a three-pane UIMergeView with an editable shared result document, conflict navigation and acceptance actions, live diff highlighting, synchronized scrolling, and contextual pane labels. Read conflict stages directly from Git's index, detect paused merge operations, restore conflict sessions, and support resolving, staging, recreating, continuing, committing, and aborting operations. Add repository-aware conflict state handling, safe asynchronous lifetimes, merge operation UI, configurable key modifiers, i18n strings, and focused merge-view and Git conflict tests.
This commit is contained in:
@@ -31,6 +31,9 @@ static constexpr char DEFAULT_NON_WORD_CHARS[] = " \t\n/\\()\"':,.;<>~!@#$%^&*|+
|
||||
|
||||
static UnorderedSet<String::HashType> TEXT_DOCUMENT_COMMANDS = {};
|
||||
|
||||
TextDocument::ScopedReadLock::ScopedReadLock( const TextDocument& document ) :
|
||||
mLinesLock( document.mLinesMutex ), mDocumentLock( *document.mDocumentMutex ) {}
|
||||
|
||||
bool TextDocument::fileMightBeBinary( const std::string& file ) {
|
||||
static constexpr size_t MAX_READ = 4096;
|
||||
static constexpr std::array<char, 4> NULL_SEQUENCE = { 0, 0, 0, 0 };
|
||||
|
||||
@@ -268,6 +268,7 @@ ResourcePtr<UIIconTheme> IconManager::init( const std::string& iconThemeName,
|
||||
{ "git-stash-apply", 0xec27 },
|
||||
{ "git-stash-pop", 0xec28 },
|
||||
{ "git-merge", 0xeafe },
|
||||
{ "git-branch-staged-changes", 0xec6d },
|
||||
{ "diff-single", 0xec22 },
|
||||
{ "remove", 0xeb3b },
|
||||
{ "tag", 0xea66 },
|
||||
@@ -305,6 +306,12 @@ ResourcePtr<UIIconTheme> IconManager::init( const std::string& iconThemeName,
|
||||
{ "link", 0xeb15 },
|
||||
{ "agent", 0xec67 },
|
||||
{ "diff", 0xeae1 },
|
||||
{ "arrow-both", 0xea99 },
|
||||
{ "arrow-left", 0xea9b },
|
||||
{ "arrow-right", 0xea9c },
|
||||
{ "arrow-circle-left", 0xebfd },
|
||||
{ "arrow-circle-right", 0xebfe },
|
||||
{ "discard", 0xeae2 },
|
||||
|
||||
} ) {
|
||||
iconTheme->add( UIGlyphIcon::New( icon.first, codIconFont, icon.second ) );
|
||||
|
||||
@@ -77,7 +77,7 @@ static Sprite* setImageViewerImage( UIImageViewer* viewer, Image* image ) {
|
||||
}
|
||||
|
||||
UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText,
|
||||
const std::string& repoPath ) {
|
||||
const std::string& repoPath, ViewMode viewMode ) {
|
||||
auto scrollView = UIScrollView::New();
|
||||
auto vbox = UILinearLayout::NewVertical();
|
||||
vbox->setParent( scrollView );
|
||||
@@ -87,6 +87,7 @@ UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText,
|
||||
|
||||
for ( const auto& diff : diffs ) {
|
||||
auto* diffView = UIDiffView::New();
|
||||
diffView->setViewMode( viewMode );
|
||||
diffView->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
|
||||
diffView->setParent( vbox );
|
||||
diffView->setHeadersVisible( true );
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/ui/tools/uimergeview.hpp>
|
||||
#include <eepp/ui/uiicon.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uiscrollbar.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/window/window.hpp>
|
||||
|
||||
#include <dtl/dtl.hpp>
|
||||
|
||||
namespace EE::UI::Tools {
|
||||
|
||||
class UIMergeEditorPlugin : public UICodeEditorPlugin {
|
||||
public:
|
||||
explicit UIMergeEditorPlugin( UIMergeView* view ) : mView( view ) {}
|
||||
enum class LineType : Uint8 { Common, Added, Removed, Conflict };
|
||||
|
||||
std::string getId() override { return "MergeEditorPlugin"; }
|
||||
std::string getTitle() override { return "MergeEditorPlugin"; }
|
||||
std::string getDescription() override { return "Highlights merge changes and conflicts."; }
|
||||
bool isReady() const override { return true; }
|
||||
void onRegister( UICodeEditor* editor ) override {
|
||||
auto* themeManager = editor->getUISceneNode()->getUIThemeManager();
|
||||
Font* font = themeManager->getDefaultFont();
|
||||
mHeaderHeight =
|
||||
font ? std::ceil( themeManager->getDefaultFontSize() ) + PixelDensity::dpToPxI( 8 )
|
||||
: PixelDensity::dpToPxI( 24 );
|
||||
editor->registerTopSpace( this, mHeaderHeight, 0 );
|
||||
}
|
||||
void onUnregister( UICodeEditor* editor ) override { editor->unregisterTopSpace( this ); }
|
||||
bool onKeyDown( UICodeEditor*, const KeyEvent& event ) override {
|
||||
return mView->executeMergeKeyBinding( event );
|
||||
}
|
||||
|
||||
void setLines( std::vector<LineType> lines ) { mLines = std::move( lines ); }
|
||||
void setHeader( String label, bool present, const String& missingVersionLabel ) {
|
||||
mHeader = std::move( label );
|
||||
if ( !present && !missingVersionLabel.empty() )
|
||||
mHeader += " · " + missingVersionLabel;
|
||||
}
|
||||
|
||||
void drawTop( UICodeEditor* editor, const Vector2f& screenStart, const Sizef& size,
|
||||
const Float& ) override {
|
||||
Primitives primitives;
|
||||
primitives.setColor(
|
||||
editor->getColorScheme().getEditorColor( SyntaxStyleTypes::Background ) );
|
||||
primitives.drawRectangle( Rectf( screenStart, size ) );
|
||||
primitives.setColor(
|
||||
editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineBreakColumn ) );
|
||||
primitives.drawRectangle(
|
||||
{ { screenStart.x, screenStart.y + size.y - PixelDensity::dpToPx( 1 ) },
|
||||
{ size.x, PixelDensity::dpToPx( 1 ) } } );
|
||||
|
||||
auto* themeManager = editor->getUISceneNode()->getUIThemeManager();
|
||||
Font* font = themeManager->getDefaultFont();
|
||||
if ( !font || mHeader.empty() )
|
||||
return;
|
||||
const Float fontSize = themeManager->getDefaultFontSize();
|
||||
const Float offsetY = eefloor( ( size.y - font->getLineSpacing( fontSize ) ) * 0.5f );
|
||||
Text::draw( mHeader, { screenStart.x + PixelDensity::dpToPx( 8 ), screenStart.y + offsetY },
|
||||
font, fontSize,
|
||||
editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineNumber2 ) );
|
||||
}
|
||||
|
||||
void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position,
|
||||
const Float&, const Float& lineHeight ) override {
|
||||
if ( index < 0 || index >= static_cast<Int64>( mLines.size() ) ||
|
||||
mLines[index] == LineType::Common )
|
||||
return;
|
||||
Color color;
|
||||
switch ( mLines[index] ) {
|
||||
case LineType::Added:
|
||||
color = Color( 0, 150, 32, 40 );
|
||||
break;
|
||||
case LineType::Removed:
|
||||
color = Color( 180, 0, 32, 40 );
|
||||
break;
|
||||
case LineType::Conflict:
|
||||
color = Color( 180, 0, 32, 64 );
|
||||
break;
|
||||
case LineType::Common:
|
||||
return;
|
||||
}
|
||||
Primitives primitives;
|
||||
primitives.setColor( color );
|
||||
primitives.drawRectangle( { { editor->getScreenPos().x, position.y },
|
||||
{ editor->getPixelsSize().x, lineHeight } } );
|
||||
}
|
||||
|
||||
private:
|
||||
UIMergeView* mView;
|
||||
std::vector<LineType> mLines;
|
||||
String mHeader;
|
||||
Float mHeaderHeight{ 0 };
|
||||
};
|
||||
|
||||
UIMergeView* UIMergeView::New() {
|
||||
return eeNew( UIMergeView, () );
|
||||
}
|
||||
|
||||
UIMergeView::UIMergeView() :
|
||||
UILinearLayout( "mergeview", UIOrientation::Vertical ),
|
||||
WidgetCommandExecuter( KeyBindings{ getUISceneNode()->getWindow()->getInput() } ) {
|
||||
setFlags( UI_AUTO_SIZE );
|
||||
createToolbar();
|
||||
mEditorsLayout = UILinearLayout::NewHorizontal();
|
||||
mEditorsLayout->setParent( this );
|
||||
mEditorsLayout->setId( "merge_editors" );
|
||||
mEditorsLayout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed );
|
||||
mEditorsLayout->setLayoutWeight( 1 );
|
||||
for ( auto** editor : { &mLeftEditor, &mResultEditor, &mRightEditor } ) {
|
||||
*editor = UICodeEditor::New();
|
||||
( *editor )->setParent( mEditorsLayout );
|
||||
( *editor )->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::MatchParent );
|
||||
( *editor )->setLayoutWeight( 1 );
|
||||
( *editor )->setShowFoldingRegion( false );
|
||||
}
|
||||
mLeftEditor->setLocked( true );
|
||||
mRightEditor->setLocked( true );
|
||||
// The right editor owns the single visible vertical scrollbar for the three panes.
|
||||
mLeftEditor->setVerticalScrollBarEnabled( false );
|
||||
mResultEditor->setVerticalScrollBarEnabled( false );
|
||||
mLeftPlugin = std::make_unique<UIMergeEditorPlugin>( this );
|
||||
mResultPlugin = std::make_unique<UIMergeEditorPlugin>( this );
|
||||
mRightPlugin = std::make_unique<UIMergeEditorPlugin>( this );
|
||||
mLeftEditor->registerPlugin( mLeftPlugin.get() );
|
||||
mResultEditor->registerPlugin( mResultPlugin.get() );
|
||||
mRightEditor->registerPlugin( mRightPlugin.get() );
|
||||
mResultEditor->on( Event::OnTextChanged, [this]( const Event* ) {
|
||||
if ( mApplyingBlock )
|
||||
return;
|
||||
debounce(
|
||||
[this] {
|
||||
refreshBlocks();
|
||||
refreshHighlights();
|
||||
},
|
||||
Milliseconds( 100 ), String::hash( "merge-view-refresh" ) );
|
||||
} );
|
||||
|
||||
for ( auto* source : { mLeftEditor, mResultEditor, mRightEditor } ) {
|
||||
source->on( Event::OnScrollChange,
|
||||
[this, source]( const Event* ) { syncScroll( source ); } );
|
||||
source->getVScrollBar()->on( Event::OnValueChange,
|
||||
[this, source]( const Event* ) { syncScroll( source ); } );
|
||||
}
|
||||
}
|
||||
|
||||
void UIMergeView::syncScroll( UICodeEditor* source ) {
|
||||
if ( mSyncingScroll )
|
||||
return;
|
||||
mSyncingScroll = true;
|
||||
const Float sourceMaxY = source->getMaxScroll().y;
|
||||
const Float scrollRatio = sourceMaxY > 0 ? source->getScroll().y / sourceMaxY : 0;
|
||||
for ( auto* target : { mLeftEditor, mResultEditor, mRightEditor } ) {
|
||||
if ( target != source ) {
|
||||
target->setScrollY( scrollRatio * target->getMaxScroll().y, false );
|
||||
target->setScrollX( source->getScroll().x, false );
|
||||
}
|
||||
}
|
||||
mSyncingScroll = false;
|
||||
}
|
||||
|
||||
void UIMergeView::createToolbar() {
|
||||
static constexpr auto TOOLBAR_LAYOUT = R"xml(
|
||||
<style>
|
||||
mergeview .mergeview_toolbar_button {
|
||||
margin: 0dp 4dp 4dp 0dp;
|
||||
padding: 2dp 4dp 2dp 4dp;
|
||||
}
|
||||
</style>
|
||||
<StackLayout id="merge_toolbar" lw="mp" lh="wc" padding="4dp 4dp 0dp 4dp">
|
||||
<PushButton id="merge_previous_conflict" text="Previous Conflict" icon="icon(arrow-up, 12dp)" class="mergeview_toolbar_button" />
|
||||
<PushButton id="merge_accept_left" text="Accept Left" icon="icon(arrow-left, 12dp)" class="mergeview_toolbar_button" />
|
||||
<PushButton id="merge_accept_both" text="Accept Both" icon="icon(arrow-both, 12dp)" class="mergeview_toolbar_button" />
|
||||
<PushButton id="merge_accept_right" text="Accept Right" icon="icon(arrow-right, 12dp)" class="mergeview_toolbar_button" />
|
||||
<PushButton id="merge_recreate_conflict" text="Recreate Conflict" icon="icon(refresh, 12dp)" class="mergeview_toolbar_button" />
|
||||
<PushButton id="merge_next_conflict" text="Next Conflict" icon="icon(arrow-down, 12dp)" class="mergeview_toolbar_button" />
|
||||
</StackLayout>
|
||||
)xml";
|
||||
getUISceneNode()->loadLayoutFromString( TOOLBAR_LAYOUT, this,
|
||||
String::hash( "uimergeview_toolbar" ) );
|
||||
mToolbar = find<UIStackLayout>( "merge_toolbar" );
|
||||
|
||||
const auto bindAction = [this]( const char* buttonId, const char* command,
|
||||
const KeyBindings::Shortcut& shortcut,
|
||||
CommandCallback callback ) {
|
||||
setCommand( command, std::move( callback ) );
|
||||
if ( !shortcut.empty() )
|
||||
getKeyBindings().addKeybind( shortcut, command );
|
||||
auto* button = find<UIPushButton>( buttonId );
|
||||
const auto keybind = getKeyBindings().getCommandKeybindString( command );
|
||||
if ( !keybind.empty() )
|
||||
button->setTooltipText( button->getText() + " (" + keybind + ")" );
|
||||
button->onClick(
|
||||
[this, command = std::string{ command }]( const Event* ) { execute( command ); } );
|
||||
};
|
||||
bindAction( "merge_previous_conflict", "merge-previous-conflict", { KEY_F7 },
|
||||
[this] { goToPreviousConflict(); } );
|
||||
bindAction( "merge_accept_left", "merge-accept-left",
|
||||
{ KEY_1, KeyMod::getDefaultSecondaryModifier() },
|
||||
[this] { acceptCurrentStage2(); } );
|
||||
bindAction( "merge_accept_both", "merge-accept-both",
|
||||
{ KEY_2, KeyMod::getDefaultSecondaryModifier() }, [this] { acceptCurrentBoth(); } );
|
||||
bindAction( "merge_accept_right", "merge-accept-right",
|
||||
{ KEY_3, KeyMod::getDefaultSecondaryModifier() },
|
||||
[this] { acceptCurrentStage3(); } );
|
||||
bindAction( "merge_recreate_conflict", "merge-recreate-conflict", {}, [this] {
|
||||
if ( mRecreateConflictCallback )
|
||||
mRecreateConflictCallback();
|
||||
else
|
||||
recreateConflict();
|
||||
} );
|
||||
bindAction( "merge_next_conflict", "merge-next-conflict", { KEY_F8 },
|
||||
[this] { goToNextConflict(); } );
|
||||
}
|
||||
|
||||
UIPushButton* UIMergeView::addToolbarAction( const std::string& command, const String& text,
|
||||
const KeyBindings::Shortcut& shortcut,
|
||||
const std::string& icon, CommandCallback callback ) {
|
||||
setCommand( command, std::move( callback ) );
|
||||
if ( !shortcut.empty() )
|
||||
getKeyBindings().addKeybind( shortcut, command );
|
||||
auto* button = UIPushButton::New();
|
||||
button->setParent( mToolbar );
|
||||
button->setId( command );
|
||||
button->setText( text );
|
||||
if ( auto* toolbarIcon = getUISceneNode()->findIcon( icon ) )
|
||||
button->setIcon( toolbarIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) );
|
||||
button->setClass( "mergeview_toolbar_button" );
|
||||
button->setLayoutMarginRight( PixelDensity::dpToPx( 4 ) );
|
||||
const auto keybind = getKeyBindings().getCommandKeybindString( command );
|
||||
button->setTooltipText( keybind.empty() ? text : text + " (" + keybind + ")" );
|
||||
button->onClick( [this, command]( const Event* ) { execute( command ); } );
|
||||
return button;
|
||||
}
|
||||
|
||||
UIMergeView::~UIMergeView() {
|
||||
if ( mLeftEditor && mLeftPlugin )
|
||||
mLeftEditor->unregisterPlugin( mLeftPlugin.get() );
|
||||
if ( mResultEditor && mResultPlugin )
|
||||
mResultEditor->unregisterPlugin( mResultPlugin.get() );
|
||||
if ( mRightEditor && mRightPlugin )
|
||||
mRightEditor->unregisterPlugin( mRightPlugin.get() );
|
||||
}
|
||||
|
||||
Uint32 UIMergeView::getType() const {
|
||||
return UI_TYPE_MERGE_VIEW;
|
||||
}
|
||||
|
||||
bool UIMergeView::isType( const Uint32& type ) const {
|
||||
return type == getType() || UILinearLayout::isType( type );
|
||||
}
|
||||
|
||||
void UIMergeView::load( MergeInput input ) {
|
||||
mInput = std::move( input );
|
||||
mLeftPlugin->setHeader( mInput.stage2.label, mInput.stage2.present,
|
||||
mInput.missingVersionLabel );
|
||||
mResultPlugin->setHeader( mInput.resultLabel, true, mInput.missingVersionLabel );
|
||||
mRightPlugin->setHeader( mInput.stage3.label, mInput.stage3.present,
|
||||
mInput.missingVersionLabel );
|
||||
if ( mInput.resultDocument )
|
||||
mResultEditor->setDocument( mInput.resultDocument );
|
||||
const auto& syntaxDefinition = mResultEditor->getDocument().getSyntaxDefinition();
|
||||
const auto makeSideDocument = [&syntaxDefinition]( const MergeVersion& version ) {
|
||||
auto document = std::make_shared<Doc::TextDocument>();
|
||||
if ( version.present )
|
||||
document->textInput( version.text );
|
||||
document->setSyntaxDefinition( syntaxDefinition );
|
||||
document->setSelection( { 0, 0 } );
|
||||
return document;
|
||||
};
|
||||
mLeftEditor->setDocument( makeSideDocument( mInput.stage2 ) );
|
||||
mRightEditor->setDocument( makeSideDocument( mInput.stage3 ) );
|
||||
mOriginalResultText = mResultEditor->getDocument().getText();
|
||||
refreshBlocks();
|
||||
refreshHighlights();
|
||||
}
|
||||
|
||||
void UIMergeView::refreshHighlights() {
|
||||
using LineType = UIMergeEditorPlugin::LineType;
|
||||
const auto& leftDocument = mLeftEditor->getDocument();
|
||||
const auto& resultDocument = mResultEditor->getDocument();
|
||||
const auto& rightDocument = mRightEditor->getDocument();
|
||||
Doc::TextDocument::ScopedReadLock leftLock( leftDocument );
|
||||
Doc::TextDocument::ScopedReadLock resultLock( resultDocument );
|
||||
Doc::TextDocument::ScopedReadLock rightLock( rightDocument );
|
||||
const auto lineViews = []( const Doc::TextDocument& document ) {
|
||||
const size_t lineCount = document.linesCount();
|
||||
std::vector<String::View> lines;
|
||||
lines.reserve( lineCount );
|
||||
for ( size_t line = 0; line < lineCount; ++line )
|
||||
lines.emplace_back( document.line( line ).getTextViewWithoutNewLine() );
|
||||
return lines;
|
||||
};
|
||||
const auto leftLines = lineViews( leftDocument );
|
||||
const auto resultLines = lineViews( resultDocument );
|
||||
const auto rightLines = lineViews( rightDocument );
|
||||
std::vector<LineType> leftTypes( leftLines.size(), LineType::Common );
|
||||
std::vector<LineType> resultTypes( resultLines.size(), LineType::Common );
|
||||
std::vector<LineType> rightTypes( rightLines.size(), LineType::Common );
|
||||
const auto mergeType = []( LineType& current, LineType type ) {
|
||||
if ( current == LineType::Common )
|
||||
current = type;
|
||||
else if ( current != type )
|
||||
current = LineType::Conflict;
|
||||
};
|
||||
const auto computeDiff = [&mergeType]( const std::vector<String::View>& oldLines,
|
||||
const std::vector<String::View>& newLines,
|
||||
std::vector<LineType>& oldTypes,
|
||||
std::vector<LineType>& newTypes ) {
|
||||
dtl::Diff<String::View> diff( oldLines, newLines );
|
||||
diff.compose();
|
||||
size_t oldLine = 0;
|
||||
size_t newLine = 0;
|
||||
for ( const auto& entry : diff.getSes().getSequence() ) {
|
||||
switch ( entry.second.type ) {
|
||||
case dtl::SES_COMMON:
|
||||
++oldLine;
|
||||
++newLine;
|
||||
break;
|
||||
case dtl::SES_DELETE:
|
||||
if ( oldLine < oldTypes.size() )
|
||||
mergeType( oldTypes[oldLine], LineType::Removed );
|
||||
++oldLine;
|
||||
break;
|
||||
case dtl::SES_ADD:
|
||||
if ( newLine < newTypes.size() )
|
||||
mergeType( newTypes[newLine], LineType::Added );
|
||||
++newLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
computeDiff( leftLines, resultLines, leftTypes, resultTypes );
|
||||
computeDiff( resultLines, rightLines, resultTypes, rightTypes );
|
||||
|
||||
for ( const auto& block : mBlocks ) {
|
||||
const Int64 start = std::max<Int64>( 0, block.range.start().line() );
|
||||
const Int64 end = std::min<Int64>( resultTypes.size(), block.range.end().line() );
|
||||
for ( Int64 line = start; line < end; ++line )
|
||||
resultTypes[line] = LineType::Conflict;
|
||||
}
|
||||
mLeftPlugin->setLines( std::move( leftTypes ) );
|
||||
mResultPlugin->setLines( std::move( resultTypes ) );
|
||||
mRightPlugin->setLines( std::move( rightTypes ) );
|
||||
for ( auto* editor : { mLeftEditor, mResultEditor, mRightEditor } )
|
||||
editor->invalidateDraw();
|
||||
}
|
||||
|
||||
std::vector<UIMergeView::ConflictBlock> UIMergeView::parseConflictBlocks( const String& text ) {
|
||||
std::vector<ConflictBlock> blocks;
|
||||
const auto markerWidth = []( std::string_view line, char marker ) {
|
||||
size_t width = 0;
|
||||
while ( width < line.size() && line[width] == marker )
|
||||
++width;
|
||||
return width >= 7 ? width : size_t{ 0 };
|
||||
};
|
||||
enum class ParseState { Outside, Stage2, Base, Stage3 };
|
||||
ParseState state = ParseState::Outside;
|
||||
size_t lineNumber = 0;
|
||||
size_t start = 0;
|
||||
size_t width = 0;
|
||||
std::string stage2;
|
||||
std::string stage3;
|
||||
bool firstStage2Line = true;
|
||||
bool firstStage3Line = true;
|
||||
const auto appendLine = []( std::string& destination, bool& firstLine, std::string_view line ) {
|
||||
if ( !firstLine )
|
||||
destination += '\n';
|
||||
destination.append( line.data(), line.size() );
|
||||
firstLine = false;
|
||||
};
|
||||
const auto processLine = [&]( std::string_view line ) {
|
||||
switch ( state ) {
|
||||
case ParseState::Outside:
|
||||
width = markerWidth( line, '<' );
|
||||
if ( width != 0 ) {
|
||||
start = lineNumber;
|
||||
stage2.clear();
|
||||
stage3.clear();
|
||||
firstStage2Line = true;
|
||||
firstStage3Line = true;
|
||||
state = ParseState::Stage2;
|
||||
}
|
||||
break;
|
||||
case ParseState::Stage2:
|
||||
if ( markerWidth( line, '|' ) == width )
|
||||
state = ParseState::Base;
|
||||
else if ( markerWidth( line, '=' ) == width )
|
||||
state = ParseState::Stage3;
|
||||
else
|
||||
appendLine( stage2, firstStage2Line, line );
|
||||
break;
|
||||
case ParseState::Base:
|
||||
if ( markerWidth( line, '=' ) == width )
|
||||
state = ParseState::Stage3;
|
||||
break;
|
||||
case ParseState::Stage3:
|
||||
if ( markerWidth( line, '>' ) == width ) {
|
||||
ConflictBlock block;
|
||||
block.range = { { static_cast<Int64>( start ), 0 },
|
||||
{ static_cast<Int64>( lineNumber + 1 ), 0 } };
|
||||
block.stage2 = String::fromUtf8( stage2 );
|
||||
block.stage3 = String::fromUtf8( stage3 );
|
||||
blocks.emplace_back( std::move( block ) );
|
||||
state = ParseState::Outside;
|
||||
} else {
|
||||
appendLine( stage3, firstStage3Line, line );
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
const std::string utf8Text = text.toUtf8();
|
||||
bool processedCurrentLine = false;
|
||||
String::splitCb(
|
||||
[&]( std::string_view token ) {
|
||||
if ( token == "\n" ) {
|
||||
if ( !processedCurrentLine )
|
||||
processLine( {} );
|
||||
processedCurrentLine = false;
|
||||
++lineNumber;
|
||||
} else {
|
||||
processLine( token );
|
||||
processedCurrentLine = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
utf8Text, "", "\n", "" );
|
||||
return blocks;
|
||||
}
|
||||
|
||||
void UIMergeView::refreshBlocks() {
|
||||
mBlocks = mResultEditor->hasDocument()
|
||||
? parseConflictBlocks( mResultEditor->getDocument().getText() )
|
||||
: std::vector<ConflictBlock>{};
|
||||
if ( mCurrentBlock >= mBlocks.size() )
|
||||
mCurrentBlock = mBlocks.empty() ? 0 : mBlocks.size() - 1;
|
||||
}
|
||||
|
||||
bool UIMergeView::hasUnresolvedMarkerBlocks() const {
|
||||
return !mBlocks.empty();
|
||||
}
|
||||
|
||||
void UIMergeView::recreateConflict() {
|
||||
if ( !mResultEditor->hasDocument() ||
|
||||
mResultEditor->getDocument().getText() == mOriginalResultText )
|
||||
return;
|
||||
auto& document = mResultEditor->getDocument();
|
||||
document.selectAll();
|
||||
mApplyingBlock = true;
|
||||
document.textInput( mOriginalResultText );
|
||||
mApplyingBlock = false;
|
||||
refreshBlocks();
|
||||
refreshHighlights();
|
||||
}
|
||||
|
||||
void UIMergeView::goToNextConflict() {
|
||||
refreshBlocks();
|
||||
if ( mBlocks.empty() )
|
||||
return;
|
||||
const Int64 line = mResultEditor->getDocument().getSelection().start().line();
|
||||
auto next = std::find_if( mBlocks.begin(), mBlocks.end(), [line]( const auto& block ) {
|
||||
return block.range.start().line() > line;
|
||||
} );
|
||||
goToConflict( next == mBlocks.end() ? 0 : std::distance( mBlocks.begin(), next ) );
|
||||
}
|
||||
|
||||
void UIMergeView::goToPreviousConflict() {
|
||||
refreshBlocks();
|
||||
if ( mBlocks.empty() )
|
||||
return;
|
||||
const Int64 line = mResultEditor->getDocument().getSelection().start().line();
|
||||
auto previous = std::find_if( mBlocks.rbegin(), mBlocks.rend(), [line]( const auto& block ) {
|
||||
return block.range.start().line() < line;
|
||||
} );
|
||||
goToConflict( previous == mBlocks.rend() ? mBlocks.size() - 1
|
||||
: std::distance( previous, mBlocks.rend() ) - 1 );
|
||||
}
|
||||
|
||||
void UIMergeView::goToConflict( size_t block ) {
|
||||
if ( block >= mBlocks.size() )
|
||||
return;
|
||||
mCurrentBlock = block;
|
||||
const TextPosition position = mBlocks[block].range.start();
|
||||
mResultEditor->getDocument().setSelection( position );
|
||||
mResultEditor->goToLine( position );
|
||||
mResultEditor->setFocus();
|
||||
}
|
||||
|
||||
size_t UIMergeView::getCurrentConflictBlock() {
|
||||
const TextPosition cursor = mResultEditor->getDocument().getSelection().start();
|
||||
auto block = std::find_if( mBlocks.begin(), mBlocks.end(), [&cursor]( const auto& conflict ) {
|
||||
return conflict.range.contains( cursor );
|
||||
} );
|
||||
if ( block != mBlocks.end() )
|
||||
mCurrentBlock = std::distance( mBlocks.begin(), block );
|
||||
return std::min( mCurrentBlock, mBlocks.size() - 1 );
|
||||
}
|
||||
|
||||
void UIMergeView::acceptCurrentStage2() {
|
||||
refreshBlocks();
|
||||
if ( !mBlocks.empty() )
|
||||
acceptStage2( getCurrentConflictBlock() );
|
||||
}
|
||||
|
||||
void UIMergeView::acceptCurrentStage3() {
|
||||
refreshBlocks();
|
||||
if ( !mBlocks.empty() )
|
||||
acceptStage3( getCurrentConflictBlock() );
|
||||
}
|
||||
|
||||
void UIMergeView::acceptCurrentBoth() {
|
||||
refreshBlocks();
|
||||
if ( !mBlocks.empty() )
|
||||
acceptBoth( getCurrentConflictBlock() );
|
||||
}
|
||||
|
||||
void UIMergeView::replaceBlock( size_t block, const String& replacement ) {
|
||||
if ( block >= mBlocks.size() )
|
||||
return;
|
||||
auto& document = mResultEditor->getDocument();
|
||||
const TextRange range = mBlocks[block].range;
|
||||
String text( replacement );
|
||||
if ( !text.empty() && text.back() != '\n' &&
|
||||
range.end().line() < static_cast<Int64>( document.linesCount() ) )
|
||||
text += '\n';
|
||||
document.setSelection( range );
|
||||
mApplyingBlock = true;
|
||||
document.textInput( text );
|
||||
mApplyingBlock = false;
|
||||
refreshBlocks();
|
||||
refreshHighlights();
|
||||
}
|
||||
|
||||
void UIMergeView::acceptStage2( size_t block ) {
|
||||
refreshBlocks();
|
||||
if ( block < mBlocks.size() )
|
||||
replaceBlock( block, String( mBlocks[block].stage2 ) );
|
||||
}
|
||||
|
||||
void UIMergeView::acceptStage3( size_t block ) {
|
||||
refreshBlocks();
|
||||
if ( block < mBlocks.size() )
|
||||
replaceBlock( block, String( mBlocks[block].stage3 ) );
|
||||
}
|
||||
|
||||
void UIMergeView::acceptBoth( size_t block, Order order ) {
|
||||
refreshBlocks();
|
||||
if ( block >= mBlocks.size() )
|
||||
return;
|
||||
const auto& first =
|
||||
order == Order::Stage2ThenStage3 ? mBlocks[block].stage2 : mBlocks[block].stage3;
|
||||
const auto& second =
|
||||
order == Order::Stage2ThenStage3 ? mBlocks[block].stage3 : mBlocks[block].stage2;
|
||||
replaceBlock( block, first + ( first.empty() || second.empty() ? "" : "\n" ) + second );
|
||||
}
|
||||
|
||||
void UIMergeView::setSyntaxColorScheme( const SyntaxColorScheme& colorScheme ) {
|
||||
for ( auto* editor : { mLeftEditor, mResultEditor, mRightEditor } )
|
||||
editor->setColorScheme( colorScheme );
|
||||
}
|
||||
|
||||
void UIMergeView::setToolbarVisible( bool visible ) {
|
||||
if ( mToolbarVisible == visible )
|
||||
return;
|
||||
mToolbarVisible = visible;
|
||||
mToolbar->setVisible( visible );
|
||||
}
|
||||
|
||||
Uint32 UIMergeView::onKeyDown( const KeyEvent& event ) {
|
||||
const Uint32 handled = executeMergeKeyBinding( event );
|
||||
return handled ? handled : UIWidget::onKeyDown( event );
|
||||
}
|
||||
|
||||
bool UIMergeView::executeMergeKeyBinding( const KeyEvent& event ) {
|
||||
return WidgetCommandExecuter::onKeyDown( event ) != 0;
|
||||
}
|
||||
|
||||
} // namespace EE::UI::Tools
|
||||
@@ -143,6 +143,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis
|
||||
UIWidget( elementTag ),
|
||||
mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ),
|
||||
mDoc( std::make_shared<TextDocument>() ),
|
||||
mAsyncLifetime( this, this ),
|
||||
mDocView( mDoc, mFontStyleConfig,
|
||||
{ .textHints = TextHints::NoKerning, .tabStops = mTabStops } ),
|
||||
mBlinkTime( Seconds( 0.5f ) ),
|
||||
@@ -201,6 +202,8 @@ UICodeEditor::UICodeEditor( const bool& autoRegisterBaseCommands,
|
||||
UICodeEditor( "codeeditor", autoRegisterBaseCommands, autoRegisterBaseKeybindings ) {}
|
||||
|
||||
UICodeEditor::~UICodeEditor() {
|
||||
mAsyncLifetime.invalidate();
|
||||
|
||||
if ( getUISceneNode()->hasThreadPool() ) {
|
||||
Uint64 tag = reinterpret_cast<Uint64>( this );
|
||||
getUISceneNode()->getThreadPool()->removeWithTag( tag );
|
||||
@@ -535,32 +538,33 @@ bool UICodeEditor::loadAsyncFromFile(
|
||||
bool wasLocked = isLocked();
|
||||
if ( !wasLocked )
|
||||
setLocked( true );
|
||||
const auto lifetime = mAsyncLifetime.weakHandle();
|
||||
auto document = mDoc;
|
||||
bool ret = mDoc->loadAsyncFromFile(
|
||||
path, pool, [this, onLoaded, wasLocked]( TextDocument*, bool success ) {
|
||||
if ( !success ) {
|
||||
runOnMainThread( [this, onLoaded, wasLocked, success] {
|
||||
if ( !wasLocked )
|
||||
setLocked( false );
|
||||
path, pool, [lifetime, document, onLoaded, wasLocked]( TextDocument*, bool success ) {
|
||||
lifetime.run( [lifetime, onLoaded, wasLocked, document,
|
||||
success]( UICodeEditor* editor ) {
|
||||
if ( editor->mDoc != document ) {
|
||||
if ( onLoaded )
|
||||
onLoaded( mDoc, success );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
if ( mMinimapEnabled && getUISceneNode()->hasThreadPool() ) {
|
||||
mDoc->getHighlighter()->tokenizeAsync( getUISceneNode()->getThreadPool(), [this] {
|
||||
runOnMainThread( [this] { invalidateDraw(); } );
|
||||
} );
|
||||
}
|
||||
|
||||
if ( mDocView.isWrapEnabled() )
|
||||
mDocView.setPendingReconstruction( true );
|
||||
|
||||
runOnMainThread( [this, onLoaded, wasLocked, success] {
|
||||
onLoaded( document, success );
|
||||
return;
|
||||
}
|
||||
if ( !wasLocked )
|
||||
setLocked( false );
|
||||
onDocumentLoaded();
|
||||
editor->setLocked( false );
|
||||
if ( success ) {
|
||||
if ( editor->mMinimapEnabled && editor->getUISceneNode()->hasThreadPool() ) {
|
||||
editor->mDoc->getHighlighter()->tokenizeAsync(
|
||||
editor->getUISceneNode()->getThreadPool(), [lifetime] {
|
||||
lifetime.run(
|
||||
[]( UICodeEditor* editor ) { editor->invalidateDraw(); } );
|
||||
} );
|
||||
}
|
||||
if ( editor->mDocView.isWrapEnabled() )
|
||||
editor->mDocView.setPendingReconstruction( true );
|
||||
editor->onDocumentLoaded();
|
||||
}
|
||||
if ( onLoaded )
|
||||
onLoaded( mDoc, success );
|
||||
onLoaded( editor->mDoc, success );
|
||||
} );
|
||||
} );
|
||||
if ( !ret && !wasLocked )
|
||||
@@ -583,27 +587,36 @@ bool UICodeEditor::loadAsyncFromURL(
|
||||
bool wasLocked = isLocked();
|
||||
if ( !wasLocked )
|
||||
setLocked( true );
|
||||
const auto lifetime = mAsyncLifetime.weakHandle();
|
||||
auto document = mDoc;
|
||||
bool ret = mDoc->loadAsyncFromURL(
|
||||
url, headers,
|
||||
[this, onLoaded, wasLocked]( TextDocument*, bool success ) {
|
||||
if ( mMinimapEnabled && getUISceneNode()->hasThreadPool() )
|
||||
mDoc->getHighlighter()->tokenizeAsync( getUISceneNode()->getThreadPool(), [this] {
|
||||
runOnMainThread( [this] { invalidateDraw(); } );
|
||||
[lifetime, document, onLoaded, wasLocked]( TextDocument*, bool success ) {
|
||||
lifetime.run(
|
||||
[lifetime, document, success, onLoaded, wasLocked]( UICodeEditor* editor ) {
|
||||
if ( editor->mDoc != document ) {
|
||||
if ( onLoaded )
|
||||
onLoaded( document, success );
|
||||
return;
|
||||
}
|
||||
if ( editor->mMinimapEnabled && editor->getUISceneNode()->hasThreadPool() )
|
||||
editor->mDoc->getHighlighter()->tokenizeAsync(
|
||||
editor->getUISceneNode()->getThreadPool(), [lifetime] {
|
||||
lifetime.run(
|
||||
[]( UICodeEditor* editor ) { editor->invalidateDraw(); } );
|
||||
} );
|
||||
if ( !wasLocked )
|
||||
editor->setLocked( false );
|
||||
editor->onDocumentLoaded();
|
||||
if ( onLoaded )
|
||||
onLoaded( editor->mDoc, success );
|
||||
} );
|
||||
|
||||
runOnMainThread( [this, success, onLoaded, wasLocked] {
|
||||
if ( !wasLocked )
|
||||
setLocked( false );
|
||||
onDocumentLoaded();
|
||||
if ( onLoaded )
|
||||
onLoaded( mDoc, success );
|
||||
} );
|
||||
},
|
||||
[this]( const Http&, const Http::Request&, const Http::Response&,
|
||||
const Http::Request::Status& status, size_t /*totalBytes*/,
|
||||
size_t /*currentBytes*/ ) {
|
||||
[lifetime]( const Http&, const Http::Request&, const Http::Response&,
|
||||
const Http::Request::Status& status, size_t /*totalBytes*/,
|
||||
size_t /*currentBytes*/ ) {
|
||||
if ( status == Http::Request::ContentReceived ) {
|
||||
runOnMainThread( [this] { invalidateDraw(); } );
|
||||
lifetime.run( []( UICodeEditor* editor ) { editor->invalidateDraw(); } );
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <eepp/ui/css/stylesheetparser.hpp>
|
||||
#include <eepp/ui/tools/uidiffview.hpp>
|
||||
#include <eepp/ui/tools/uiimageviewer.hpp>
|
||||
#include <eepp/ui/tools/uimergeview.hpp>
|
||||
#include <eepp/ui/tools/uitextureviewer.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
@@ -247,6 +248,7 @@ void UIWidgetCreator::createBaseWidgetList() {
|
||||
registeredWidget["richtext"] = UIRichText::New;
|
||||
registeredWidget["textspan"] = UITextSpan::New;
|
||||
registeredWidget["markdownview"] = UIMarkdownView::New;
|
||||
registeredWidget["mergeview"] = Tools::UIMergeView::New;
|
||||
|
||||
// Aliases
|
||||
registeredWidget["hbox"] = UILinearLayout::NewHorizontal;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "utest.h"
|
||||
|
||||
#include "../../tools/ecode/plugins/git/git.hpp"
|
||||
#include <chrono>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
using namespace ecode;
|
||||
|
||||
namespace {
|
||||
|
||||
struct GitTempDirectory {
|
||||
GitTempDirectory() {
|
||||
path = std::filesystem::temp_directory_path() /
|
||||
( "eepp-git-conflict-" +
|
||||
std::to_string( std::chrono::steady_clock::now().time_since_epoch().count() ) );
|
||||
std::filesystem::create_directories( path );
|
||||
}
|
||||
~GitTempDirectory() { FileSystem::dirRemoveAll( path.string() ); }
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
UTEST( GitConflict, ParsesNulDelimitedStageRecordsAndUnusualPaths ) {
|
||||
if ( Sys::which( "git" ).empty() )
|
||||
UTEST_SKIP( "Git is not installed" );
|
||||
|
||||
const std::string oid( 40, 'a' );
|
||||
const std::string path = "space tab\tquote\" unicode-ñ newline\n.txt";
|
||||
std::string records = "100644 " + oid + " 1\t" + path;
|
||||
records += '\0';
|
||||
records += "100644 " + oid + " 2\t" + path;
|
||||
records += '\0';
|
||||
|
||||
auto state = Git::parseUnmergedIndex( records );
|
||||
EXPECT_TRUE( state.error.empty() );
|
||||
ASSERT_EQ( 1u, state.files.size() );
|
||||
EXPECT_STREQ( path.c_str(), state.files.front().path.c_str() );
|
||||
EXPECT_TRUE( state.files.front().base.has_value() );
|
||||
EXPECT_TRUE( state.files.front().stage2.has_value() );
|
||||
EXPECT_FALSE( state.files.front().stage3.has_value() );
|
||||
}
|
||||
|
||||
UTEST( GitConflict, RejectsMalformedUnmergedIndexRecords ) {
|
||||
if ( Sys::which( "git" ).empty() )
|
||||
UTEST_SKIP( "Git is not installed" );
|
||||
|
||||
auto state = Git::parseUnmergedIndex( "not an index record\0" );
|
||||
EXPECT_FALSE( state.error.empty() );
|
||||
EXPECT_TRUE( state.files.empty() );
|
||||
}
|
||||
|
||||
UTEST( GitConflict, DetectsRebaseDirectory ) {
|
||||
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;
|
||||
ASSERT_EQ( EXIT_SUCCESS,
|
||||
git.git( std::vector<std::string>{ "init" }, temp.path.string(), output ) );
|
||||
ASSERT_TRUE( FileSystem::makeDir( ( temp.path / ".git/rebase-merge" ).string(), true ) );
|
||||
EXPECT_EQ( Git::GitOperation::Rebase, git.operation( temp.path.string() ) );
|
||||
}
|
||||
|
||||
UTEST( GitConflict, KeepsMergeOperationAfterAllConflictsAreStaged ) {
|
||||
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", "eepp tests" } ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "config", "user.email", "eepp-tests@example.invalid" } ) );
|
||||
const std::string file = ( temp.path / "conflict.txt" ).string();
|
||||
ASSERT_TRUE( FileSystem::fileWrite( file, "base\n" ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "add", "conflict.txt" } ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "commit", "-m", "base" } ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "switch", "-c", "incoming" } ) );
|
||||
ASSERT_TRUE( FileSystem::fileWrite( file, "incoming\n" ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "commit", "-am", "incoming" } ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "switch", "main" } ) );
|
||||
ASSERT_TRUE( FileSystem::fileWrite( file, "current\n" ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "commit", "-am", "current" } ) );
|
||||
EXPECT_NE( EXIT_SUCCESS, run( { "merge", "incoming" } ) );
|
||||
ASSERT_TRUE( FileSystem::fileWrite( file, "resolved\n" ) );
|
||||
ASSERT_EQ( EXIT_SUCCESS, run( { "add", "conflict.txt" } ) );
|
||||
|
||||
auto state = git.conflictState( temp.path.string(), false );
|
||||
EXPECT_TRUE( state.error.empty() );
|
||||
EXPECT_FALSE( state.hasConflicts() );
|
||||
EXPECT_EQ( Git::GitOperation::Merge, state.operation );
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
|
||||
#include "../../tools/ecode/keybindingshelper.cpp"
|
||||
|
||||
@@ -15,6 +16,43 @@ using namespace EE::UI::Doc;
|
||||
using namespace EE::Scene;
|
||||
using namespace EE::System;
|
||||
|
||||
UTEST( MainThreadLifetime, InvalidatedCallbacksDoNotRun ) {
|
||||
UIApplication app( WindowSettings{ 320, 240, "eepp - main thread lifetime test" } );
|
||||
int owner = 42;
|
||||
MainThreadLifetime<int> lifetime( &owner, app.getUI() );
|
||||
auto weak = lifetime.weakHandle();
|
||||
bool called = false;
|
||||
weak.run( [&called]( int* ) { called = true; } );
|
||||
lifetime.invalidate();
|
||||
SceneManager::instance()->update();
|
||||
EXPECT_FALSE( called );
|
||||
}
|
||||
|
||||
UTEST( MainThreadLifetime, LiveCallbacksReceiveOwner ) {
|
||||
UIApplication app( WindowSettings{ 320, 240, "eepp - main thread lifetime test" } );
|
||||
int owner = 42;
|
||||
MainThreadLifetime<int> lifetime( &owner, app.getUI() );
|
||||
int value = 0;
|
||||
lifetime.weakHandle().run( [&value]( int* object ) { value = *object; } );
|
||||
SceneManager::instance()->update();
|
||||
EXPECT_EQ( 42, value );
|
||||
}
|
||||
|
||||
UTEST( MainThreadLifetime, DispatcherCanBeAttachedAfterConstruction ) {
|
||||
UIApplication app( WindowSettings{ 320, 240, "eepp - deferred dispatcher test" } );
|
||||
int owner = 42;
|
||||
MainThreadLifetime<int> lifetime( &owner, nullptr );
|
||||
bool called = false;
|
||||
lifetime.weakHandle().run( [&called]( int* ) { called = true; } );
|
||||
SceneManager::instance()->update();
|
||||
EXPECT_FALSE( called );
|
||||
|
||||
lifetime.setDispatcher( app.getUI() );
|
||||
lifetime.weakHandle().run( [&called]( int* ) { called = true; } );
|
||||
SceneManager::instance()->update();
|
||||
EXPECT_TRUE( called );
|
||||
}
|
||||
|
||||
static const std::string userCode = R"objcpp(#import "common.h"
|
||||
#import <cmath>
|
||||
#import <gdiplus.h>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <eepp/ui/tools/uiimageviewer.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
#include <eepp/ui/uiscrollview.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::UI;
|
||||
@@ -103,6 +104,32 @@ UTEST( UIDiffView, LoadFromPatchAndVerifyCleanText ) {
|
||||
eeDelete( diffView );
|
||||
}
|
||||
|
||||
UTEST( UIDiffView, MultiFileViewerUsesRequestedViewMode ) {
|
||||
UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } );
|
||||
std::string patchText = R"patch(diff --git a/first.txt b/first.txt
|
||||
--- a/first.txt
|
||||
+++ b/first.txt
|
||||
@@ -1 +1 @@
|
||||
-old
|
||||
+new
|
||||
diff --git a/second.txt b/second.txt
|
||||
--- a/second.txt
|
||||
+++ b/second.txt
|
||||
@@ -1 +1 @@
|
||||
-before
|
||||
+after
|
||||
)patch";
|
||||
|
||||
auto* viewer =
|
||||
UIDiffView::NewMultiFileDiffViewer( patchText, "", UIDiffView::ViewMode::SideBySide );
|
||||
auto diffViews = viewer->findAllByType<UIDiffView>( UI_TYPE_DIFF_VIEW );
|
||||
ASSERT_EQ( size_t{ 2 }, diffViews.size() );
|
||||
for ( const auto* diffView : diffViews )
|
||||
EXPECT_EQ( UIDiffView::ViewMode::SideBySide, diffView->getViewMode() );
|
||||
|
||||
eeDelete( viewer );
|
||||
}
|
||||
|
||||
UTEST( UIDiffView, LoadFromFileImageDiffUsesImageViewers ) {
|
||||
UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } );
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "utest.h"
|
||||
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
|
||||
#include <eepp/ui/tools/uimergeview.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::UI::Tools;
|
||||
|
||||
UTEST( UIMergeView, ParsesNormalConflictMarkers ) {
|
||||
auto blocks = UIMergeView::parseConflictBlocks(
|
||||
"before\n<<<<<<< ours\nleft one\nleft two\n=======\nright\n>>>>>>> theirs\nafter\n" );
|
||||
ASSERT_EQ( 1u, blocks.size() );
|
||||
EXPECT_TRUE( blocks[0].stage2 == "left one\nleft two" );
|
||||
EXPECT_TRUE( blocks[0].stage3 == "right" );
|
||||
EXPECT_EQ( 1, blocks[0].range.start().line() );
|
||||
EXPECT_EQ( 7, blocks[0].range.end().line() );
|
||||
}
|
||||
|
||||
UTEST( UIMergeView, ParsesDiff3WithoutTreatingBaseAsOurs ) {
|
||||
auto blocks = UIMergeView::parseConflictBlocks(
|
||||
"<<<<<<< ours\nleft\n||||||| base\nold\n=======\nright\n>>>>>>> theirs\n" );
|
||||
ASSERT_EQ( 1u, blocks.size() );
|
||||
EXPECT_TRUE( blocks[0].stage2 == "left" );
|
||||
EXPECT_TRUE( blocks[0].stage3 == "right" );
|
||||
}
|
||||
|
||||
UTEST( UIMergeView, PreservesEmptyLinesAndLineRanges ) {
|
||||
auto blocks = UIMergeView::parseConflictBlocks(
|
||||
"before\n<<<<<<< ours\n\nleft\n\n=======\n\nright\n\n>>>>>>> theirs\nafter" );
|
||||
ASSERT_EQ( 1u, blocks.size() );
|
||||
EXPECT_TRUE( blocks[0].stage2 == "\nleft\n" );
|
||||
EXPECT_TRUE( blocks[0].stage3 == "\nright\n" );
|
||||
EXPECT_EQ( 1, blocks[0].range.start().line() );
|
||||
EXPECT_EQ( 10, blocks[0].range.end().line() );
|
||||
}
|
||||
|
||||
UTEST( UIMergeView, SupportsConfiguredMarkerWidthsAndIgnoresMalformedBlocks ) {
|
||||
auto blocks = UIMergeView::parseConflictBlocks(
|
||||
"<<<<<<<<<< ours\nleft\n==========\nright\n>>>>>>>>>> theirs\n<<<<<<< incomplete\n" );
|
||||
ASSERT_EQ( 1u, blocks.size() );
|
||||
EXPECT_TRUE( blocks[0].stage2 == "left" );
|
||||
EXPECT_TRUE( blocks[0].stage3 == "right" );
|
||||
}
|
||||
|
||||
UTEST( UIMergeView, UsesSharedResultDocumentAndAcceptIsUndoable ) {
|
||||
UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } );
|
||||
auto result = std::make_shared<Doc::TextDocument>();
|
||||
result->setSyntaxDefinition(
|
||||
Doc::SyntaxDefinitionManager::instance()->getByLanguageName( "JavaScript" ) );
|
||||
const String conflicted = "<<<<<<< ours\nleft\n=======\nright\n>>>>>>> theirs\n";
|
||||
result->textInput( conflicted );
|
||||
result->resetUndoRedo();
|
||||
MergeInput input;
|
||||
input.resultDocument = result;
|
||||
input.stage2.present = true;
|
||||
input.stage2.text = "left";
|
||||
input.stage2.label = "Ours";
|
||||
input.stage3.present = true;
|
||||
input.stage3.text = "right";
|
||||
input.stage3.label = "Theirs";
|
||||
auto* view = UIMergeView::New();
|
||||
view->load( std::move( input ) );
|
||||
auto* toolbar = view->find<UIStackLayout>( "merge_toolbar" );
|
||||
auto* editorsLayout = view->find<UILinearLayout>( "merge_editors" );
|
||||
ASSERT_TRUE( toolbar != nullptr );
|
||||
ASSERT_TRUE( editorsLayout != nullptr );
|
||||
EXPECT_TRUE( view->getFirstChild() == toolbar );
|
||||
EXPECT_TRUE( toolbar->getNextNode() == editorsLayout );
|
||||
EXPECT_TRUE( view->getLeftEditor()->getParent() == editorsLayout );
|
||||
EXPECT_TRUE( view->getResultEditor()->getParent() == editorsLayout );
|
||||
EXPECT_TRUE( view->getRightEditor()->getParent() == editorsLayout );
|
||||
EXPECT_TRUE( view->getResultEditor()->getDocumentRef() == result );
|
||||
EXPECT_EQ( result->getSyntaxDefinition().getLanguageIndex(),
|
||||
view->getLeftEditor()->getDocument().getSyntaxDefinition().getLanguageIndex() );
|
||||
EXPECT_EQ( result->getSyntaxDefinition().getLanguageIndex(),
|
||||
view->getRightEditor()->getDocument().getSyntaxDefinition().getLanguageIndex() );
|
||||
EXPECT_FALSE( view->getLeftEditor()->getVerticalScrollBarEnabled() );
|
||||
EXPECT_FALSE( view->getResultEditor()->getVerticalScrollBarEnabled() );
|
||||
EXPECT_TRUE( view->getRightEditor()->getVerticalScrollBarEnabled() );
|
||||
EXPECT_TRUE( view->isToolbarVisible() );
|
||||
EXPECT_TRUE( view->hasCommand( "merge-accept-left" ) );
|
||||
view->setToolbarVisible( false );
|
||||
EXPECT_FALSE( view->isToolbarVisible() );
|
||||
view->setToolbarVisible( true );
|
||||
EXPECT_TRUE( view->execute( "merge-accept-left" ) );
|
||||
EXPECT_TRUE( result->getText() == "left\n" );
|
||||
result->undo();
|
||||
EXPECT_TRUE( result->getText() == conflicted );
|
||||
EXPECT_TRUE( view->execute( "merge-accept-left" ) );
|
||||
view->recreateConflict();
|
||||
EXPECT_TRUE( result->getText() == conflicted );
|
||||
const String twoConflicts =
|
||||
"before\n<<<<<<< ours\nleft one\n=======\nright one\n>>>>>>> theirs\nmiddle\n<<<<<<< "
|
||||
"ours\nleft two\n=======\nright two\n>>>>>>> theirs\nafter\n";
|
||||
result->selectAll();
|
||||
result->textInput( twoConflicts );
|
||||
result->setSelection( { 10, 0 } );
|
||||
EXPECT_TRUE( view->execute( "merge-accept-right" ) );
|
||||
EXPECT_TRUE( result->getText() == "before\n<<<<<<< ours\nleft one\n=======\nright one\n>>>>>>> "
|
||||
"theirs\nmiddle\nright two\nafter\n" );
|
||||
eeDelete( view );
|
||||
}
|
||||
@@ -2629,7 +2629,8 @@ void App::loadDiffFromMemory( const std::string& content, const std::string& ori
|
||||
if ( !icon )
|
||||
icon = getUISceneNode()->findIcon( "file" );
|
||||
|
||||
auto scrollView = UIDiffView::NewMultiFileDiffViewer( content, repoPath );
|
||||
auto scrollView =
|
||||
UIDiffView::NewMultiFileDiffViewer( content, repoPath, mConfig.editor.diffViewMode );
|
||||
auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle );
|
||||
if ( icon )
|
||||
tab->setIcon( icon->createDrawable( getMenuIconSize() ) );
|
||||
@@ -2684,7 +2685,8 @@ void App::loadDiffFromPath( const std::string& path ) {
|
||||
if ( !icon )
|
||||
icon = getUISceneNode()->findIcon( "file" );
|
||||
|
||||
auto scrollView = UIDiffView::NewMultiFileDiffViewer( content );
|
||||
auto scrollView =
|
||||
UIDiffView::NewMultiFileDiffViewer( content, "", mConfig.editor.diffViewMode );
|
||||
auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle );
|
||||
if ( icon )
|
||||
tab->setIcon( icon->createDrawable( getMenuIconSize() ) );
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "git.hpp"
|
||||
#include <eepp/core/containers.hpp>
|
||||
#include <eepp/system/clock.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/lock.hpp>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <eepp/system/sys.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
@@ -17,6 +19,7 @@ using namespace std::literals;
|
||||
namespace ecode {
|
||||
|
||||
static constexpr auto sNotCommittedYetHash = "0000000000000000000000000000000000000000";
|
||||
static constexpr std::string_view sAsciiWhitespace = " \t\r\n";
|
||||
|
||||
Git::Blame::Blame( const std::string& error ) : error( error ), line( 0 ) {}
|
||||
|
||||
@@ -39,6 +42,16 @@ Git::Git( const std::string& projectDir, const std::string& gitPath ) : mGitPath
|
||||
}
|
||||
|
||||
int Git::git( const std::string& args, const std::string& projectDir, std::string& buf ) const {
|
||||
return git( Process::parseArgs( args ), projectDir, buf );
|
||||
}
|
||||
|
||||
int Git::git( const std::vector<std::string>& args, const std::string& projectDir,
|
||||
std::string& buf ) const {
|
||||
return git( args, projectDir, buf, {} );
|
||||
}
|
||||
|
||||
int Git::git( const std::vector<std::string>& args, const std::string& projectDir, std::string& buf,
|
||||
std::string_view input ) const {
|
||||
Clock clock;
|
||||
buf.clear();
|
||||
Process p;
|
||||
@@ -49,17 +62,293 @@ int Git::git( const std::string& args, const std::string& projectDir, std::strin
|
||||
projectDir.empty() ? mProjectPath : projectDir ) ) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
p.readAllStdOut( buf );
|
||||
int retCode = 0;
|
||||
p.join( &retCode );
|
||||
if ( !input.empty() ) {
|
||||
size_t written = 0;
|
||||
while ( written < input.size() ) {
|
||||
const size_t count = p.write( input.substr( written ) );
|
||||
if ( count == 0 )
|
||||
break;
|
||||
written += count;
|
||||
}
|
||||
p.join( &retCode );
|
||||
p.readAllStdOut( buf );
|
||||
} else {
|
||||
p.readAllStdOut( buf );
|
||||
p.join( &retCode );
|
||||
}
|
||||
if ( !mSilent || retCode != EXIT_SUCCESS ) {
|
||||
const std::string joinedArgs = String::join( args );
|
||||
Log::instance()->writef( retCode != EXIT_SUCCESS ? LogLevel::Info : LogLevel::Debug,
|
||||
"GitPlugin cmd in %s (%d): %s %s",
|
||||
clock.getElapsedTime().toString(), retCode, mGitPath, args );
|
||||
clock.getElapsedTime().toString(), retCode, mGitPath, joinedArgs );
|
||||
}
|
||||
return retCode;
|
||||
}
|
||||
|
||||
Git::ConflictState Git::parseUnmergedIndex( const std::string& output ) {
|
||||
ConflictState state;
|
||||
UnorderedMap<std::string, size_t> fileIndices;
|
||||
size_t recordStart = 0;
|
||||
while ( recordStart < output.size() ) {
|
||||
const size_t recordEnd = output.find( '\0', recordStart );
|
||||
const size_t end = recordEnd == std::string::npos ? output.size() : recordEnd;
|
||||
const std::string_view record( output.data() + recordStart, end - recordStart );
|
||||
const size_t space = record.find( ' ' );
|
||||
const size_t secondSpace =
|
||||
space == std::string_view::npos ? space : record.find( ' ', space + 1 );
|
||||
const size_t tab = secondSpace == std::string_view::npos
|
||||
? secondSpace
|
||||
: record.find( '\t', secondSpace + 1 );
|
||||
if ( space == std::string_view::npos || secondSpace == std::string_view::npos ||
|
||||
tab == std::string_view::npos || tab <= secondSpace + 1 ) {
|
||||
state.error = "Invalid git ls-files --unmerged output";
|
||||
return state;
|
||||
}
|
||||
|
||||
ConflictStage conflictStage;
|
||||
try {
|
||||
conflictStage.mode = static_cast<Uint32>(
|
||||
std::stoul( std::string( record.substr( 0, space ) ), nullptr, 8 ) );
|
||||
conflictStage.stage = static_cast<Uint8>( std::stoul(
|
||||
std::string( record.substr( secondSpace + 1, tab - secondSpace - 1 ) ) ) );
|
||||
} catch ( const std::exception& ) {
|
||||
state.error = "Invalid mode or stage in git ls-files --unmerged output";
|
||||
return state;
|
||||
}
|
||||
conflictStage.objectId = std::string( record.substr( space + 1, secondSpace - space - 1 ) );
|
||||
std::string path( record.substr( tab + 1 ) );
|
||||
auto [it, inserted] = fileIndices.emplace( path, state.files.size() );
|
||||
if ( inserted )
|
||||
state.files.emplace_back( ConflictFile{ std::move( path ) } );
|
||||
auto& file = state.files[it->second];
|
||||
switch ( conflictStage.stage ) {
|
||||
case 1:
|
||||
file.base = std::move( conflictStage );
|
||||
break;
|
||||
case 2:
|
||||
file.stage2 = std::move( conflictStage );
|
||||
break;
|
||||
case 3:
|
||||
file.stage3 = std::move( conflictStage );
|
||||
break;
|
||||
default:
|
||||
state.error = "Invalid index stage in git ls-files --unmerged output";
|
||||
return state;
|
||||
}
|
||||
recordStart = end + 1;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Git::GitOperation Git::operation( const std::string& projectDir ) const {
|
||||
const auto hasRef = [this, &projectDir]( const char* ref ) {
|
||||
std::string output;
|
||||
return git( { "rev-parse", "-q", "--verify", ref }, projectDir, output ) == EXIT_SUCCESS;
|
||||
};
|
||||
if ( hasRef( "MERGE_HEAD" ) )
|
||||
return GitOperation::Merge;
|
||||
if ( hasRef( "CHERRY_PICK_HEAD" ) )
|
||||
return GitOperation::CherryPick;
|
||||
if ( hasRef( "REVERT_HEAD" ) )
|
||||
return GitOperation::Revert;
|
||||
|
||||
const auto hasGitPath = [this, &projectDir]( const char* name ) {
|
||||
std::string output;
|
||||
if ( git( { "rev-parse", "--git-path", name }, projectDir, output ) != EXIT_SUCCESS )
|
||||
return false;
|
||||
String::trimInPlace( output, sAsciiWhitespace );
|
||||
const bool absolute =
|
||||
!output.empty() && ( output.front() == '/' || output.front() == '\\' ||
|
||||
( output.size() > 1 && output[1] == ':' ) );
|
||||
if ( !absolute ) {
|
||||
const std::string& repo = projectDir.empty() ? mProjectPath : projectDir;
|
||||
output = repo + ( !repo.empty() && repo.back() == '/' ? "" : "/" ) + output;
|
||||
}
|
||||
return FileSystem::fileExists( output ) || FileSystem::isDirectory( output );
|
||||
};
|
||||
if ( hasGitPath( "rebase-merge" ) || hasGitPath( "rebase-apply" ) )
|
||||
return GitOperation::Rebase;
|
||||
if ( hasGitPath( "MERGE_AUTOSTASH" ) )
|
||||
return GitOperation::StashApply;
|
||||
return GitOperation::None;
|
||||
}
|
||||
|
||||
Git::ConflictState Git::conflictState( const std::string& projectDir, bool loadContents ) const {
|
||||
std::string output;
|
||||
const int ret = git( { "ls-files", "--unmerged", "--stage", "-z" }, projectDir, output );
|
||||
ConflictState state = parseUnmergedIndex( output );
|
||||
state.operation = operation( projectDir );
|
||||
if ( ret != EXIT_SUCCESS ) {
|
||||
state.error = std::move( output );
|
||||
state.files.clear();
|
||||
return state;
|
||||
}
|
||||
const std::string& repo = projectDir.empty() ? mProjectPath : projectDir;
|
||||
for ( auto& file : state.files ) {
|
||||
file.workingTreeExists = FileSystem::fileExists(
|
||||
repo + ( !repo.empty() && repo.back() == '/' ? "" : "/" ) + file.path );
|
||||
if ( !loadContents )
|
||||
continue;
|
||||
for ( auto* stage : { &file.base, &file.stage2, &file.stage3 } ) {
|
||||
if ( !stage->has_value() )
|
||||
continue;
|
||||
std::string contents;
|
||||
if ( git( { "cat-file", "blob", ( *stage )->objectId }, projectDir, contents ) !=
|
||||
EXIT_SUCCESS ) {
|
||||
state.error = std::move( contents );
|
||||
return state;
|
||||
}
|
||||
( *stage )->contents = std::move( contents );
|
||||
file.binary = file.binary || ( *stage )->contents.find( '\0' ) != std::string::npos;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Git::Result Git::resolveConflict( const std::string& path, bool remove,
|
||||
const std::string& projectDir ) const {
|
||||
Result result;
|
||||
result.returnCode = git( remove ? std::vector<std::string>{ "rm", "--", path }
|
||||
: std::vector<std::string>{ "add", "--", path },
|
||||
projectDir, result.result );
|
||||
return result;
|
||||
}
|
||||
|
||||
Git::Result Git::acceptConflictStage( const std::string& path, bool stage2, bool present,
|
||||
const std::string& projectDir ) const {
|
||||
if ( !present )
|
||||
return resolveConflict( path, true, projectDir );
|
||||
Result result;
|
||||
result.returnCode = git( { "checkout", stage2 ? "--ours" : "--theirs", "--", path }, projectDir,
|
||||
result.result );
|
||||
if ( result.success() )
|
||||
return resolveConflict( path, false, projectDir );
|
||||
return result;
|
||||
}
|
||||
|
||||
Git::Result Git::restoreConflictStages( const ConflictFile& conflict,
|
||||
const std::string& projectDir ) const {
|
||||
Result result;
|
||||
const ConflictStage* firstStage = conflict.base ? &*conflict.base
|
||||
: conflict.stage2 ? &*conflict.stage2
|
||||
: conflict.stage3 ? &*conflict.stage3
|
||||
: nullptr;
|
||||
if ( !firstStage ) {
|
||||
result.returnCode = EXIT_FAILURE;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string indexInfo;
|
||||
indexInfo.reserve( conflict.path.size() * 4 + 512 );
|
||||
indexInfo += "0 ";
|
||||
indexInfo.append( firstStage->objectId.size(), '0' );
|
||||
indexInfo += '\t';
|
||||
indexInfo += conflict.path;
|
||||
indexInfo += '\0';
|
||||
for ( const auto* stage : { &conflict.base, &conflict.stage2, &conflict.stage3 } ) {
|
||||
if ( !stage->has_value() )
|
||||
continue;
|
||||
char header[128];
|
||||
const int length = std::snprintf( header, sizeof( header ), "%06o %s %u\t",
|
||||
( *stage )->mode, ( *stage )->objectId.c_str(),
|
||||
static_cast<unsigned int>( ( *stage )->stage ) );
|
||||
if ( length <= 0 || static_cast<size_t>( length ) >= sizeof( header ) ) {
|
||||
result.returnCode = EXIT_FAILURE;
|
||||
return result;
|
||||
}
|
||||
indexInfo.append( header, static_cast<size_t>( length ) );
|
||||
indexInfo += conflict.path;
|
||||
indexInfo += '\0';
|
||||
}
|
||||
result.returnCode =
|
||||
git( { "update-index", "-z", "--index-info" }, projectDir, result.result, indexInfo );
|
||||
return result;
|
||||
}
|
||||
|
||||
Git::Result Git::preparedMergeMessage( const std::string& projectDir ) const {
|
||||
Result result;
|
||||
std::string path;
|
||||
result.returnCode = git( { "rev-parse", "--path-format=absolute", "--git-path", "MERGE_MSG" },
|
||||
projectDir, path );
|
||||
String::trimInPlace( path, sAsciiWhitespace );
|
||||
if ( result.success() && FileSystem::fileGet( path, result.result ) )
|
||||
return result;
|
||||
|
||||
std::string gitDir;
|
||||
result.returnCode =
|
||||
git( std::vector<std::string>{ "rev-parse", "--absolute-git-dir" }, projectDir, gitDir );
|
||||
String::trimInPlace( gitDir, sAsciiWhitespace );
|
||||
if ( result.success() ) {
|
||||
FileSystem::dirAddSlashAtEnd( gitDir );
|
||||
path = gitDir + "MERGE_MSG";
|
||||
if ( FileSystem::fileGet( path, result.result ) )
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string mergeName;
|
||||
result.returnCode = git( std::vector<std::string>{ "name-rev", "--name-only", "--no-undefined",
|
||||
"--refs=refs/heads/*",
|
||||
"--refs=refs/remotes/*", "MERGE_HEAD" },
|
||||
projectDir, mergeName );
|
||||
String::trimInPlace( mergeName, sAsciiWhitespace );
|
||||
if ( result.fail() || mergeName.empty() ) {
|
||||
result.returnCode = git( std::vector<std::string>{ "rev-parse", "--short", "MERGE_HEAD" },
|
||||
projectDir, mergeName );
|
||||
String::trimInPlace( mergeName, sAsciiWhitespace );
|
||||
}
|
||||
if ( result.success() && !mergeName.empty() ) {
|
||||
result.result = "Merge '" + mergeName + "'";
|
||||
return result;
|
||||
}
|
||||
|
||||
// MERGE_HEAD was already verified when the operation state was detected. Keep the commit
|
||||
// workflow usable even when the tool that initiated the merge did not create MERGE_MSG.
|
||||
result.returnCode = EXIT_SUCCESS;
|
||||
result.result = "Merge";
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<std::string> operationArgs( Git::GitOperation operation, bool abort ) {
|
||||
const char* action = abort ? "--abort" : "--continue";
|
||||
switch ( operation ) {
|
||||
case Git::GitOperation::Merge:
|
||||
return { "-c", "core.editor=true", "merge", action };
|
||||
case Git::GitOperation::Rebase:
|
||||
return { "-c", "core.editor=true", "rebase", action };
|
||||
case Git::GitOperation::CherryPick:
|
||||
return { "-c", "core.editor=true", "cherry-pick", action };
|
||||
case Git::GitOperation::Revert:
|
||||
return { "-c", "core.editor=true", "revert", action };
|
||||
case Git::GitOperation::None:
|
||||
case Git::GitOperation::StashApply:
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Git::Result Git::continueOperation( GitOperation operation, const std::string& projectDir ) const {
|
||||
Result result;
|
||||
auto args = operationArgs( operation, false );
|
||||
if ( args.empty() ) {
|
||||
result.returnCode = EXIT_FAILURE;
|
||||
return result;
|
||||
}
|
||||
result.returnCode = git( args, projectDir, result.result );
|
||||
return result;
|
||||
}
|
||||
|
||||
Git::Result Git::abortOperation( GitOperation operation, const std::string& projectDir ) const {
|
||||
Result result;
|
||||
auto args = operationArgs( operation, true );
|
||||
if ( args.empty() ) {
|
||||
result.returnCode = EXIT_FAILURE;
|
||||
return result;
|
||||
}
|
||||
result.returnCode = git( args, projectDir, result.result );
|
||||
return result;
|
||||
}
|
||||
|
||||
void Git::gitSubmodules( const std::string& args, const std::string& projectDir,
|
||||
std::string& buf ) {
|
||||
git( String::format( "submodule foreach \"git %s\"", args ), projectDir, buf );
|
||||
@@ -68,7 +357,7 @@ void Git::gitSubmodules( const std::string& args, const std::string& projectDir,
|
||||
bool Git::isGitRepo( const std::string& projectDir ) {
|
||||
std::string buf;
|
||||
git( "rev-parse --is-inside-work-tree", projectDir, buf );
|
||||
String::trimInPlace( buf );
|
||||
String::trimInPlace( buf, sAsciiWhitespace );
|
||||
return "true" == buf;
|
||||
}
|
||||
|
||||
@@ -308,7 +597,7 @@ Git::Result Git::mergeBranch( const std::string& branch, bool fastForward,
|
||||
}
|
||||
|
||||
Git::Result Git::commit( const std::string& commitMsg, bool amend, bool byPassCommitHook,
|
||||
const std::string& projectDir ) {
|
||||
const std::string& projectDir, bool cleanupComments ) {
|
||||
auto tmpPath = Sys::getTempPath() + ".ecode-git-commit-" + String::randString( 16 );
|
||||
if ( !FileSystem::fileWrite( tmpPath, commitMsg ) ) {
|
||||
Git::Result res;
|
||||
@@ -324,9 +613,9 @@ Git::Result Git::commit( const std::string& commitMsg, bool amend, bool byPassCo
|
||||
if ( byPassCommitHook )
|
||||
opts += " --no-verify";
|
||||
|
||||
int retCode = git(
|
||||
String::format( "commit %s --cleanup=whitespace --allow-empty --file=%s", opts, tmpPath ),
|
||||
projectDir, buf );
|
||||
int retCode = git( String::format( "commit %s --cleanup=%s --allow-empty --file=%s", opts,
|
||||
cleanupComments ? "strip" : "whitespace", tmpPath ),
|
||||
projectDir, buf );
|
||||
FileSystem::fileRemove( tmpPath );
|
||||
Git::Result res;
|
||||
res.returnCode = retCode;
|
||||
@@ -358,7 +647,7 @@ Git::CountResult Git::branchHistoryPosition( const std::string& localBranch,
|
||||
Git::CountResult res;
|
||||
res.returnCode = retCode;
|
||||
if ( res.success() ) {
|
||||
String::trimInPlace( buf );
|
||||
String::trimInPlace( buf, sAsciiWhitespace );
|
||||
auto results = String::split( buf, '\t' );
|
||||
if ( results.size() == 2 ) {
|
||||
Int64 behind = 0;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -18,6 +20,31 @@ namespace ecode {
|
||||
|
||||
class Git {
|
||||
public:
|
||||
enum class GitOperation : uint8_t { None, Merge, Rebase, CherryPick, Revert, StashApply };
|
||||
|
||||
struct ConflictStage {
|
||||
std::string objectId;
|
||||
std::string contents;
|
||||
uint32_t mode{ 0 };
|
||||
uint8_t stage{ 0 };
|
||||
};
|
||||
|
||||
struct ConflictFile {
|
||||
std::string path;
|
||||
std::optional<ConflictStage> base;
|
||||
std::optional<ConflictStage> stage2;
|
||||
std::optional<ConflictStage> stage3;
|
||||
bool workingTreeExists{ false };
|
||||
bool binary{ false };
|
||||
};
|
||||
|
||||
struct ConflictState {
|
||||
std::vector<ConflictFile> files;
|
||||
GitOperation operation{ GitOperation::None };
|
||||
std::string error;
|
||||
|
||||
bool hasConflicts() const { return !files.empty(); }
|
||||
};
|
||||
struct Blame {
|
||||
Blame( const std::string& error );
|
||||
|
||||
@@ -243,6 +270,34 @@ class Git {
|
||||
|
||||
int git( const std::string& args, const std::string& projectDir, std::string& buf ) const;
|
||||
|
||||
int git( const std::vector<std::string>& args, const std::string& projectDir,
|
||||
std::string& buf ) const;
|
||||
|
||||
int git( const std::vector<std::string>& args, const std::string& projectDir, std::string& buf,
|
||||
std::string_view input ) const;
|
||||
|
||||
static ConflictState parseUnmergedIndex( const std::string& output );
|
||||
|
||||
ConflictState conflictState( const std::string& projectDir = "",
|
||||
bool loadContents = true ) const;
|
||||
|
||||
GitOperation operation( const std::string& projectDir = "" ) const;
|
||||
|
||||
Result resolveConflict( const std::string& path, bool remove,
|
||||
const std::string& projectDir = "" ) const;
|
||||
|
||||
Result acceptConflictStage( const std::string& path, bool stage2, bool present,
|
||||
const std::string& projectDir = "" ) const;
|
||||
|
||||
Result restoreConflictStages( const ConflictFile& conflict,
|
||||
const std::string& projectDir = "" ) const;
|
||||
|
||||
Result preparedMergeMessage( const std::string& projectDir = "" ) const;
|
||||
|
||||
Result continueOperation( GitOperation operation, const std::string& projectDir = "" ) const;
|
||||
|
||||
Result abortOperation( GitOperation operation, const std::string& projectDir = "" ) const;
|
||||
|
||||
void gitSubmodules( const std::string& args, const std::string& projectDir, std::string& buf );
|
||||
|
||||
bool isGitRepo( const std::string& projectDir );
|
||||
@@ -286,7 +341,7 @@ class Git {
|
||||
const std::string& projectDir = "" );
|
||||
|
||||
Result commit( const std::string& commitMsg, bool amend, bool byPassCommitHook,
|
||||
const std::string& projectDir = "" );
|
||||
const std::string& projectDir = "", bool cleanupComments = false );
|
||||
|
||||
Result fetch( const std::string& projectDir = "" );
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,16 @@
|
||||
#include "../plugin.hpp"
|
||||
#include "../pluginmanager.hpp"
|
||||
#include "git.hpp"
|
||||
#include <eepp/scene/eventconnection.hpp>
|
||||
#include <eepp/scene/mainthreadlifetime.hpp>
|
||||
#include <eepp/ui/models/model.hpp>
|
||||
#include <eepp/ui/tools/uimergeview.hpp>
|
||||
#include <eepp/ui/uilinearlayout.hpp>
|
||||
#include <optional>
|
||||
|
||||
using namespace EE::UI::Models;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::Scene;
|
||||
|
||||
namespace EE::UI {
|
||||
class UITreeView;
|
||||
@@ -17,6 +21,7 @@ class UIDropDownList;
|
||||
class UIStackWidget;
|
||||
class UIListBoxItem;
|
||||
class UIMenu;
|
||||
class UITextView;
|
||||
} // namespace EE::UI
|
||||
|
||||
namespace ecode {
|
||||
@@ -84,7 +89,9 @@ class GitPlugin : public PluginBase {
|
||||
bool isSilent() const { return mSilent; }
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Git> mGit;
|
||||
MainThreadLifetime<GitPlugin> mLifetime;
|
||||
|
||||
std::shared_ptr<Git> mGit;
|
||||
std::unordered_map<std::string, std::string> mGitBranches;
|
||||
Git::Status mGitStatus;
|
||||
std::vector<std::pair<std::string, std::string>> mRepos;
|
||||
@@ -121,9 +128,13 @@ class GitPlugin : public PluginBase {
|
||||
std::vector<UIWidget*> mStackMap;
|
||||
UIWidget* mGitContentView{ nullptr };
|
||||
UIWidget* mGitNoContentView{ nullptr };
|
||||
UIWidget* mConflictStateBar{ nullptr };
|
||||
UITextView* mConflictStateText{ nullptr };
|
||||
UILoader* mLoader{ nullptr };
|
||||
std::atomic<int> mRunningUpdateBranches{ 0 };
|
||||
std::atomic<int> mRunningUpdateStatus{ 0 };
|
||||
std::atomic<bool> mPendingForcedStatusUpdate{ false };
|
||||
std::shared_ptr<std::atomic<int>> mRunningAsyncTasks{ std::make_shared<std::atomic<int>>( 0 ) };
|
||||
Clock mLastBranchesUpdate;
|
||||
Mutex mGitBranchMutex;
|
||||
Mutex mGitStatusMutex;
|
||||
@@ -131,6 +142,18 @@ class GitPlugin : public PluginBase {
|
||||
Mutex mRepoMutex;
|
||||
Mutex mReposMutex;
|
||||
String mLastCommitMsg;
|
||||
struct GitConflictSession {
|
||||
std::string repoPath;
|
||||
std::vector<std::string> files;
|
||||
size_t currentFile{ 0 };
|
||||
Uint64 generation{ 0 };
|
||||
Git::GitOperation operation{ Git::GitOperation::None };
|
||||
};
|
||||
UnorderedMap<std::string, std::unique_ptr<GitConflictSession>> mConflictSessions;
|
||||
std::string mActiveConflictRepo;
|
||||
Tools::UIMergeView* mConflictView{ nullptr };
|
||||
EE::Scene::EventConnection mConflictViewCloseConnection;
|
||||
Uint64 mConflictGeneration{ 0 };
|
||||
Uint32 mRepositionCbId{ 0 };
|
||||
|
||||
struct CustomTokenizer {
|
||||
@@ -176,7 +199,7 @@ class GitPlugin : public PluginBase {
|
||||
|
||||
void branchCreate();
|
||||
|
||||
void commit( const std::string& repoPath );
|
||||
void commit( const std::string& repoPath, bool mergeCommit = false );
|
||||
|
||||
void stage( const std::vector<std::string>& files );
|
||||
|
||||
@@ -198,6 +221,27 @@ class GitPlugin : public PluginBase {
|
||||
|
||||
void openFile( const std::string& file );
|
||||
|
||||
void openConflictResolver( const std::string& file );
|
||||
|
||||
void loadConflictResolverView( std::shared_ptr<Doc::TextDocument> resultDocument,
|
||||
Git::ConflictFile conflict, std::string repository,
|
||||
std::vector<std::string> files, size_t currentFile,
|
||||
Git::GitOperation operation, Uint64 generation );
|
||||
|
||||
void recreateConflict();
|
||||
|
||||
void saveAndStageConflict();
|
||||
|
||||
void openAdjacentConflict( bool next );
|
||||
|
||||
void continueConflictOperation();
|
||||
|
||||
void abortConflictOperation();
|
||||
|
||||
void acceptConflictSide( const std::string& file, bool stage2 );
|
||||
|
||||
void runAsyncTask( std::function<void()> task );
|
||||
|
||||
void updateStatus( bool force = false );
|
||||
|
||||
void updateStatusBarSync();
|
||||
@@ -225,6 +269,12 @@ class GitPlugin : public PluginBase {
|
||||
void runAsync( std::function<Git::Result()> fn, bool updateStatus, bool updateBranches,
|
||||
bool displaySuccessMsg = false, bool updateBranchesOnError = false,
|
||||
bool updateStatusOnError = false );
|
||||
void runMergeLikeAsync( std::function<Git::Result( Git& )> fn, const std::string& repoPath );
|
||||
|
||||
GitConflictSession* conflictSession( const std::string& repoPath );
|
||||
|
||||
GitConflictSession* activeConflictSession();
|
||||
bool updateConflictSessions( UnorderedMap<std::string, Git::ConflictState>& conflictStates );
|
||||
|
||||
void menuAdd( UIMenu* menu, const std::string& cmd, const std::string& text,
|
||||
const std::string& icon = "",
|
||||
|
||||
@@ -918,9 +918,9 @@ void SettingsPanel::addUserSettings( PanelState& panel ) {
|
||||
auto mode =
|
||||
selected == 0 ? UIDiffView::ViewMode::Unified : UIDiffView::ViewMode::SideBySide;
|
||||
mApp->getConfig().editor.diffViewMode = mode;
|
||||
mApp->getSplitter()->forEachWidgetType( UI_TYPE_DIFF_VIEW, [mode]( UIWidget* widget ) {
|
||||
widget->asType<UIDiffView>()->setViewMode( mode );
|
||||
} );
|
||||
for ( auto* diffView :
|
||||
mApp->getUISceneNode()->findAllByType<UIDiffView>( UI_TYPE_DIFF_VIEW ) )
|
||||
diffView->setViewMode( mode );
|
||||
} );
|
||||
auto addEditorBool = [this, &panel]( std::string id, const char* nameKey, const char* name,
|
||||
const char* descriptionKey, const char* description,
|
||||
|
||||
Reference in New Issue
Block a user