diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 222214b09..3d22fe538 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -1263,6 +1263,9 @@ Für sichtbare Änderung ecode neu starten. Aktualisieren Ältere Commits laden Noch keine Commits + Arbeitsbaum / Index + %zu geändert, %zu vorgemerkt + Nicht übernommene Änderungen Keine weiteren Commits Im Git-Verlauf anzeigen Git-Commit-Verlauf – %s diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index 8c203c154..27065ba12 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -1248,6 +1248,9 @@ Restart ecode to see the changes. Refresh Load older commits No commits yet + Working Tree / Index + %zu changed, %zu staged + Uncommitted changes No additional commits Show in Git History Git Commit History - %s diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index 55582671e..9be5a2b4b 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -1247,6 +1247,9 @@ Redémarrer ecode pour voir les changements. Actualiser Charger les commits précédents Aucun commit pour le moment + Arbre de travail / Index + %zu modifié(s), %zu indexé(s) + Modifications non validées Aucun commit supplémentaire Afficher dans l’historique Git Historique des commits Git – %s diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 3e815aa44..7e91ee8c9 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -1052,6 +1052,9 @@ file in the directory tree. 刷新 加载更早的提交 尚无提交 + 工作树 / 索引 + %zu 个更改,%zu 个已暂存 + 未提交的更改 没有其他提交 在 Git 历史记录中显示 Git 提交历史记录 - %s diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp index ef50cfb76..5126368e8 100644 --- a/include/eepp/ui/tools/uidiffview.hpp +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -45,21 +45,29 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { virtual ~UIDiffView(); virtual Uint32 getType() const override; + virtual bool isType( const Uint32& type ) const override; void loadFromPatch( const std::string& patchText, const std::string& originalFilePath = "", const std::string& oldFilePath = "", const std::string& repoPath = "" ); + void loadFromStrings( const std::string& oldText, const std::string& newText, const std::string& originalFilePath = "" ); + void loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ); UICodeEditor* getEditor() const { return mEditor; } + UICodeEditor* getLeftEditor() const { return mLeftEditor; } + UICodeEditor* getRightEditor() const { return mRightEditor; } + UIImageViewer* getLeftImageViewer() const { return mLeftImageViewer; } + UIImageViewer* getRightImageViewer() const { return mRightImageViewer; } enum class DiffLineType { Common, Added, Removed, Header }; + struct DiffLine { DiffLineType type{ DiffLineType::Common }; String text; @@ -69,27 +77,37 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { }; const std::vector& getDiffLines() const { return mLines; } + const std::vector& getViewLines() const { return mViewLines; } void setViewMode( ViewMode mode ); + ViewMode getViewMode() const { return mViewMode; } void setViewModeToggleVisible( bool visible ); + bool isViewModeToggleVisible() const { return mViewModeToggleVisible; } void setCompleteView( bool complete ); + bool isCompleteView() const { return mShowCompleteView; } void setCompleteViewToggleVisible( bool visible ); + bool isCompleteViewToggleVisible() const { return mCompleteViewToggleVisible; } void setSubLineDiffAlgorithm( SubLineDiffAlgorithm algo ); + SubLineDiffAlgorithm getSubLineDiffAlgorithm() const { return mSubLineDiffAlgorithm; } void setSyntaxColorScheme( const SyntaxColorScheme& colorScheme ); void setHeadersVisible( bool visible ); + void setInteractiveFileHeader( bool enabled ); + + bool isInteractiveFileHeader() const { return mInteractiveFileHeader; } + void setCollapsed( bool collapsed ); bool isCollapsed() const { return mCollapsed; } @@ -97,6 +115,14 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { const String& getFileName() const { return mFileName; } + const String& getFileDisplayName() const { return mFileDisplayName; } + + const String& getFileDisplayPath() const { return mFileDisplayPath; } + + const String& getAddedLinesText() const { return mAddedLinesText; } + + const String& getRemovedLinesText() const { return mRemovedLinesText; } + bool isImageDiff() const { return mIsImageDiff; } void setAutoDeleteOldTempImage( bool set ) { mAutoDeleteOldTempImage = set; } @@ -125,8 +151,13 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { bool mIsImageDiff{ false }; bool mAutoDeleteOldTempImage{ false }; bool mCollapsed{ false }; + bool mInteractiveFileHeader{ false }; std::shared_ptr mSyntaxDef; String mFileName; + String mFileDisplayName; + String mFileDisplayPath; + String mAddedLinesText; + String mRemovedLinesText; std::string mImageDiffOldPath; std::string mImageDiffNewPath; @@ -141,18 +172,32 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { virtual Uint32 onKeyDown( const KeyEvent& event ) override; void createEditor( UICodeEditor*& editor, std::unique_ptr& plugin ); + void syncScroll( UICodeEditor* source, UICodeEditor* target, bool emitEvent = false ); + void updateModeButton(); + void computeSubLineDiff( DiffLine& oldLine, DiffLine& newLine ); + void updateEditorsText(); + void updateButtonsText(); + void updateButtonsVisibility(); + void createImageViewers(); + bool loadImageDiffFromPaths( const std::string& oldFilePath, const std::string& newFilePath ); + void updateImageDiffView(); + void resetToTextDiffView(); + void imageDisplayState( bool& displayDiffImage, bool& displayLeftImage ); + void updateImagesPosAndSize(); + + void updateFileHeaderInfo(); }; } // namespace Tools diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index 7a19f2014..07fb6758d 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -11,10 +11,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -152,6 +154,20 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { if ( mView->areHeadersVisible() ) { editor->registerTopSpace( this, mPluginTopSpace, 0 ); } + + if ( mView->isInteractiveFileHeader() ) { + mHeaderIconWidth = 0; + const int iconSize = PixelDensity::dpToPxI( 14 ); + if ( auto* icon = editor->getUISceneNode()->findIcon( "chevron-down" ) ) + mExpandedIcon = icon->createDrawable( iconSize ); + if ( auto* icon = editor->getUISceneNode()->findIcon( "chevron-right" ) ) + mCollapsedIcon = icon->createDrawable( iconSize ); + if ( mExpandedIcon ) + mHeaderIconWidth = mExpandedIcon->getPixelsSize().getWidth(); + if ( mCollapsedIcon ) + mHeaderIconWidth = + std::max( mHeaderIconWidth, mCollapsedIcon->getPixelsSize().getWidth() ); + } } Float getPluginTopSpace() const { return mPluginTopSpace; } @@ -188,13 +204,88 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { 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 ); + const Uint32 textHints = mView->getFileName().getTextHints() | mView->getDefaultTextHints(); + Color textColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::Text ) ); + Color hintColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineNumber2 ) ); + FontStyleConfig textConfig; + textConfig.Font = font; + textConfig.CharacterSize = fontSize; + textConfig.FontColor = textColor; + Float left = screenStart.x + eefloor( PixelDensity::dpToPx( 8 ) ); + const Float gap = eefloor( PixelDensity::dpToPx( 6 ) ); - Text::draw( mView->getFileName(), pos, font, fontSize, textColor, 0, 0.f, Color::Black, - Color::Black, { 1, 1 }, 4, - mView->getFileName().getTextHints() | mView->getDefaultTextHints() ); + if ( mView->isInteractiveFileHeader() ) { + auto& icon = mView->isCollapsed() ? mCollapsedIcon : mExpandedIcon; + if ( icon ) { + icon->setColor( textColor ); + const Sizef iconSize = icon->getPixelsSize(); + icon->draw( + { left + eefloor( ( mHeaderIconWidth - iconSize.x ) * 0.5f ), + screenStart.y + eefloor( ( size.getHeight() - iconSize.y ) * 0.5f ) } ); + } + left += mHeaderIconWidth + gap; + } + + Vector2f pos( left, screenStart.y + textOffsetY ); + const String& fileName = mView->getFileDisplayName().empty() ? mView->getFileName() + : mView->getFileDisplayName(); + Text::draw( fileName, pos, font, fontSize, textColor, 0, 0.f, Color::Black, Color::Black, + { 1, 1 }, 4, textHints ); + pos.x += Text::getTextWidth( fileName, textConfig, 4, textHints ) + gap; + if ( !mView->getFileDisplayPath().empty() ) + Text::draw( mView->getFileDisplayPath(), pos, font, fontSize, hintColor, 0, 0.f, + Color::Black, Color::Black, { 1, 1 }, 4, textHints ); + + if ( mView->isInteractiveFileHeader() ) { + const auto variableColor = [editor]( const char* variable, Color fallback ) { + auto value = + editor->getUISceneNode()->getRoot()->getUIStyle()->getVariable( variable ); + return value.isEmpty() ? fallback : Color::fromString( value.getValue() ); + }; + const Color addedColor = variableColor( "--theme-success", Color( 0, 180, 60 ) ); + const Color removedColor = variableColor( "--theme-error", Color( 220, 50, 70 ) ); + FontStyleConfig addedConfig( textConfig ); + addedConfig.FontColor = addedColor; + FontStyleConfig removedConfig( textConfig ); + removedConfig.FontColor = removedColor; + const Float removedWidth = + Text::getTextWidth( mView->getRemovedLinesText(), removedConfig, 4, textHints ); + const Float addedWidth = + Text::getTextWidth( mView->getAddedLinesText(), addedConfig, 4, textHints ); + Float right = screenStart.x + width - eefloor( PixelDensity::dpToPx( 8 ) ); + right -= removedWidth; + Text::draw( mView->getRemovedLinesText(), { right, screenStart.y + textOffsetY }, font, + fontSize, removedColor, 0, 0.f, Color::Black, Color::Black, { 1, 1 }, 4, + textHints ); + right -= gap + addedWidth; + Text::draw( mView->getAddedLinesText(), { right, screenStart.y + textOffsetY }, font, + fontSize, addedColor, 0, 0.f, Color::Black, Color::Black, { 1, 1 }, 4, + textHints ); + } + } + + bool onMouseClick( UICodeEditor* editor, const Vector2i& position, + const Uint32& flags ) override { + if ( !mView->isInteractiveFileHeader() || !( flags & EE_BUTTON_LMASK ) ) + return false; + const Vector2f localPos( editor->convertToNodeSpace( position.asFloat() ) ); + if ( localPos.x >= 0 && localPos.x < editor->getTopAreaWidth() && localPos.y >= 0 && + localPos.y < mPluginTopSpace ) { + mView->setCollapsed( !mView->isCollapsed() ); + return true; + } + return false; + } + + bool onMouseMove( UICodeEditor* editor, const Vector2i& position, + const Uint32& /*flags*/ ) override { + if ( !mView->isInteractiveFileHeader() ) + return false; + const Vector2f localPos( editor->convertToNodeSpace( position.asFloat() ) ); + if ( localPos.x >= 0 && localPos.x < editor->getTopAreaWidth() && localPos.y >= 0 && + localPos.y < mPluginTopSpace ) + editor->getUISceneNode()->setCursor( Cursor::Hand ); + return false; } void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position, @@ -321,6 +412,9 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { UIDiffView* mView; Float mGutterWidth{ 0 }; Float mPluginTopSpace{ 0 }; + Float mHeaderIconWidth{ 0 }; + DrawablePtr mExpandedIcon; + DrawablePtr mCollapsedIcon; }; UIDiffView* UIDiffView::New() { @@ -1119,6 +1213,7 @@ void UIDiffView::loadFromPatch( const std::string& patchText, const std::string& if ( loadImageDiffFromPaths( oldImagePath, newImagePath ) ) { if ( !imagePatch.fileName.empty() ) mFileName = std::move( imagePatch.fileName ); + updateFileHeaderInfo(); return; } } @@ -1230,6 +1325,7 @@ void UIDiffView::loadFromPatch( const std::string& patchText, const std::string& SyntaxDefinitionManager::instance()->getLanguageDefinition( def.getLanguageIndex() ); mFileName = std::move( filename ); } + updateFileHeaderInfo(); updateEditorsText(); updateButtonsText(); @@ -1282,6 +1378,7 @@ void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& SyntaxDefinitionManager::instance()->getLanguageDefinition( def.getLanguageIndex() ); mFileName = FileSystem::fileNameFromPath( originalFilePath ); } + updateFileHeaderInfo(); updateEditorsText(); updateButtonsText(); @@ -1357,6 +1454,33 @@ void UIDiffView::setHeadersVisible( bool visible ) { updateModeButton(); } +void UIDiffView::setInteractiveFileHeader( bool enabled ) { + if ( enabled == mInteractiveFileHeader ) + return; + mInteractiveFileHeader = enabled; + mPlugin->registerUpdate( mEditor ); + mLeftPlugin->registerUpdate( mLeftEditor ); + mRightPlugin->registerUpdate( mRightEditor ); + mEditor->invalidateDraw(); + mLeftEditor->invalidateDraw(); + mRightEditor->invalidateDraw(); +} + +void UIDiffView::updateFileHeaderInfo() { + const std::string fileName( mFileName.toUtf8() ); + mFileDisplayName = String::fromUtf8( FileSystem::fileNameFromPath( fileName ) ); + mFileDisplayPath = String::fromUtf8( FileSystem::fileRemoveFileName( fileName ) ); + + std::size_t added = 0; + std::size_t removed = 0; + for ( const auto& line : mLines ) { + added += line.type == DiffLineType::Added; + removed += line.type == DiffLineType::Removed; + } + mAddedLinesText = String::format( "+ %zu", added ); + mRemovedLinesText = String::format( "- %zu", removed ); +} + Uint32 UIDiffView::onKeyDown( const KeyEvent& event ) { auto editor = mViewMode == ViewMode::Unified ? mEditor : mRightEditor; diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp index 5d1ff1f39..0e3a02f4b 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp @@ -747,8 +747,11 @@ void AutoCompletePlugin::onRegister( UICodeEditor* editor ) { std::vector listeners; listeners.push_back( editor->on( Event::OnDocumentLoaded, [this, editor]( const Event* ) { mDirty = true; - mDocs.insert( editor->getDocumentRef().get() ); - mEditorDocs[editor] = editor->getDocumentRef().get(); + { + Lock l( mDocMutex ); + mDocs.insert( editor->getDocumentRef().get() ); + mEditorDocs[editor] = editor->getDocumentRef().get(); + } tryRequestCapabilities( editor ); } ) ); @@ -2587,8 +2590,11 @@ AutoCompletePlugin::getDocumentSymbols( const std::shared_ptr& doc } ) ) symbols.push_back( std::move( matchStr ) ); } - if ( mShuttingDown || mDocs.find( doc ) == mDocs.end() ) - break; + { + Lock l( mDocMutex ); + if ( mShuttingDown || mDocs.find( doc ) == mDocs.end() ) + break; + } } return symbols; } diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 5211369da..3d7c4e56b 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -286,6 +286,64 @@ Git::CommitFiles Git::commitFiles( const Commit& commit, const std::string& proj return result; } +Git::CommitFiles Git::workingTreeFiles( const std::string& projectDir ) { + CommitFiles result; + Status current = status( false, projectDir ); + for ( const auto& [_, files] : current.files ) { + for ( const auto& file : files ) { + auto found = std::find_if( + result.files.begin(), result.files.end(), + [&file]( const CommitFile& item ) { return item.path == file.file; } ); + if ( found == result.files.end() ) { + CommitFile item; + item.path = file.file; + item.status = std::string( 1, static_cast( file.report.symbol ) ); + item.inserts = file.inserts; + item.deletes = file.deletes; + item.isBinary = file.isBinary; + result.files.emplace_back( std::move( item ) ); + } else { + found->inserts += file.inserts; + found->deletes += file.deletes; + found->isBinary |= file.isBinary; + } + } + } + + result.returnCode = git( { "diff", "--no-ext-diff", "--no-color", "-M", "HEAD", "--" }, + projectDir, result.patch ); + if ( result.fail() ) { + result.patch.clear(); + result.returnCode = + git( { "diff", "--no-ext-diff", "--no-color", "-M", "--" }, projectDir, result.patch ); + } + if ( result.fail() ) { + result.result = std::move( result.patch ); + return result; + } + + for ( const auto& file : result.files ) { + auto statusFile = + std::find_if( current.files.begin(), current.files.end(), [&file]( const auto& repo ) { + return std::any_of( repo.second.begin(), repo.second.end(), + [&]( const DiffFile& item ) { + return item.file == file.path && + item.report.type == GitStatusType::Untracked; + } ); + } ); + if ( statusFile == current.files.end() ) + continue; + auto patch = diffUntracked( file.path, projectDir ); + if ( patch.success() ) { + if ( !result.patch.empty() && result.patch.back() != '\n' ) + result.patch += '\n'; + result.patch += patch.result; + } + } + result.result.clear(); + return result; +} + Git::Result Git::commitDiff( const Commit& commit, const CommitFile& file, const std::string& projectDir ) const { Result result; diff --git a/src/tools/ecode/plugins/git/git.hpp b/src/tools/ecode/plugins/git/git.hpp index fcde7edd9..9ea2eda65 100644 --- a/src/tools/ecode/plugins/git/git.hpp +++ b/src/tools/ecode/plugins/git/git.hpp @@ -468,6 +468,8 @@ class Git { CommitFiles commitFiles( const Commit& commit, const std::string& projectDir = "" ) const; + CommitFiles workingTreeFiles( const std::string& projectDir = "" ); + Result commitDiff( const Commit& commit, const CommitFile& file, const std::string& projectDir = "" ) const; diff --git a/src/tools/ecode/plugins/git/githistorymodel.cpp b/src/tools/ecode/plugins/git/githistorymodel.cpp index 44025547a..5654b576a 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.cpp +++ b/src/tools/ecode/plugins/git/githistorymodel.cpp @@ -82,9 +82,11 @@ Variant GitHistoryModel::data( const ModelIndex& index, ModelRole role ) const { if ( !item ) return {}; if ( role == ModelRole::Class ) { + if ( item->type == NodeType::WorkingTree ) + return Variant( "git_history_working_tree" ); if ( item->type == NodeType::LoadMore || item->type == NodeType::Error ) return Variant( "git_history_action" ); - if ( item->type != NodeType::Commit ) + if ( item->type != NodeType::Commit && item->type != NodeType::WorkingTree ) return Variant( "git_history_secondary" ); if ( item->commit.isMerge() ) return Variant( "git_history_merge" ); @@ -99,7 +101,7 @@ Variant GitHistoryModel::data( const ModelIndex& index, ModelRole role ) const { } if ( role != ModelRole::Display ) return {}; - if ( item->type != NodeType::Commit ) + if ( item->type != NodeType::Commit && item->type != NodeType::WorkingTree ) return index.column() == Subject ? Variant( &item->message ) : Variant( "" ); switch ( index.column() ) { case Subject: @@ -138,6 +140,29 @@ GitHistoryModel::commitNode( Git::Commit commit, Node* parent, return item; } +std::unique_ptr +GitHistoryModel::workingTreeNode( const Git::Status& status, const std::string& repoName ) const { + auto repo = status.files.find( repoName ); + if ( repo == status.files.end() || repo->second.empty() ) + return {}; + size_t changed = 0; + size_t staged = 0; + for ( const auto& file : repo->second ) { + if ( file.report.type == Git::GitStatusType::Staged ) + ++staged; + else + ++changed; + } + auto item = std::make_unique(); + item->type = NodeType::WorkingTree; + item->subject = mPlugin->i18n( "git_working_tree_index", "Working Tree / Index" ); + item->message = String::format( + mPlugin->i18n( "git_working_tree_summary", "%zu changed, %zu staged" ).toUtf8(), changed, + staged ); + item->tooltip = item->subject + String{ "\n" } + item->message; + return item; +} + void GitHistoryModel::fillPage( Nodes& nodes, Node* parent, Git::HistoryPage page, const Git::HistoryQuery& query ) { for ( auto& commit : page.commits ) @@ -179,8 +204,11 @@ void GitHistoryModel::setRootLoading() { invalidate(); } -void GitHistoryModel::setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query ) { +void GitHistoryModel::setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query, + const Git::Status& status, const std::string& repoName ) { mRoots.clear(); + if ( auto item = workingTreeNode( status, repoName ) ) + mRoots.emplace_back( std::move( item ) ); fillPage( mRoots, nullptr, std::move( page ), query ); if ( mRoots.empty() ) { auto item = std::make_unique(); @@ -191,6 +219,25 @@ void GitHistoryModel::setRootPage( Git::HistoryPage page, const Git::HistoryQuer invalidate(); } +void GitHistoryModel::setWorkingTreeStatus( const Git::Status& status, + const std::string& repoName ) { + auto item = workingTreeNode( status, repoName ); + const bool hasItem = !mRoots.empty() && mRoots.front()->type == NodeType::WorkingTree; + if ( hasItem && !item ) { + beginDeleteRows( {}, 0, 0 ); + mRoots.erase( mRoots.begin() ); + endDeleteRows(); + } else if ( !hasItem && item ) { + beginInsertRows( {}, 0, 0 ); + mRoots.insert( mRoots.begin(), std::move( item ) ); + endInsertRows(); + } else if ( hasItem && item ) { + mRoots.front()->message = std::move( item->message ); + mRoots.front()->tooltip = std::move( item->tooltip ); + } + invalidate( Model::DontInvalidateIndexes ); +} + void GitHistoryModel::setRootError( std::string error ) { mRoots.clear(); auto item = std::make_unique(); diff --git a/src/tools/ecode/plugins/git/githistorymodel.hpp b/src/tools/ecode/plugins/git/githistorymodel.hpp index 277d908bb..e4bb5c5ac 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.hpp +++ b/src/tools/ecode/plugins/git/githistorymodel.hpp @@ -13,7 +13,7 @@ class GitPlugin; class GitHistoryModel : public Model { public: - enum class NodeType : uint8_t { Commit, LoadMore, Loading, Error, Empty }; + enum class NodeType : uint8_t { WorkingTree, Commit, LoadMore, Loading, Error, Empty }; enum Column { Subject, Date, Author, Hash }; struct Node { Git::Commit commit; @@ -65,7 +65,10 @@ class GitHistoryModel : public Model { void setRootLoading(); - void setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query ); + void setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query, + const Git::Status& status, const std::string& repoName ); + + void setWorkingTreeStatus( const Git::Status& status, const std::string& repoName ); void setRootError( std::string error ); @@ -94,6 +97,8 @@ class GitHistoryModel : public Model { std::unique_ptr commitNode( Git::Commit commit, Node* parent, const Git::HistoryQuery& query ) const; + std::unique_ptr workingTreeNode( const Git::Status& status, + const std::string& repoName ) const; void fillPage( Nodes& nodes, Node* parent, Git::HistoryPage page, const Git::HistoryQuery& query ); diff --git a/src/tools/ecode/plugins/git/githistorytreeview.cpp b/src/tools/ecode/plugins/git/githistorytreeview.cpp index 0e48190e7..bd10206d0 100644 --- a/src/tools/ecode/plugins/git/githistorytreeview.cpp +++ b/src/tools/ecode/plugins/git/githistorytreeview.cpp @@ -29,7 +29,8 @@ Sizef GitHistoryTreeViewCell::updateLayout() { void GitHistoryTreeViewCell::updateCell( Model* model ) { auto* historyModel = static_cast( model ); const auto* item = historyModel->node( getCurIndex() ); - if ( !item || item->type != GitHistoryModel::NodeType::Commit ) { + if ( !item || ( item->type != GitHistoryModel::NodeType::Commit && + item->type != GitHistoryModel::NodeType::WorkingTree ) ) { mMetadataText.setString( "" ); mTextBox->setTextAlign( UI_HALIGN_LEFT | UI_VALIGN_CENTER ); return; diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index 026a6a869..04725ebbf 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -739,6 +739,16 @@ void GitPlugin::updateStatus( bool force ) { [conflictStates = std::move( conflictStates )]( GitPlugin* plugin ) mutable { const bool selectStatusPanel = plugin->updateConflictSessions( conflictStates ); plugin->updateStatusBarSync(); + if ( plugin->mHistoryLoaded && plugin->mHistoryModel ) { + Git::Status status; + { + Lock l( plugin->mGitStatusMutex ); + status = plugin->mGitStatus; + } + const std::string repo = plugin->repoSelected(); + plugin->mHistoryModel->setWorkingTreeStatus( + status, plugin->mGit->repoName( repo, true, repo ) ); + } if ( selectStatusPanel && plugin->mPanelSwicher ) plugin->mPanelSwicher->getListBox()->setSelected( 1 ); } ); @@ -2437,18 +2447,22 @@ void GitPlugin::reloadHistory() { mThreadPool->run( [git = std::move( git ), lifetime, repo, generation, query]() mutable { auto page = git->history( query, repo ); - lifetime.run( - [repo, generation, query, page = std::move( page )]( GitPlugin* plugin ) mutable { - if ( plugin->mShuttingDown || generation != plugin->mHistoryGeneration || - repo != plugin->repoSelected() ) - return; - plugin->mHistoryLoaded = true; - if ( page.success() ) - plugin->mHistoryModel->setRootPage( std::move( page ), query ); - else - plugin->mHistoryModel->setRootError( std::move( page.result ) ); - plugin->focusDetachedHistory(); - } ); + auto status = git->status( false, repo ); + auto repoName = git->repoName( repo, true, repo ); + lifetime.run( [repo, generation, query, page = std::move( page ), + status = std::move( status ), + repoName = std::move( repoName )]( GitPlugin* plugin ) mutable { + if ( plugin->mShuttingDown || generation != plugin->mHistoryGeneration || + repo != plugin->repoSelected() ) + return; + plugin->mHistoryLoaded = true; + if ( page.success() ) + plugin->mHistoryModel->setRootPage( std::move( page ), query, status, + repoName ); + else + plugin->mHistoryModel->setRootError( std::move( page.result ) ); + plugin->focusDetachedHistory(); + } ); }, [this]( auto ) { --mRunningHistoryRequests; } ); } @@ -2626,9 +2640,12 @@ void GitPlugin::showGitHistory( const Git::Commit* commit ) { openHistoryMenu( modelEvent->getModelIndex() ); } ); mDetachedHistory.tree->setOnSelection( [this]( const ModelIndex& index ) { - if ( const auto* node = mHistoryModel ? mHistoryModel->node( index ) : nullptr; - node && node->type == GitHistoryModel::NodeType::Commit ) - openDetachedCommitDetails( node->commit ); + if ( const auto* node = mHistoryModel ? mHistoryModel->node( index ) : nullptr; node ) { + if ( node->type == GitHistoryModel::NodeType::WorkingTree ) + openWorkingTreeDetails( true ); + else if ( node->type == GitHistoryModel::NodeType::Commit ) + openDetachedCommitDetails( node->commit ); + } } ); auto* view = mDetachedHistory.view; mDetachedHistory.closeConnection = @@ -2666,16 +2683,24 @@ void GitPlugin::focusDetachedHistory() { if ( !index.isValid() ) return; mDetachedHistory.tree->setSelection( index, true, true ); - if ( const auto* node = mHistoryModel->node( index ); - node && node->type == GitHistoryModel::NodeType::Commit && - ( !mDetachedHistory.details.view || - mDetachedHistory.details.commit.hash != node->commit.hash ) ) - openDetachedCommitDetails( node->commit ); + if ( const auto* node = mHistoryModel->node( index ); node ) { + if ( node->type == GitHistoryModel::NodeType::WorkingTree ) + openWorkingTreeDetails( true ); + else if ( node->type == GitHistoryModel::NodeType::Commit && + ( !mDetachedHistory.details.view || + mDetachedHistory.details.commit.hash != node->commit.hash ) ) + openDetachedCommitDetails( node->commit ); + } } void GitPlugin::openDetachedCommitDetails( const Git::Commit& commit ) { if ( commit.hash.empty() || !mDetachedHistory.view ) return; + ensureDetachedCommitDetailsHost(); + mDetachedHistory.details.openCommitDetails( *this, commit, true ); +} + +void GitPlugin::ensureDetachedCommitDetailsHost() { if ( !mDetachedHistory.detailsHost ) { mDetachedHistory.detailsHost = UILinearLayout::NewVertical(); mDetachedHistory.detailsHost->setId( "git_history_detached_details" ); @@ -2684,30 +2709,38 @@ void GitPlugin::openDetachedCommitDetails( const Git::Commit& commit ) { mDetachedHistory.detailsHost->setParent( mDetachedHistory.view ); mDetachedHistory.view->setSplitPartition( StyleSheetLength( "55%" ) ); } - mDetachedHistory.details.openCommitDetails( *this, commit, true ); } void GitPlugin::openCommitDetails( const Git::Commit& commit ) { mCommitDetails.openCommitDetails( *this, commit, false ); } +void GitPlugin::openWorkingTreeDetails( bool detached ) { + Git::Commit commit; + commit.subject = i18n( "git_working_tree_index", "Working Tree / Index" ); + if ( detached ) + ensureDetachedCommitDetailsHost(); + ( detached ? mDetachedHistory.details : mCommitDetails ) + .openCommitDetails( *this, commit, detached, true ); +} + void GitPlugin::CommitDetailsState::openCommitDetails( GitPlugin& plugin, const Git::Commit& commit, - bool detached ) { - if ( commit.hash.empty() ) + bool detached, bool isWorkingTree ) { + if ( commit.hash.empty() && !isWorkingTree ) return; const std::string selectedRepo = plugin.repoSelected(); const Uint64 requestGeneration = ++generation; this->commit = commit; + workingTree = isWorkingTree; repo = selectedRepo; const bool createView = !view; if ( createView ) { view = plugin.getUISceneNode()->loadLayoutFromString( R"xml( - + - + setText( String::fromUtf8( commit.subject ) ); - author->setText( String::fromUtf8( commit.authorName ) ); - dateEmail->setText( Sys::epochToString( commit.commitTime ) + " - " + - String::fromUtf8( commit.authorEmail ) ); + author->setText( isWorkingTree ? plugin.i18n( "git_uncommitted_changes", "Uncommitted changes" ) + : String::fromUtf8( commit.authorName ) ); + dateEmail->setText( isWorkingTree ? String{} + : Sys::epochToString( commit.commitTime ) + " - " + + String::fromUtf8( commit.authorEmail ) ); messageBody.clear(); messageExpanded = false; message->setText( "" ); @@ -2835,13 +2870,16 @@ void GitPlugin::CommitDetailsState::openCommitDetails( GitPlugin& plugin, const filesToggle->setIcon( icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); url.clear(); gitHub->setVisible( false ); + view->find( "git_commit_sha" )->setVisible( !isWorkingTree ); + view->find( "git_commit_show_history" )->setVisible( !isWorkingTree ); diff = nullptr; diffContainer->closeAllChildren(); auto* shaButton = view->find( "git_commit_sha" ); shaButton->setTooltipText( String::format( plugin.i18n( "git_copy_commit_sha", "Copy Commit SHA\n%s" ).toUtf8(), commit.hash ) ); - const std::string tabName = commit.shortHash + " " + commit.subject; + const std::string tabName = + isWorkingTree ? commit.subject : commit.shortHash + " " + commit.subject; if ( !detached && !plugin.mManager->getSplitter()->ownedWidgetExists( view ) ) { auto tab = plugin.mManager->getSplitter()->createWidget( view, tabName, true ).first; if ( tab ) @@ -2861,16 +2899,20 @@ void GitPlugin::CommitDetailsState::loadCommitFiles( GitPlugin& plugin, bool det const Uint64 generation = this->generation; const std::string repo = this->repo; const Git::Commit commit = this->commit; + const bool workingTree = this->workingTree; auto git = plugin.mGit; const auto lifetime = plugin.mLifetime.weakHandle(); - plugin.runAsyncTask( [git = std::move( git ), lifetime, generation, repo, commit, detached] { - auto result = git->commitFiles( commit, repo ); - lifetime.run( [generation, repo, commit, detached, + plugin.runAsyncTask( [git = std::move( git ), lifetime, generation, repo, commit, detached, + workingTree] { + auto result = + workingTree ? git->workingTreeFiles( repo ) : git->commitFiles( commit, repo ); + lifetime.run( [generation, repo, commit, detached, workingTree, result = std::move( result )]( GitPlugin* plugin ) mutable { auto& details = detached ? plugin->mDetachedHistory.details : plugin->mCommitDetails; if ( plugin->mShuttingDown || generation != details.generation || repo != details.repo || repo != plugin->repoSelected() || - commit.hash != details.commit.hash || !details.view || + commit.hash != details.commit.hash || workingTree != details.workingTree || + !details.view || ( detached ? !plugin->mDetachedHistory.view : !plugin->mManager->getSplitter()->ownedWidgetExists( details.view ) ) ) @@ -2932,6 +2974,7 @@ void GitPlugin::CommitDetailsState::loadCommitFiles( GitPlugin& plugin, bool det SizePolicy::MatchParent ); details.diff->setParent( details.diffContainer ); for ( auto* diff : UIDiffView::multiFileDiffViews( details.diff ) ) { + diff->setInteractiveFileHeader( true ); if ( const auto* scheme = plugin->getPluginContext()->getCurrentColorScheme() ) diff->setSyntaxColorScheme( *scheme ); } @@ -3185,8 +3228,9 @@ void GitPlugin::buildSidePanelTab() { - - + + + @@ -3227,6 +3271,7 @@ void GitPlugin::buildSidePanelTab() { mTabContents->find( "branch_push" )->onClick( [this]( auto ) { push( repoSelected() ); } ); mTabContents->find( "branch_add" )->onClick( [this]( auto ) { branchCreate(); } ); mTabContents->find( "git_history_refresh" )->onClick( [this]( auto ) { reloadHistory(); } ); + mTabContents->find( "git_history_open" )->onClick( [this]( auto ) { showGitHistory(); } ); mHistoryRefDropDown->getListView()->setColumnsVisible( { 0 } ); mHistoryRefDropDown->getListView()->setAutoExpandOnSingleColumn( true ); mHistoryRefDropDown->on( Event::OnItemSelected, [this]( const Event* ) { @@ -3344,9 +3389,12 @@ void GitPlugin::buildSidePanelTab() { activateHistoryIndex( modelEvent->getModelIndex(), true ); else if ( modelEvent->getModelEventType() == ModelEventType::Open ) { activateHistoryIndex( modelEvent->getModelIndex(), false ); - if ( const auto* node = mHistoryModel->node( modelEvent->getModelIndex() ); - node && node->type == GitHistoryModel::NodeType::Commit ) - openCommitDetails( node->commit ); + if ( const auto* node = mHistoryModel->node( modelEvent->getModelIndex() ); node ) { + if ( node->type == GitHistoryModel::NodeType::WorkingTree ) + openWorkingTreeDetails( false ); + else if ( node->type == GitHistoryModel::NodeType::Commit ) + openCommitDetails( node->commit ); + } } else if ( modelEvent->getModelEventType() == ModelEventType::OpenMenu ) openHistoryMenu( modelEvent->getModelIndex() ); } ); @@ -3354,7 +3402,9 @@ void GitPlugin::buildSidePanelTab() { if ( !mHistoryModel ) return; const auto* node = mHistoryModel->node( index ); - if ( node && node->type == GitHistoryModel::NodeType::Commit ) + if ( node && node->type == GitHistoryModel::NodeType::WorkingTree ) + openWorkingTreeDetails( false ); + else if ( node && node->type == GitHistoryModel::NodeType::Commit ) openCommitDetails( node->commit ); } ); diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 45814d724..8f298fca3 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -190,8 +190,10 @@ class GitPlugin : public PluginBase { Tools::UIDiffView::ViewMode viewMode{ Tools::UIDiffView::ViewMode::Unified }; bool messageExpanded{ false }; bool filesCollapsed{ false }; + bool workingTree{ false }; - void openCommitDetails( GitPlugin& plugin, const Git::Commit& commit, bool detached ); + void openCommitDetails( GitPlugin& plugin, const Git::Commit& commit, bool detached, + bool workingTree = false ); void loadCommitFiles( GitPlugin& plugin, bool detached ); @@ -216,6 +218,7 @@ class GitPlugin : public PluginBase { viewMode = Tools::UIDiffView::ViewMode::Unified; messageExpanded = false; filesCollapsed = false; + workingTree = false; } }; struct DetachedHistoryState { @@ -374,6 +377,7 @@ class GitPlugin : public PluginBase { void activateHistoryIndex( const ModelIndex& index, bool expand ); void openCommitDetails( const Git::Commit& commit ); + void openWorkingTreeDetails( bool detached ); void showGitHistory( const Git::Commit* commit = nullptr ); @@ -382,6 +386,7 @@ class GitPlugin : public PluginBase { void openHistoryMenu( const ModelIndex& index ); void openDetachedCommitDetails( const Git::Commit& commit ); + void ensureDetachedCommitDetailsHost(); std::string detachedHistoryTitle();