diff --git a/src/tools/ecode/applayout.xml.hpp b/src/tools/ecode/applayout.xml.hpp index 8411c3280..580173434 100644 --- a/src/tools/ecode/applayout.xml.hpp +++ b/src/tools/ecode/applayout.xml.hpp @@ -420,6 +420,12 @@ Anchor.error:hover { #code_container TabWidget::TabBar ScrollBarMini:focus-within { opacity: 1; } +.notbold { + font-style: normal; +} +.bold { + font-style: bold; +} )html" diff --git a/src/tools/ecode/featureshealth.cpp b/src/tools/ecode/featureshealth.cpp index 38f68a888..8c31e121f 100644 --- a/src/tools/ecode/featureshealth.cpp +++ b/src/tools/ecode/featureshealth.cpp @@ -266,9 +266,9 @@ class HealthModel : public Model { } virtual Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const { - static const char* SUCCESS = "theme-success"; - static const char* ERROR = "theme-error"; - static const char* NONE = "theme-none"; + static const char* HEALTH_SUCCESS = "theme-success"; + static const char* HEALTH_ERROR = "theme-error"; + static const char* HEALTH_NONE = "theme-none"; eeASSERT( index.row() < (Int64)mData.size() ); static std::string none; @@ -306,24 +306,24 @@ class HealthModel : public Model { case ModelRole::Class: { switch ( index.column() ) { case 1: - return Variant( SUCCESS ); + return Variant( HEALTH_SUCCESS ); case 2: if ( !lang.lsp.name.empty() ) - return Variant( lang.lsp.found ? SUCCESS : ERROR ); + return Variant( lang.lsp.found ? HEALTH_SUCCESS : HEALTH_ERROR ); else - return Variant( NONE ); + return Variant( HEALTH_NONE ); break; case 3: if ( !lang.linter.name.empty() ) - return Variant( lang.linter.found ? SUCCESS : ERROR ); + return Variant( lang.linter.found ? HEALTH_SUCCESS : HEALTH_ERROR ); else - return Variant( NONE ); + return Variant( HEALTH_NONE ); break; case 4: if ( !lang.formatter.name.empty() ) - return Variant( lang.formatter.found ? SUCCESS : ERROR ); + return Variant( lang.formatter.found ? HEALTH_SUCCESS : HEALTH_ERROR ); else - return Variant( NONE ); + return Variant( HEALTH_NONE ); break; default: { } diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 22565777f..afd844fa6 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -24,6 +24,17 @@ static int countLines( const std::string& text ) { return count; } +static void readAllLines( const std::string_view& buf, + std::function onLineRead ) { + auto lastNL = 0; + auto nextNL = buf.find_first_of( '\n' ); + while ( nextNL != std::string_view::npos ) { + onLineRead( buf.substr( lastNL, nextNL - lastNL ) ); + lastNL = nextNL + 1; + nextNL = buf.find_first_of( '\n', nextNL + 1 ); + } +} + static constexpr auto sNotCommitedYetHash = "0000000000000000000000000000000000000000"; Git::Blame::Blame( const std::string& error ) : error( error ), line( 0 ) {} @@ -58,6 +69,7 @@ int Git::git( const std::string& args, const std::string& projectDir, std::strin const_cast( this )->mLastProjectPath = projectDir; const_cast( this )->mSubModulesUpdated = false; } + Log::debug( "GitPlugin run: %s %s", mGitPath, args ); return retCode; } @@ -159,16 +171,18 @@ std::vector Git::getAllBranches( const std::string& projectDir ) { projectDir ); } -static Git::Branch parseLocalBranch( const std::string& raw ) { +static Git::Branch parseLocalBranch( const std::string_view& raw ) { static constexpr size_t len = std::string_view{ "refs/heads/"sv }.size(); - return Git::Branch{ raw.substr( len ), std::string{}, Git::RefType::Head, std::string{} }; + return Git::Branch{ std::string{ raw.substr( len ) }, std::string{}, Git::RefType::Head, + std::string{} }; } -static Git::Branch parseRemoteBranch( const std::string& raw ) { +static Git::Branch parseRemoteBranch( const std::string_view& raw ) { static constexpr size_t len = std::string_view( "refs/remotes/"sv ).size(); size_t indexOfRemote = raw.find_first_of( '/', len ); if ( indexOfRemote != std::string::npos ) - return Git::Branch{ raw.substr( len ), raw.substr( len, indexOfRemote - len ), + return Git::Branch{ std::string{ raw.substr( len ) }, + std::string{ raw.substr( len, indexOfRemote - len ) }, Git::RefType::Remote, std::string{} }; return {}; } @@ -192,34 +206,23 @@ std::vector Git::getAllBranchesAndTags( RefType ref, const std::str if ( EXIT_SUCCESS != git( args, projectDir, buf ) ) return branches; - auto out = String::split( buf ); - branches.reserve( out.size() ); - for ( const auto& branch : out ) { + readAllLines( buf, [&branches, ref]( const std::string_view& line ) { + auto branch = String::trim( line, '\n' ); + branch = String::trim( line, '\'' ); if ( ( ref & Head ) && String::startsWith( branch, "refs/heads/" ) ) { branches.emplace_back( parseLocalBranch( branch ) ); } else if ( ( ref & Remote ) && String::startsWith( branch, "refs/remotes/" ) ) { branches.emplace_back( parseRemoteBranch( branch ) ); } else if ( ( ref & Tag ) && String::startsWith( branch, "refs/tags/" ) ) { static constexpr size_t len = std::string_view{ "refs/tags/"sv }.size(); - branches.push_back( - { branch.substr( len ), std::string{}, RefType::Tag, std::string{} } ); + branches.push_back( { std::string{ branch.substr( len ) }, std::string{}, RefType::Tag, + std::string{} } ); } - } + } ); return branches; } -static void readAllLines( const std::string_view& buf, - std::function onLineRead ) { - auto lastNL = 0; - auto nextNL = buf.find_first_of( '\n' ); - while ( nextNL != std::string_view::npos ) { - onLineRead( buf.substr( lastNL, nextNL - lastNL ) ); - lastNL = nextNL; - nextNL = buf.find_first_of( '\n', nextNL + 1 ); - } -} - std::vector Git::fetchSubModules( const std::string& projectDir ) { std::vector submodules; std::string buf; @@ -228,8 +231,8 @@ std::vector Git::fetchSubModules( const std::string& projectDir ) { readAllLines( buf, [&pattern, &submodules]( const std::string_view& line ) { LuaPattern::Range matches[2]; if ( pattern.matches( line.data(), 0, matches, line.size() ) ) { - submodules.emplace_back( - line.substr( matches[1].start, matches[1].end - matches[1].start ) ); + submodules.emplace_back( String::trim( + line.substr( matches[1].start, matches[1].end - matches[1].start ), '\n' ) ); } } ); return submodules; diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index 6e89329d7..a55fd1e34 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1,11 +1,12 @@ #include "gitplugin.hpp" -#include "eepp/ui/uistyle.hpp" #include #include #include #include #include +#include #include +#include #include using namespace EE::UI::Doc; @@ -19,6 +20,208 @@ using json = nlohmann::json; namespace ecode { +static const char* GIT_EMPTY = ""; +static const char* GIT_SUCCESS = "theme-success"; +static const char* GIT_ERROR = "theme-error"; +static const char* GIT_BOLD = "bold"; +static const char* GIT_NOT_BOLD = "notbold"; + +class GitBranchModel : public Model { + public: + static std::shared_ptr asModel( std::vector&& branches, + GitPlugin* gitPlugin ) { + return std::make_shared( std::move( branches ), gitPlugin ); + } + + enum Column { Name, Remote, Type, LastCommit }; + + struct BranchData { + std::string branch; + std::vector data; + }; + + std::string refTypeToString( Git::RefType type ) { + switch ( type ) { + case Git::RefType::Head: + return mPlugin->i18n( "git_local_branches", "Local Branches" ).toUtf8(); + case Git::RefType::Remote: + return mPlugin->i18n( "git_remote_branches", "Remote Branches" ).toUtf8(); + case Git::RefType::Tag: + return mPlugin->i18n( "git_tags", "Tags" ).toUtf8(); + default: + break; + } + return ""; + } + + GitBranchModel( std::vector&& branches, GitPlugin* gitPlugin ) : + mPlugin( gitPlugin ) { + std::map> branchTypes; + for ( auto& branch : branches ) { + auto& type = branchTypes[refTypeToString( branch.type )]; + type.emplace_back( std::move( branch ) ); + } + for ( auto& branch : branchTypes ) { + mBranches.emplace_back( + BranchData{ std::move( branch.first ), std::move( branch.second ) } ); + } + } + + size_t treeColumn() const { return Column::Name; } + + size_t rowCount( const ModelIndex& index ) const { + if ( !index.isValid() ) + return mBranches.size(); + if ( index.internalId() == -1 ) + return mBranches[index.row()].data.size(); + return 0; + } + + size_t columnCount( const ModelIndex& ) const { return 4; } + + ModelIndex parentIndex( const ModelIndex& index ) const { + if ( !index.isValid() || index.internalId() == -1 ) + return {}; + return createIndex( index.internalId(), index.column(), &mBranches[index.internalId()], + -1 ); + } + + ModelIndex index( int row, int column, const ModelIndex& parent ) const { + if ( row < 0 || column < 0 ) + return {}; + if ( !parent.isValid() ) + return createIndex( row, column, &mBranches[row], -1 ); + if ( parent.internalData() ) + return createIndex( row, column, &mBranches[parent.row()].data[row], parent.row() ); + return {}; + } + + Variant data( const ModelIndex& index, ModelRole role ) const { + switch ( role ) { + case ModelRole::Display: { + if ( index.internalId() == -1 ) { + if ( index.column() == Column::Name ) + return Variant( mBranches[index.row()].branch.c_str() ); + return Variant( GIT_EMPTY ); + } + const Git::Branch& branch = mBranches[index.internalId()].data[index.row()]; + switch ( index.column() ) { + case Column::Name: + return Variant( branch.name.c_str() ); + case Column::Remote: + return Variant( branch.remote.c_str() ); + case Column::Type: + return Variant( branch.typeStr() ); + case Column::LastCommit: + return Variant( branch.lastCommit.c_str() ); + } + return Variant( GIT_EMPTY ); + } + case ModelRole::Class: { + if ( index.internalId() == -1 ) + return Variant( GIT_BOLD ); + const Git::Branch& branch = mBranches[index.internalId()].data[index.row()]; + if ( branch.name == mPlugin->gitBranch() ) + return Variant( GIT_BOLD ); + return Variant( GIT_NOT_BOLD ); + } + default: + break; + } + return {}; + } + + virtual bool classModelRoleEnabled() { return true; } + + protected: + std::vector mBranches; + GitPlugin* mPlugin; +}; + +class GitStatusModel : public Model { + public: + static std::shared_ptr asModel( Git::FilesStatus&& status ) { + return std::make_shared( std::move( status ) ); + } + + struct RepoStatus { + std::string repo; + std::vector files; + }; + + enum Column { File, Inserted, Removed, FileStatus }; + + GitStatusModel( Git::FilesStatus&& status ) { + mStatus.reserve( status.size() ); + for ( auto& s : status ) + mStatus.emplace_back( RepoStatus{ std::move( s.first ), std::move( s.second ) } ); + } + + size_t treeColumn() const { return Column::File; } + + size_t rowCount( const ModelIndex& index ) const { + if ( !index.isValid() ) + return mStatus.size(); + if ( index.internalId() == -1 ) + return mStatus[index.row()].files.size(); + return 0; + } + + size_t columnCount( const ModelIndex& ) const { return 4; } + + ModelIndex parentIndex( const ModelIndex& index ) const { + if ( !index.isValid() || index.internalId() == -1 ) + return {}; + return createIndex( index.internalId(), index.column(), &mStatus[index.internalId()], -1 ); + } + + ModelIndex index( int row, int column, const ModelIndex& parent ) const { + if ( row < 0 || column < 0 ) + return {}; + if ( !parent.isValid() ) + return createIndex( row, column, &mStatus[row], -1 ); + if ( parent.internalData() ) + return createIndex( row, column, &mStatus[parent.row()].files[row], parent.row() ); + return {}; + } + + Variant data( const ModelIndex& index, ModelRole role ) const { + if ( role == ModelRole::Display ) { + if ( index.internalId() == -1 ) { + if ( index.column() == Column::File ) + return Variant( mStatus[index.row()].repo.c_str() ); + return Variant( GIT_EMPTY ); + } + const Git::DiffFile& s = mStatus[index.internalId()].files[index.row()]; + switch ( index.column() ) { + case Column::File: + return Variant( s.file.c_str() ); + case Column::Inserted: + return Variant( s.inserts ); + case Column::Removed: + return Variant( s.deletes ); + case Column::FileStatus: + return Variant( std::string( static_cast( s.status ), 1 ) ); + } + } else if ( role == ModelRole::Class ) { + switch ( index.column() ) { + case Column::Inserted: + return Variant( GIT_SUCCESS ); + case Column::Removed: + return Variant( GIT_ERROR ); + default: + break; + } + } + return Variant( GIT_EMPTY ); + } + + virtual bool classModelRoleEnabled() { return true; } + + protected: + std::vector mStatus; +}; + Plugin* GitPlugin::New( PluginManager* pluginManager ) { return eeNew( GitPlugin, ( pluginManager, false ) ); } @@ -43,6 +246,8 @@ GitPlugin::~GitPlugin() { mShuttingDown = true; if ( mStatusButton ) mStatusButton->close(); + if ( mSidePanel && mTab ) + mSidePanel->removeTab( mTab ); } void GitPlugin::load( PluginManager* pluginManager ) { @@ -137,8 +342,10 @@ void GitPlugin::load( PluginManager* pluginManager ) { mGit = std::make_unique( pluginManager->getWorkspaceFolder() ); mGitFound = !mGit->getGitPath().empty(); - if ( getUISceneNode() ) + if ( getUISceneNode() ) { updateStatusBar(); + // updateBranches(); + } subscribeFileSystemListener(); mReady = true; @@ -151,6 +358,7 @@ void GitPlugin::updateUINow( bool force ) { return; updateStatusBar( force ); + // updateBranches(); } void GitPlugin::updateUI() { @@ -275,7 +483,7 @@ void GitPlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) { return; if ( String::startsWith( file.getFilepath(), mGit->getGitFolder() ) && - file.getExtension() == "lock" ) + ( file.getExtension() == "lock" || file.isDirectory() ) ) return; updateUI(); @@ -368,6 +576,10 @@ bool GitPlugin::onMouseLeave( UICodeEditor* editor, const Vector2i&, const Uint3 return false; } +std::string GitPlugin::gitBranch() const { + return mGitBranch; +} + void GitPlugin::onRegisterListeners( UICodeEditor* editor, std::vector& listeners ) { listeners.push_back( editor->addEventListener( Event::OnCursorPosChange, [this, editor]( const Event* ) { @@ -472,4 +684,56 @@ bool GitPlugin::onKeyDown( UICodeEditor* editor, const KeyEvent& event ) { return false; } +void GitPlugin::updateBranches() { + if ( !mGit || !mGitFound ) + return; + + mThreadPool->run( [this] { + if ( !mGit ) + return; + + if ( !mGit->getGitFolder().empty() ) { + if ( mGitBranch.empty() ) + mGitBranch = mGit->branch(); + auto branches = mGit->getAllBranchesAndTags(); + auto model = GitBranchModel::asModel( std::move( branches ), this ); + getUISceneNode()->runOnMainThread( [this, model] { updateSidePanelTab( model ); } ); + } + } ); +} + +void GitPlugin::updateSidePanelTab( std::shared_ptr model ) { + buildSidePanelTab(); + + UITreeView* tree = mSidePanel->find( "git_branches_tree" ); + tree->setModel( model ); + tree->setColumnsVisible( { GitBranchModel::Name } ); +} + +void GitPlugin::buildSidePanelTab() { + if ( mTab ) + return; + if ( mSidePanel == nullptr ) + getUISceneNode()->bind( "panel", mSidePanel ); + UIIcon* icon = getUISceneNode()->findIcon( "source-control" ); + UIWidget* node = getUISceneNode()->loadLayoutFromString( + R"html( + + + + + )html" ); + mTab = mSidePanel->add( getUISceneNode()->i18n( "source_control", "Source Control" ), node, + icon ? icon->getSize( PixelDensity::dpToPx( 12 ) ) : nullptr ); + mTab->setId( "source_control" ); + mTab->setTextAsFallback( true ); + + UITreeView* tree = mSidePanel->find( "git_branches_tree" ); + tree->setAutoExpandOnSingleColumn( true ); + tree->setHeadersVisible( false ); +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 626bd9e91..22e9f597f 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -14,6 +14,7 @@ using namespace EE::UI; namespace ecode { class Git; +class GitBranchModel; class GitPlugin : public PluginBase { public: @@ -46,6 +47,8 @@ class GitPlugin : public PluginBase { bool onMouseLeave( UICodeEditor*, const Vector2i&, const Uint32& ) override; + std::string gitBranch() const; + protected: std::unique_ptr mGit; std::string mGitBranch; @@ -80,6 +83,8 @@ class GitPlugin : public PluginBase { Uint32 mOldTextStyle{ 0 }; Uint32 mOldTextAlign{ 0 }; Color mOldBackgroundColor; + UITabWidget* mSidePanel{ nullptr }; + UITab* mTab{ nullptr }; struct CustomTokenizer { SyntaxDefinition def; @@ -99,170 +104,12 @@ class GitPlugin : public PluginBase { void updateUI(); void updateUINow( bool force = false ); -}; -class GitBranchModel : public Model { - public: - static std::shared_ptr asModel( std::vector&& branches ) { - return std::make_shared( std::move( branches ) ); - } + void updateBranches(); - enum Column { Name, Remote, Type, LastCommit }; + void buildSidePanelTab(); - struct BranchData { - std::string branch; - std::vector data; - }; - - GitBranchModel( std::vector&& branches ) { - std::map> branchTypes; - for ( auto& branch : branches ) { - auto& type = branchTypes[Git::refTypeToString( branch.type )]; - type.emplace_back( std::move( branch ) ); - } - for ( auto& branch : branchTypes ) { - mBranches.emplace_back( - BranchData{ std::move( branch.first ), std::move( branch.second ) } ); - } - } - - size_t treeColumn() const { return Column::Name; } - - size_t rowCount( const ModelIndex& index ) const { - if ( !index.isValid() ) - return mBranches.size(); - return mBranches[index.row()].data.size(); - } - - size_t columnCount( const ModelIndex& ) const { return 4; } - - ModelIndex parentIndex( const ModelIndex& index ) const { - if ( !index.isValid() || index.internalId() == -1 ) - return {}; - return createIndex( index.internalId(), index.column(), &mBranches[index.internalId()], - -1 ); - } - - ModelIndex index( int row, int column, const ModelIndex& parent ) const { - if ( row < 0 || column < 0 ) - return {}; - if ( !parent.isValid() ) - return createIndex( row, column, &mBranches[row], -1 ); - if ( parent.internalData() ) - return createIndex( row, column, &mBranches[parent.row()].data[row], parent.row() ); - return {}; - } - - Variant data( const ModelIndex& index, ModelRole role ) const { - static const char* EMPTY = ""; - if ( role == ModelRole::Display ) { - if ( index.internalId() == -1 ) { - if ( index.column() == Column::Name ) - return mBranches[index.row()].branch.c_str(); - return EMPTY; - } - const Git::Branch& branch = mBranches[index.internalId()].data[index.row()]; - switch ( index.column() ) { - case Column::Name: - return branch.name.c_str(); - case Column::Remote: - return branch.remote.c_str(); - case Column::Type: - return branch.typeStr(); - case Column::LastCommit: - return branch.lastCommit.c_str(); - } - } - return EMPTY; - } - - protected: - std::vector mBranches; -}; - -class GitStatusModel : public Model { - public: - static std::shared_ptr asModel( Git::FilesStatus&& status ) { - return std::make_shared( std::move( status ) ); - } - - struct RepoStatus { - std::string repo; - std::vector files; - }; - - enum Column { File, Inserted, Removed, FileStatus }; - - GitStatusModel( Git::FilesStatus&& status ) { - mStatus.reserve( status.size() ); - for ( auto& s : status ) - mStatus.emplace_back( RepoStatus{ std::move( s.first ), std::move( s.second ) } ); - } - - size_t treeColumn() const { return Column::File; } - - size_t rowCount( const ModelIndex& index ) const { - if ( !index.isValid() ) - return mStatus.size(); - return mStatus[index.row()].files.size(); - } - - size_t columnCount( const ModelIndex& ) const { return 4; } - - ModelIndex parentIndex( const ModelIndex& index ) const { - if ( !index.isValid() || index.internalId() == -1 ) - return {}; - return createIndex( index.internalId(), index.column(), &mStatus[index.internalId()], -1 ); - } - - ModelIndex index( int row, int column, const ModelIndex& parent ) const { - if ( row < 0 || column < 0 ) - return {}; - if ( !parent.isValid() ) - return createIndex( row, column, &mStatus[row], -1 ); - if ( parent.internalData() ) - return createIndex( row, column, &mStatus[parent.row()].files[row], parent.row() ); - return {}; - } - - Variant data( const ModelIndex& index, ModelRole role ) const { - static const char* EMPTY = ""; - static const char* SUCCESS = "theme-success"; - static const char* ERROR = "theme-error"; - if ( role == ModelRole::Display ) { - if ( index.internalId() == -1 ) { - if ( index.column() == Column::File ) - return mStatus[index.row()].repo.c_str(); - return EMPTY; - } - const Git::DiffFile& s = mStatus[index.internalId()].files[index.row()]; - switch ( index.column() ) { - case Column::File: - return s.file.c_str(); - case Column::Inserted: - return s.inserts; - case Column::Removed: - return s.deletes; - case Column::FileStatus: - return Variant( std::string( static_cast( s.status ), 1 ) ); - } - } else if ( role == ModelRole::Class ) { - switch ( index.column() ) { - case Column::Inserted: - return SUCCESS; - case Column::Removed: - return ERROR; - default: - break; - } - } - return EMPTY; - } - - virtual bool classModelRoleEnabled() { return true; } - - protected: - std::vector mStatus; + void updateSidePanelTab( std::shared_ptr ); }; } // namespace ecode diff --git a/src/tools/ecode/projectbuild.cpp b/src/tools/ecode/projectbuild.cpp index 78f07acd5..d34aeac64 100644 --- a/src/tools/ecode/projectbuild.cpp +++ b/src/tools/ecode/projectbuild.cpp @@ -847,7 +847,7 @@ void ProjectBuildManager::buildSidePanelTab() { )html" ); - mTab = mSidePanel->add( mUISceneNode->getTranslatorStringFromKey( "build", "Build" ), node, + mTab = mSidePanel->add( mUISceneNode->i18n( "build", "Build" ), node, icon ? icon->getSize( PixelDensity::dpToPx( 12 ) ) : nullptr ); mTab->setId( "build_tab" ); mTab->setTextAsFallback( true );