mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Add a work-in-progress Git History view to the source-control panel.
Display bounded first-parent commit history with asynchronous loading, independent pagination, and lazily expandable merge commits. Propagate inherited exclusions through nested merges to avoid repeating already represented history. Add custom compact history rows, loading/error/retry states, keyboard navigation, repository and history-generation guards, and lifecycle invalidation after history-changing operations. Add a commit details tab with full commit metadata and message, changed-line totals, GitHub links when available, and a multi-file diff viewer with global collapse and unified/split controls. Extend UIDiffView with multi-file view controls and register the required Codicons. The Git History interface and visual design remain a work in progress. Fix caret positioning inside shaped ligature clusters. Add explicit closest-glyph and interpolated ligature caret-positioning modes while keeping interpolation as the default for general text geometry. Use the original closest-glyph behavior for caret and IME positioning in UICodeEditor, UITextEdit, UITextInput, and UIConsole, preventing cursor positions from being synthesized inside ligature glyphs. Add regression coverage for both positioning modes. Optimize SyntaxTokenizer by returning tokenizer results in a SmallVector, avoiding heap allocations in common cases.
This commit is contained in:
294
src/tools/ecode/plugins/git/githistorymodel.cpp
Normal file
294
src/tools/ecode/plugins/git/githistorymodel.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
#include "githistorymodel.hpp"
|
||||
#include "gitplugin.hpp"
|
||||
#include <algorithm>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <iterator>
|
||||
|
||||
namespace ecode {
|
||||
|
||||
size_t GitHistoryModel::rowCount( const ModelIndex& index ) const {
|
||||
if ( !index.isValid() )
|
||||
return mRoots.size();
|
||||
const Node* item = node( index );
|
||||
return item ? item->children.size() : 0;
|
||||
}
|
||||
|
||||
std::string GitHistoryModel::columnName( const size_t& column ) const {
|
||||
switch ( column ) {
|
||||
case Subject:
|
||||
return mPlugin->i18n( "git_history_subject", "Subject" );
|
||||
case Author:
|
||||
return mPlugin->i18n( "git_history_author", "Author" );
|
||||
case Date:
|
||||
return mPlugin->i18n( "git_history_date", "Date" );
|
||||
case Hash:
|
||||
return mPlugin->i18n( "git_history_hash", "Hash" );
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GitHistoryModel::Node* GitHistoryModel::node( const ModelIndex& index ) const {
|
||||
return index.isValid() ? static_cast<Node*>( index.internalData() ) : nullptr;
|
||||
}
|
||||
|
||||
const GitHistoryModel::Nodes& GitHistoryModel::siblings( const Node* parent ) const {
|
||||
return parent ? parent->children : mRoots;
|
||||
}
|
||||
|
||||
GitHistoryModel::Nodes& GitHistoryModel::siblings( Node* parent ) {
|
||||
return parent ? parent->children : mRoots;
|
||||
}
|
||||
|
||||
ModelIndex GitHistoryModel::indexForNode( const Node* item, int column ) const {
|
||||
if ( !item )
|
||||
return {};
|
||||
const auto& items = siblings( item->parent );
|
||||
for ( size_t row = 0; row < items.size(); ++row )
|
||||
if ( items[row].get() == item )
|
||||
return createIndex( row, column, item );
|
||||
return {};
|
||||
}
|
||||
|
||||
ModelIndex GitHistoryModel::parentIndex( const ModelIndex& index ) const {
|
||||
Node* item = node( index );
|
||||
return item ? indexForNode( item->parent ) : ModelIndex{};
|
||||
}
|
||||
|
||||
ModelIndex GitHistoryModel::index( int row, int column, const ModelIndex& parent ) const {
|
||||
if ( row < 0 || column < 0 )
|
||||
return {};
|
||||
const auto& items = siblings( node( parent ) );
|
||||
if ( static_cast<size_t>( row ) >= items.size() )
|
||||
return {};
|
||||
return createIndex( row, column, items[row].get() );
|
||||
}
|
||||
|
||||
Variant GitHistoryModel::data( const ModelIndex& index, ModelRole role ) const {
|
||||
const Node* item = node( index );
|
||||
if ( !item )
|
||||
return {};
|
||||
if ( role == ModelRole::Class ) {
|
||||
if ( item->type == NodeType::LoadMore || item->type == NodeType::Error )
|
||||
return Variant( "git_history_action" );
|
||||
if ( item->type != NodeType::Commit )
|
||||
return Variant( "git_history_secondary" );
|
||||
if ( item->commit.isMerge() )
|
||||
return Variant( "git_history_merge" );
|
||||
return Variant( "" );
|
||||
}
|
||||
if ( role == ModelRole::Tooltip ) {
|
||||
if ( item->type == NodeType::Error )
|
||||
return Variant( &item->error );
|
||||
if ( item->type != NodeType::Commit )
|
||||
return Variant( &item->message );
|
||||
return Variant( &item->tooltip );
|
||||
}
|
||||
if ( role != ModelRole::Display )
|
||||
return {};
|
||||
if ( item->type != NodeType::Commit )
|
||||
return index.column() == Subject ? Variant( &item->message ) : Variant( "" );
|
||||
switch ( index.column() ) {
|
||||
case Subject:
|
||||
return Variant( &item->subject );
|
||||
case Author:
|
||||
return Variant( &item->author );
|
||||
case Date:
|
||||
return Variant( &item->date );
|
||||
case Hash:
|
||||
return Variant( &item->hash );
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<GitHistoryModel::Node>
|
||||
GitHistoryModel::commitNode( Git::Commit commit, Node* parent,
|
||||
const Git::HistoryQuery& query ) const {
|
||||
auto item = std::make_unique<Node>();
|
||||
item->parent = parent;
|
||||
item->commit = std::move( commit );
|
||||
item->subject = String::fromUtf8( item->commit.subject );
|
||||
item->author = String::fromUtf8( item->commit.authorName );
|
||||
item->date = Sys::epochToString( item->commit.commitTime );
|
||||
item->hash = String::fromUtf8( item->commit.shortHash );
|
||||
item->tooltip = String::format( "%s\n%s <%s>\n%s", item->commit.hash, item->commit.authorName,
|
||||
item->commit.authorEmail, item->commit.subject );
|
||||
item->message = item->date + String{ " · " } + item->author + String{ " · " } + item->hash;
|
||||
if ( item->commit.parents.size() == 2 ) {
|
||||
item->query = query;
|
||||
auto loading = std::make_unique<Node>();
|
||||
loading->type = NodeType::Loading;
|
||||
loading->parent = item.get();
|
||||
loading->message = mPlugin->i18n( "git_history_loading", "Loading..." );
|
||||
item->children.emplace_back( std::move( loading ) );
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
void GitHistoryModel::fillPage( Nodes& nodes, Node* parent, Git::HistoryPage page,
|
||||
const Git::HistoryQuery& query ) {
|
||||
for ( auto& commit : page.commits )
|
||||
nodes.emplace_back( commitNode( std::move( commit ), parent, query ) );
|
||||
if ( page.hasMore && !nodes.empty() && !nodes.back()->commit.parents.empty() ) {
|
||||
auto more = std::make_unique<Node>();
|
||||
more->type = NodeType::LoadMore;
|
||||
more->parent = parent;
|
||||
more->message = mPlugin->i18n( "git_history_load_older", "Load older commits" );
|
||||
more->query = query;
|
||||
more->query.continuation = nodes.back()->commit.parents[0];
|
||||
nodes.emplace_back( std::move( more ) );
|
||||
}
|
||||
}
|
||||
|
||||
void GitHistoryModel::replaceChildren( Node* parent, Nodes children ) {
|
||||
auto& current = parent->children;
|
||||
const ModelIndex parentIndex = indexForNode( parent );
|
||||
if ( !current.empty() && beginDeleteRows( parentIndex, 0, current.size() - 1 ) ) {
|
||||
current.clear();
|
||||
endDeleteRows();
|
||||
}
|
||||
if ( !children.empty() ) {
|
||||
beginInsertRows( parentIndex, 0, children.size() - 1 );
|
||||
current = std::move( children );
|
||||
endInsertRows();
|
||||
}
|
||||
// Views are not Model::Client instances, so structural notifications alone do not rebuild a
|
||||
// UITreeView's flattened rows and scrollable content height.
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
}
|
||||
|
||||
void GitHistoryModel::setRootLoading() {
|
||||
mRoots.clear();
|
||||
auto item = std::make_unique<Node>();
|
||||
item->type = NodeType::Loading;
|
||||
item->message = mPlugin->i18n( "git_history_loading", "Loading..." );
|
||||
mRoots.emplace_back( std::move( item ) );
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void GitHistoryModel::setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query ) {
|
||||
mRoots.clear();
|
||||
fillPage( mRoots, nullptr, std::move( page ), query );
|
||||
if ( mRoots.empty() ) {
|
||||
auto item = std::make_unique<Node>();
|
||||
item->type = NodeType::Empty;
|
||||
item->message = mPlugin->i18n( "git_history_empty", "No commits yet" );
|
||||
mRoots.emplace_back( std::move( item ) );
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void GitHistoryModel::setRootError( std::string error ) {
|
||||
mRoots.clear();
|
||||
auto item = std::make_unique<Node>();
|
||||
item->type = NodeType::Error;
|
||||
item->message = mPlugin->i18n( "git_history_error", "Could not load Git history" ) +
|
||||
String{ " · " } + mPlugin->i18n( "git_history_retry", "Retry" );
|
||||
item->error = error.empty() ? item->message : String::fromUtf8( error );
|
||||
mRoots.emplace_back( std::move( item ) );
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void GitHistoryModel::setChildrenLoading( Node* item ) {
|
||||
if ( !item )
|
||||
return;
|
||||
item->childrenLoading = true;
|
||||
if ( item->children.size() == 1 && ( item->children.front()->type == NodeType::Error ||
|
||||
item->children.front()->type == NodeType::Loading ) ) {
|
||||
item->children.front()->type = NodeType::Loading;
|
||||
item->children.front()->message = mPlugin->i18n( "git_history_loading", "Loading..." );
|
||||
item->children.front()->error.clear();
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
}
|
||||
}
|
||||
|
||||
void GitHistoryModel::setChildrenPage( Node* item, Git::HistoryPage page,
|
||||
const Git::HistoryQuery& query ) {
|
||||
if ( !item )
|
||||
return;
|
||||
Nodes children;
|
||||
fillPage( children, item, std::move( page ), query );
|
||||
item->childrenLoading = false;
|
||||
item->childrenLoaded = true;
|
||||
replaceChildren( item, std::move( children ) );
|
||||
}
|
||||
|
||||
void GitHistoryModel::setChildrenError( Node* item, std::string error,
|
||||
const Git::HistoryQuery& query ) {
|
||||
if ( !item )
|
||||
return;
|
||||
Nodes children;
|
||||
auto child = std::make_unique<Node>();
|
||||
child->type = NodeType::Error;
|
||||
child->parent = item;
|
||||
child->query = query;
|
||||
child->message = mPlugin->i18n( "git_history_error", "Could not load Git history" ) +
|
||||
String{ " · " } + mPlugin->i18n( "git_history_retry", "Retry" );
|
||||
child->error = error.empty() ? child->message : String::fromUtf8( error );
|
||||
children.emplace_back( std::move( child ) );
|
||||
item->childrenLoading = false;
|
||||
replaceChildren( item, std::move( children ) );
|
||||
}
|
||||
|
||||
void GitHistoryModel::setPageLoading( Node* item ) {
|
||||
if ( !item || item->childrenLoading )
|
||||
return;
|
||||
item->childrenLoading = true;
|
||||
item->type = NodeType::Loading;
|
||||
item->message = mPlugin->i18n( "git_history_loading", "Loading..." );
|
||||
item->error.clear();
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
}
|
||||
|
||||
void GitHistoryModel::setPageError( Node* item, std::string error ) {
|
||||
if ( !item )
|
||||
return;
|
||||
item->type = NodeType::Error;
|
||||
item->childrenLoading = false;
|
||||
item->retryAppend = true;
|
||||
item->message = mPlugin->i18n( "git_history_error", "Could not load Git history" ) +
|
||||
String{ " · " } + mPlugin->i18n( "git_history_retry", "Retry" );
|
||||
item->error = error.empty() ? item->message : String::fromUtf8( error );
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
}
|
||||
|
||||
void GitHistoryModel::appendPage( Node* loadMore, Git::HistoryPage page ) {
|
||||
if ( !loadMore || ( loadMore->type != NodeType::LoadMore && !loadMore->retryAppend ) )
|
||||
return;
|
||||
Node* parent = loadMore->parent;
|
||||
auto& items = siblings( parent );
|
||||
auto pos = std::find_if( items.begin(), items.end(),
|
||||
[loadMore]( const auto& item ) { return item.get() == loadMore; } );
|
||||
if ( pos == items.end() )
|
||||
return;
|
||||
Git::HistoryQuery query = loadMore->query;
|
||||
Nodes additions;
|
||||
fillPage( additions, parent, std::move( page ), query );
|
||||
const size_t row = std::distance( items.begin(), pos );
|
||||
const ModelIndex parentIndex = indexForNode( parent );
|
||||
if ( !beginDeleteRows( parentIndex, row, row ) )
|
||||
return;
|
||||
items.erase( pos );
|
||||
endDeleteRows();
|
||||
if ( additions.empty() ) {
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
return;
|
||||
}
|
||||
beginInsertRows( parentIndex, row, row + additions.size() - 1 );
|
||||
items.insert( items.begin() + row, std::make_move_iterator( additions.begin() ),
|
||||
std::make_move_iterator( additions.end() ) );
|
||||
endInsertRows();
|
||||
invalidate( Model::DontInvalidateIndexes );
|
||||
}
|
||||
|
||||
Git::HistoryQuery GitHistoryModel::mergeQuery( const Node* item, size_t limit ) const {
|
||||
Git::HistoryQuery query;
|
||||
query.limit = limit;
|
||||
if ( !item || item->commit.parents.size() < 2 )
|
||||
return query;
|
||||
query.revision = item->commit.parents[1];
|
||||
query.exclusions = item->query.exclusions;
|
||||
query.exclusions.emplace_back( item->commit.parents[0] );
|
||||
return query;
|
||||
}
|
||||
|
||||
} // namespace ecode
|
||||
Reference in New Issue
Block a user