diff --git a/GitUI/Avatars/AvatarService.cs b/GitUI/Avatars/AvatarService.cs
index 31c02fcd834..895208c22a6 100644
--- a/GitUI/Avatars/AvatarService.cs
+++ b/GitUI/Avatars/AvatarService.cs
@@ -4,7 +4,7 @@ namespace GitUI.Avatars
{
public static class AvatarService
{
- private static InitialsAvatarGenerator AvatarGenerator = new InitialsAvatarGenerator();
+ private static readonly InitialsAvatarGenerator AvatarGenerator = new InitialsAvatarGenerator();
public static IAvatarProvider Default { get; }
= new BackupAvatarProvider(new AvatarMemoryCache(new AvatarPersistentCache(new AvatarDownloader(AvatarGenerator), AvatarGenerator)),
Images.User80);
diff --git a/GitUI/Editor/BlameAuthorMargin.cs b/GitUI/Editor/BlameAuthorMargin.cs
new file mode 100644
index 00000000000..b77c269e778
--- /dev/null
+++ b/GitUI/Editor/BlameAuthorMargin.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using GitExtUtils.GitUI;
+using ICSharpCode.TextEditor;
+using Microsoft.VisualStudio.Threading;
+
+namespace GitUI.Editor
+{
+ ///
+ /// This class display avatars in the gutter in a blame.
+ ///
+ public class BlameAuthorMargin : AbstractMargin
+ {
+ private List _avatars;
+ private readonly int _lineHeight;
+ private readonly Color _backgroundColor;
+
+ public BlameAuthorMargin(TextArea textArea) : base(textArea)
+ {
+ _lineHeight = textArea.Font.Height + 1;
+ _backgroundColor = ((SolidBrush)SystemBrushes.Window).Color;
+ }
+
+ public void SetAvatars(List> avatars)
+ {
+ _avatars = avatars.Select(a => ThreadHelper.JoinableTaskFactory.Run(async () => await a)).ToList();
+
+ // Update the resolution otherwise the image is not drawn at the good size :(
+ foreach (var avatar in _avatars)
+ {
+ if (avatar is Bitmap bitmapAvatar)
+ {
+ bitmapAvatar.SetResolution(DpiUtil.DpiX, DpiUtil.DpiY);
+ }
+ }
+ }
+
+ public override int Width => _lineHeight;
+
+ public override bool IsVisible => true;
+
+ public override void Paint(Graphics g, Rectangle rect)
+ {
+ if (rect.Width <= 0 || rect.Height <= 0)
+ {
+ return;
+ }
+
+ g.Clear(_backgroundColor);
+
+ if (_avatars == null || _avatars.Count == 0)
+ {
+ return;
+ }
+
+ var verticalOffset = textArea.VirtualTop.Y;
+ var lineStart = verticalOffset / _lineHeight;
+ var negativeOffset = (lineStart * _lineHeight) - verticalOffset;
+ var lineCount = (int)rect.Height / _lineHeight;
+
+ for (int i = 0; i < lineCount; i++)
+ {
+ if (lineStart + i >= _avatars.Count)
+ {
+ break;
+ }
+
+ if (_avatars[lineStart + i] != null)
+ {
+ g.DrawImage(_avatars[lineStart + i], new Point(0, negativeOffset + (i * _lineHeight)));
+ }
+ }
+
+ base.Paint(g, rect);
+ }
+ }
+}
diff --git a/GitUI/Editor/FileViewer.cs b/GitUI/Editor/FileViewer.cs
index 68a80187250..f07cf1ad7b9 100644
--- a/GitUI/Editor/FileViewer.cs
+++ b/GitUI/Editor/FileViewer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
@@ -17,7 +18,9 @@
using GitUI.Hotkey;
using GitUI.Properties;
using GitUIPluginInterfaces;
+using ICSharpCode.TextEditor;
using JetBrains.Annotations;
+using Microsoft.VisualStudio.Threading;
using ResourceManager;
namespace GitUI.Editor
@@ -1517,5 +1520,10 @@ public IgnoreWhitespaceKind IgnoreWhitespace
public ToolStripButton IgnoreAllWhitespacesButton => _fileViewer.ignoreAllWhitespaces;
public ToolStripMenuItem IgnoreAllWhitespacesMenuItem => _fileViewer.ignoreAllWhitespaceChangesToolStripMenuItem;
}
+
+ public void SetGutterAvatars(List> avatars)
+ {
+ internalFileViewer.SetGutterAvatars(avatars);
+ }
}
}
diff --git a/GitUI/Editor/FileViewerInternal.cs b/GitUI/Editor/FileViewerInternal.cs
index bc1bc140174..4c361139f3b 100644
--- a/GitUI/Editor/FileViewerInternal.cs
+++ b/GitUI/Editor/FileViewerInternal.cs
@@ -1,12 +1,15 @@
using System;
+using System.Collections.Generic;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using GitCommands;
using GitExtUtils.GitUI;
using GitUI.Editor.Diff;
+using GitUI.Properties;
using ICSharpCode.TextEditor;
using ICSharpCode.TextEditor.Document;
+using Microsoft.VisualStudio.Threading;
namespace GitUI.Editor
{
@@ -28,6 +31,7 @@ public partial class FileViewerInternal : GitModuleControl, IFileViewer
private readonly CurrentViewPositionCache _currentViewPositionCache;
private DiffViewerLineNumberControl _lineNumbersControl;
private DiffHighlightService _diffHighlightService = DiffHighlightService.Instance;
+ private BlameAuthorMargin _authorsAvatarMargin;
public FileViewerInternal()
{
@@ -547,5 +551,16 @@ public TestAccessor(FileViewerInternal control)
public TextEditorControl TextEditor => _control.TextEditor;
}
+
+ public void SetGutterAvatars(List> avatars)
+ {
+ if (_authorsAvatarMargin == null)
+ {
+ _authorsAvatarMargin = new BlameAuthorMargin(TextEditor.ActiveTextAreaControl.TextArea);
+ TextEditor.ActiveTextAreaControl.TextArea.InsertLeftMargin(0, _authorsAvatarMargin);
+ }
+
+ _authorsAvatarMargin.SetAvatars(avatars);
+ }
}
}
\ No newline at end of file
diff --git a/GitUI/GitUI.csproj b/GitUI/GitUI.csproj
index 8200be4be2a..d8ead9a07be 100644
--- a/GitUI/GitUI.csproj
+++ b/GitUI/GitUI.csproj
@@ -332,6 +332,7 @@
+
diff --git a/GitUI/UserControls/BlameControl.cs b/GitUI/UserControls/BlameControl.cs
index 20a59545af4..8d9cf49e768 100644
--- a/GitUI/UserControls/BlameControl.cs
+++ b/GitUI/UserControls/BlameControl.cs
@@ -1,18 +1,23 @@
using System;
using System.Collections.Generic;
using System.Drawing;
+using System.Drawing.Imaging;
using System.Globalization;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using System.Windows.Forms;
using GitCommands;
using GitExtUtils;
+using GitUI.Avatars;
using GitUI.BranchTreePanel;
using GitUI.CommitInfo;
using GitUI.Editor;
using GitUI.HelperDialogs;
using GitUIPluginInterfaces;
+using ICSharpCode.TextEditor;
using JetBrains.Annotations;
+using Microsoft.VisualStudio.Threading;
namespace GitUI.Blame
{
@@ -268,7 +273,10 @@ private void BlameFile_VScrollPositionChanged(object sender, EventArgs e)
private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList children, Control controlToMask, int lineNumber, int scrollpos)
{
- var (gutter, body) = BuildBlameContents(filename);
+ var avatarSize = BlameAuthor.Font.Height + 1;
+ var (gutter, body, avatars) = BuildBlameContents(filename, avatarSize);
+
+ BlameAuthor.SetGutterAvatars(avatars);
ThreadHelper.JoinableTaskFactory.RunAsync(
() => BlameAuthor.ViewTextAsync("committer.txt", gutter));
@@ -293,7 +301,8 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList> avatars) BuildBlameContents(string filename,
+ int avatarSize)
{
var body = new StringBuilder(capacity: 4096);
@@ -320,14 +329,31 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList> avatars = new List>();
+ Dictionary> cacheAvatars = new Dictionary>();
+ var noAvatar = ThreadHelper.JoinableTaskFactory.RunAsync(() => Task.FromResult(null));
foreach (var line in _blame.Lines)
{
if (line.Commit == lastCommit)
{
+ avatars.Add(noAvatar);
gutter.AppendLine(emptyLine);
}
else
{
+ var authorEmail = line.Commit.AuthorMail.Trim('<', '>');
+ if (cacheAvatars.ContainsKey(authorEmail))
+ {
+ avatars.Add(cacheAvatars[authorEmail]);
+ }
+ else
+ {
+ var avatar = ThreadHelper.JoinableTaskFactory.RunAsync(async () => await AvatarService.Default.GetAvatarAsync(authorEmail, line.Commit.Author,
+ avatarSize));
+ cacheAvatars.Add(authorEmail, avatar);
+ avatars.Add(avatar);
+ }
+
BuildAuthorLine(line, lineBuilder, dateTimeFormat, filename, AppSettings.BlameShowAuthor, AppSettings.BlameShowAuthorDate, AppSettings.BlameShowOriginalFilePath, AppSettings.BlameDisplayAuthorFirst);
gutter.Append(lineBuilder);
@@ -340,7 +366,7 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList _control.BuildAuthorLine(line, lineBuilder, dateTimeFormat, filename, showAuthor, showAuthorDate, showOriginalFilePath, displayAuthorFirst);
- public (string gutter, string body) BuildBlameContents(string filename) => _control.BuildBlameContents(filename);
+ public (string gutter, string body, List> avatars) BuildBlameContents(string filename) => _control.BuildBlameContents(filename, 10);
}
}
}
diff --git a/UnitTests/GitUITests/UserControls/BlameControlTests.cs b/UnitTests/GitUITests/UserControls/BlameControlTests.cs
index 06e314e3e46..5ca21683ca9 100644
--- a/UnitTests/GitUITests/UserControls/BlameControlTests.cs
+++ b/UnitTests/GitUITests/UserControls/BlameControlTests.cs
@@ -94,7 +94,7 @@ public void BuildBlameContents_WithDateAndTime()
{
AppSettings.BlameShowAuthorTime = true;
- var (gutter, content) = _sut.BuildBlameContents("fileName.txt");
+ var (gutter, content, _) = _sut.BuildBlameContents("fileName.txt");
content.Should().Be($"line1{Environment.NewLine}line2{Environment.NewLine}line3{Environment.NewLine}line4{Environment.NewLine}");
var gutterLines = gutter.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
@@ -123,7 +123,7 @@ public void BuildBlameContents_WithDateButNotTime()
AppSettings.BlameShowAuthorTime = false;
// When
- var (gutter, content) = _sut.BuildBlameContents("fileName.txt");
+ var (gutter, content, _) = _sut.BuildBlameContents("fileName.txt");
// Then
content.Should().Be($"line1{Environment.NewLine}line2{Environment.NewLine}line3{Environment.NewLine}line4{Environment.NewLine}");