From 36200430aa21f5dcce638af9442fcffec7c17f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 31 Aug 2026 11:03:19 -0300 Subject: [PATCH] Fix conflict discard and merge view copy commands - add Discard actions for conflicted files and conflict groups - restore discarded conflicts from HEAD in both index and working tree - restrict regular discard actions to changed tracked files - preserve default editor commands when replacing an editor document - enable copying from the read-only merge-view side panels - cover conflict restoration and merge-side copying - document build parallelism and function-spacing conventions --- .agent/rules/build-project.md | 2 + .agent/rules/project-introduction.md | 6 ++ include/eepp/ui/uicodeeditor.hpp | 4 +- src/eepp/ui/uicodeeditor.cpp | 8 ++- src/tests/unit_tests/gitconflict_tests.cpp | 11 ++++ src/tests/unit_tests/uimergeview_tests.cpp | 5 ++ src/tools/ecode/plugins/git/git.cpp | 9 +++ src/tools/ecode/plugins/git/git.hpp | 1 + src/tools/ecode/plugins/git/gitplugin.cpp | 75 ++++++++++++++++++---- src/tools/ecode/plugins/git/gitplugin.hpp | 3 +- 10 files changed, 104 insertions(+), 20 deletions(-) diff --git a/.agent/rules/build-project.md b/.agent/rules/build-project.md index 21f4902ee..90c3d29db 100644 --- a/.agent/rules/build-project.md +++ b/.agent/rules/build-project.md @@ -46,6 +46,8 @@ Run this **after** all edits and **before** attempting to compile. ## Step 2: Compile the Project To compile the project in debug mode, execute the `make` command, ensuring you point to the correct directory for your current Operating System. +### Build Parallelism + Always use all processors reported by the platform when selecting the parallel job count. On Linux and other systems with `nproc`, use `-j$(nproc)` exactly; do not substitute an arbitrary fixed value such as `-j4`. Use the platform-equivalent processor-count command where `nproc` is unavailable. diff --git a/.agent/rules/project-introduction.md b/.agent/rules/project-introduction.md index a3ce994f0..70a5681b9 100644 --- a/.agent/rules/project-introduction.md +++ b/.agent/rules/project-introduction.md @@ -43,6 +43,12 @@ declarations and unqualified eepp type names, such as `UISplitter`, over repeate qualified names such as `EE::UI::UISplitter`. Keep explicit qualification only where it is required to resolve ambiguity or avoid importing an unusually broad namespace into an unsuitable scope. +## Function Spacing + +Always leave one blank line between consecutive function declarations in headers and between +consecutive function definitions in implementation files. This is a project-wide stylistic +constraint, including short inline functions. + ## Control-Statement Braces Use braces around the body of an `if`, `else`, `for`, `while`, or similar control statement whenever diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index be682f42b..e6d582796 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -407,8 +407,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void addKeyBindsString( const std::map& binds, const bool& allowLocked = false ); - void addKeyBinds( const KeyBindings::ShortcutMap& binds, - const bool& allowLocked = false ); + void addKeyBinds( const KeyBindings::ShortcutMap& binds, const bool& allowLocked = false ); const bool& getHighlightCurrentLine() const; @@ -907,6 +906,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { std::shared_ptr mDoc; MainThreadLifetime mAsyncLifetime; bool mDirtyEditor{ false }; + bool mAutoRegisterBaseCommands{ true }; bool mDirtyScroll{ false }; bool mCursorVisible{ false }; bool mMouseDown{ false }; diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index e603467c1..3e9ddc60e 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -167,6 +167,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ), mDoc( std::make_shared() ), mAsyncLifetime( this, this ), + mAutoRegisterBaseCommands( autoRegisterBaseCommands ), mDocView( mDoc, mFontStyleConfig, { .textHints = TextHints::NoKerning, .tabStops = mTabStops } ), mBlinkTime( Seconds( 0.5f ) ), @@ -214,7 +215,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis mDoc->registerClient( this ); subscribeScheduledUpdate(); - if ( autoRegisterBaseCommands ) + if ( mAutoRegisterBaseCommands ) registerCommands(); if ( autoRegisterBaseKeybindings ) registerKeybindings(); @@ -1066,6 +1067,8 @@ void UICodeEditor::setDocument( std::shared_ptr doc ) { if ( clientsOfTypeCount == 1 || useCount == 1 ) onDocumentClosed( mDoc.get() ); mDoc = doc; + if ( mAutoRegisterBaseCommands ) + mDoc->setSharedRefCommands( getDefaultEditorCommands() ); mDoc->registerClient( this ); mDocView.setDocument( doc ); onDocumentChanged( oldDocURI ); @@ -2909,8 +2912,7 @@ void UICodeEditor::addKeyBindsString( const std::map& } } -void UICodeEditor::addKeyBinds( const KeyBindings::ShortcutMap& binds, - const bool& allowLocked ) { +void UICodeEditor::addKeyBinds( const KeyBindings::ShortcutMap& binds, const bool& allowLocked ) { mKeyBindings.addKeybinds( binds ); for ( const auto& bind : binds ) { if ( allowLocked ) { diff --git a/src/tests/unit_tests/gitconflict_tests.cpp b/src/tests/unit_tests/gitconflict_tests.cpp index 4d73ab5b9..9f574cee8 100644 --- a/src/tests/unit_tests/gitconflict_tests.cpp +++ b/src/tests/unit_tests/gitconflict_tests.cpp @@ -101,6 +101,17 @@ UTEST( GitConflict, KeepsMergeOperationAfterAllConflictsAreStaged ) { EXPECT_TRUE( state.error.empty() ); EXPECT_FALSE( state.hasConflicts() ); EXPECT_EQ( Git::GitOperation::Merge, state.operation ); + + ASSERT_EQ( EXIT_SUCCESS, run( { "merge", "--abort" } ) ); + EXPECT_NE( EXIT_SUCCESS, run( { "merge", "incoming" } ) ); + state = git.conflictState( temp.path.string(), false ); + EXPECT_TRUE( state.hasConflicts() ); + ASSERT_EQ( EXIT_SUCCESS, git.restoreHead( { "conflict.txt" }, temp.path.string() ).returnCode ); + state = git.conflictState( temp.path.string(), false ); + EXPECT_FALSE( state.hasConflicts() ); + std::string restored; + ASSERT_TRUE( FileSystem::fileGet( file, restored ) ); + EXPECT_TRUE( restored == "current\n" ); } UTEST( GitHistory, PaginatesFirstParentWithoutDuplicates ) { diff --git a/src/tests/unit_tests/uimergeview_tests.cpp b/src/tests/unit_tests/uimergeview_tests.cpp index 5d658570e..b32d7b3d7 100644 --- a/src/tests/unit_tests/uimergeview_tests.cpp +++ b/src/tests/unit_tests/uimergeview_tests.cpp @@ -2,6 +2,7 @@ #include #include #include +#include using namespace EE; using namespace EE::UI; @@ -80,6 +81,10 @@ UTEST( UIMergeView, UsesSharedResultDocumentAndAcceptIsUndoable ) { EXPECT_TRUE( view->getRightEditor()->getVerticalScrollBarEnabled() ); EXPECT_TRUE( view->isToolbarVisible() ); EXPECT_TRUE( view->hasCommand( "merge-accept-left" ) ); + auto* leftEditor = view->getLeftEditor(); + leftEditor->getDocument().setSelection( { { 0, 0 }, { 0, 4 } } ); + leftEditor->getDocument().execute( "copy", leftEditor ); + EXPECT_STREQ( "left", app.getWindow()->getClipboard()->getText().c_str() ); view->setToolbarVisible( false ); EXPECT_FALSE( view->isToolbarVisible() ); view->setToolbarVisible( true ); diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 494685016..29871d938 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -862,6 +862,15 @@ Git::Result Git::restore( const std::string& file, const std::string& projectDir return gitSimple( String::format( "restore \"%s\"", file ), projectDir ); } +Git::Result Git::restoreHead( const std::vector& files, + const std::string& projectDir ) { + Result result; + std::vector args{ "restore", "--source=HEAD", "--staged", "--worktree", "--" }; + args.insert( args.end(), files.begin(), files.end() ); + result.returnCode = git( args, projectDir, result.result ); + return result; +} + Git::Result Git::reset( std::vector files, const std::string& projectDir ) { return gitSimple( String::format( "reset -q HEAD -- %s", asList( files ) ), projectDir ); } diff --git a/src/tools/ecode/plugins/git/git.hpp b/src/tools/ecode/plugins/git/git.hpp index 8caba559c..586635f8f 100644 --- a/src/tools/ecode/plugins/git/git.hpp +++ b/src/tools/ecode/plugins/git/git.hpp @@ -359,6 +359,7 @@ class Git { Result restore( std::vector files, const std::string& projectDir = "" ); Result restore( const std::string& file, const std::string& projectDir = "" ); + Result restoreHead( const std::vector& files, const std::string& projectDir = "" ); Result reset( std::vector files, const std::string& projectDir = "" ); diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index 4aa90e9cc..618d7a260 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1488,6 +1488,9 @@ void GitPlugin::runFileOperation( std::vector files, FileOperation case FileOperation::Discard: result = mGit->restore( paths, repoPath ); break; + case FileOperation::RestoreHead: + result = mGit->restoreHead( paths, repoPath ); + break; } if ( result.fail() ) return result; @@ -1513,6 +1516,27 @@ void GitPlugin::discard( const std::vector& files ) { msgBox->showWhenReady(); } +void GitPlugin::discardConflicts( const std::vector& files ) { + if ( files.empty() ) + return; + String message = + files.size() == 1 + ? String::fromUtf8( String::format( + i18n( "git_confirm_discard_changes", + "Are you sure you want to discard the changes in file: \"%s\"?" ) + .toUtf8(), + files.front() ) ) + : i18n( "git_confirm_discard_changes", + "Are you sure you want to discard all file changes?" ); + UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, message ); + msgBox->on( Event::OnConfirm, + [this, files]( auto ) { runFileOperation( files, FileOperation::RestoreHead ); } ); + msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); + msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); + msgBox->center(); + msgBox->showWhenReady(); +} + void GitPlugin::discard( const std::string& file ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, @@ -3751,7 +3775,8 @@ void GitPlugin::buildSidePanelTab() { auto type = status->type; if ( type == Git::GitStatusType::Staged || type == Git::GitStatusType::Untracked || - type == Git::GitStatusType::Changed ) { + type == Git::GitStatusType::Changed || + type == Git::GitStatusType::Unmerged ) { std::string repoPath; if ( !status->files.empty() ) repoPath = mGit->repoPath( status->files.front().file ); @@ -3780,6 +3805,9 @@ void GitPlugin::buildSidePanelTab() { menuAdd( menu, "git-discard-all", i18n( "git_discard_all", "Discard All" ) ); } + if ( type == Git::GitStatusType::Unmerged ) + menuAdd( menu, "git-discard-all", + i18n( "git_discard_all", "Discard All" ) ); menu->on( Event::OnItemClicked, [this, modelShared, repoPath, type]( const Event* event ) { @@ -3797,8 +3825,12 @@ void GitPlugin::buildSidePanelTab() { unstage( model->getFiles( repoFullName( repoPath ), (Uint32)Git::GitStatusType::Staged ) ); } else if ( id == "git-discard-all" ) { - discard( model->getFiles( repoFullName( repoPath ), - (Uint32)Git::GitStatusType::Changed ) ); + auto discardFiles = model->getFiles( repoFullName( repoPath ), + static_cast( type ) ); + if ( type == Git::GitStatusType::Unmerged ) + discardConflicts( discardFiles ); + else + discard( discardFiles ); } else if ( id == "git-diff-staged" ) { diff( Git::DiffMode::DiffStaged, repoPath ); } else if ( id == "git-diff-changed" ) { @@ -4001,19 +4033,31 @@ void GitPlugin::openFileStatusMenu( std::vector files ) { hasUnstaged |= file.report.type != Git::GitStatusType::Staged; hasUnmerged |= file.report.type == Git::GitStatusType::Unmerged; } - if ( hasUnmerged && !multiple ) { - menuAdd( menu, "git-resolve-conflict", i18n( "git_resolve_conflict", "Resolve Conflict" ), - "diff-modified" ); - menuAdd( menu, "git-accept-ours", i18n( "git_accept_ours", "Accept Ours" ) ); - menuAdd( menu, "git-accept-theirs", i18n( "git_accept_theirs", "Accept Theirs" ) ); - menu->on( Event::OnItemClicked, [this, file = files.front()]( const Event* event ) { + if ( hasUnmerged ) { + if ( !multiple ) { + menuAdd( menu, "git-resolve-conflict", + i18n( "git_resolve_conflict", "Resolve Conflict" ), "diff-modified" ); + menuAdd( menu, "git-accept-ours", i18n( "git_accept_ours", "Accept Ours" ) ); + menuAdd( menu, "git-accept-theirs", i18n( "git_accept_theirs", "Accept Theirs" ) ); + } + menu->addSeparator(); + menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) ); + menu->on( Event::OnItemClicked, [this, files = std::move( files )]( const Event* event ) { const std::string id = event->getNode()->asType()->getId(); if ( id == "git-resolve-conflict" ) - openConflictResolver( file.file ); + openConflictResolver( files.front().file ); else if ( id == "git-accept-ours" ) - acceptConflictSide( file.file, true ); + acceptConflictSide( files.front().file, true ); else if ( id == "git-accept-theirs" ) - acceptConflictSide( file.file, false ); + acceptConflictSide( files.front().file, false ); + else if ( id == "git-discard" ) { + std::vector paths; + paths.reserve( files.size() ); + for ( const auto& file : files ) + if ( file.report.type == Git::GitStatusType::Unmerged ) + paths.emplace_back( file.file ); + discardConflicts( paths ); + } } ); menu->showOverMouseCursor(); return; @@ -4035,7 +4079,10 @@ void GitPlugin::openFileStatusMenu( std::vector files ) { menu->addSeparator(); - if ( hasUnstaged ) + const bool hasDiscardable = std::any_of( files.begin(), files.end(), []( const auto& file ) { + return file.report.type == Git::GitStatusType::Changed; + } ); + if ( hasDiscardable ) menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) ); menu->on( Event::OnItemClicked, @@ -4058,7 +4105,7 @@ void GitPlugin::openFileStatusMenu( std::vector files ) { unstage( paths ); } else if ( id == "git-discard" ) { for ( const auto& file : files ) - if ( file.report.type != Git::GitStatusType::Staged ) + if ( file.report.type == Git::GitStatusType::Changed ) paths.emplace_back( file.file ); if ( paths.size() == 1 ) discard( paths.front() ); diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 9cfc56fb6..b0c45c2d1 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -323,11 +323,12 @@ class GitPlugin : public PluginBase { void unstage( const std::vector& files ); - enum class FileOperation { Stage, Unstage, Discard }; + enum class FileOperation { Stage, Unstage, Discard, RestoreHead }; void runFileOperation( std::vector files, FileOperation operation ); void discard( const std::vector& files ); + void discardConflicts( const std::vector& files ); void discard( const std::string& file );