Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions VSGitBlame.Core/CommitInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ namespace VSGitBlame.Core;

public class CommitInfo
{
public static readonly CommitInfo InProgress = new CommitInfo() { ShowDetails = false, Summary = "Blame in progress.." };
public static readonly CommitInfo Uncommitted = new CommitInfo() { ShowDetails = false, Summary = "Uncommitted changes"};

public bool ShowDetails { get; set; } = true;
public string Hash { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public string AuthorEmail { get; set; } = string.Empty;
Expand Down
19 changes: 11 additions & 8 deletions VSGitBlame.Core/FileBlameInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@ namespace VSGitBlame.Core;

public class FileBlameInfo
{
Dictionary<int, string> _lineCommitCache;
private static readonly Dictionary<int, string> defaultEmptyLineCollection = new(0);

Dictionary<int, string> _lineCommitCache = defaultEmptyLineCollection;
static readonly string[] _timeZoneFormats = [@"\+hhmm", @"\-hhmm"];

public FileBlameInfo(string porcelainBlameString)
public void Parse(string porcelainBlameString)
{
Dictionary<int, string> output = null;
try
{
output = ParsePorcelainOutput(porcelainBlameString);
_lineCommitCache = ParsePorcelainOutput(porcelainBlameString);
}
catch
{
// TODO: Stop silent failure and implement logging/telemetry
output = new();
}

_lineCommitCache = output;
}


Expand Down Expand Up @@ -79,6 +77,11 @@ Dictionary<int, string> ParsePorcelainOutput(string output)
{
lines.CropTillNth(newLine, 9);
}
else if (int.TryParse(hash, out int zeroHash) && zeroHash == 0)
{
CommitInfoCache.Add(hash, CommitInfo.Uncommitted);
lines.CropTillNth(newLine, 9);
}
else
{
line = lines.SliceTill(newLine);
Expand All @@ -97,7 +100,7 @@ Dictionary<int, string> ParsePorcelainOutput(string output)

line = lines.SliceTill(newLine);
lines.CropTillNth(newLine);

line.CropTillNth(space);
string timeZone = line.ToString();
TimeSpan timeZoneOffset = TimeSpan.ParseExact(timeZone, _timeZoneFormats, CultureInfo.InvariantCulture);
Expand Down
53 changes: 33 additions & 20 deletions VSGitBlame/CommitInfoAdornment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Windows.Controls;
using VSGitBlame.Core;
using System.Windows.Input;
using System;

namespace VSGitBlame;

Expand All @@ -21,10 +22,22 @@ public CommitInfoAdornment(IWpfTextView view)
_textDocument = _view.TextBuffer.Properties.GetProperty<ITextDocument>(typeof(ITextDocument));

// Event Subscriptions
_view.LayoutChanged += OnLayoutChanged;
_view.GotAggregateFocus += (sender, args) => RefreshBlameOnCurrentLine();
_view.LayoutChanged += (sender, args) => RefreshBlameOnCurrentLine();
_view.Closed += (sender, args) => GitBlamer.OnBlameFinished -= OnBlameFinished;

_textDocument.FileActionOccurred += TextDocument_FileActionOccurred;
_view.Caret.PositionChanged += Caret_PositionChanged;
//_view.VisualElement.MouseLeftButtonUp += VisualElement_MouseLeftButtonUp;

GitBlamer.OnBlameFinished += OnBlameFinished;

RefreshBlameOnCurrentLine();
}

private void RefreshBlameOnCurrentLine()
{
OnCaretLineChanged(_lastCaretLine, new CaretPositionChangedEventArgs(_view, _view.Caret.Position, _view.Caret.Position));
}

private void Caret_PositionChanged(object sender, CaretPositionChangedEventArgs e)
Expand All @@ -37,31 +50,46 @@ private void Caret_PositionChanged(object sender, CaretPositionChangedEventArgs
}
}


private void TextDocument_FileActionOccurred(object sender, TextDocumentFileActionEventArgs e)
{
GitBlamer.InvalidateCache(_textDocument.FilePath);
RefreshBlameOnCurrentLine();
}

private void OnCaretLineChanged(int lineNumber, CaretPositionChangedEventArgs e)
private void OnBlameFinished(object sender, string filePath)
{
_adornmentLayer.RemoveAllAdornments();
if (filePath != _textDocument.FilePath)
return;

RefreshBlameOnCurrentLine();
}

private void OnCaretLineChanged(int lineNumber, CaretPositionChangedEventArgs e)
{
// Only show commit info if the document is not dirty (no unsaved changes)
if (_textDocument.IsDirty)
{
_adornmentLayer.RemoveAllAdornments();
return;
}

// Get the caret position in the view
var caretPosition = e.NewPosition.BufferPosition;
var textViewLine = _view.GetTextViewLineContainingBufferPosition(caretPosition);

if (textViewLine == null)
{
_adornmentLayer.RemoveAllAdornments();
return;
}

var commitInfo = GitBlamer.GetBlame(_textDocument.FilePath, lineNumber + 1);
var commitInfo = GitBlamer.GetBlame(_textDocument.FilePath, Math.Max(0, lineNumber) + 1);

if (commitInfo == null)
{
_adornmentLayer.RemoveAllAdornments();
return;
}

ShowCommitInfo(commitInfo, textViewLine);
}
Expand Down Expand Up @@ -106,28 +134,13 @@ private void VisualElement_MouseLeftButtonUp(object sender, MouseButtonEventArgs
ShowCommitInfo(commitInfo, textView);
}

void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
foreach (var line in e.NewOrReformattedLines)
{
CreateVisuals(line);
}
}

void CreateVisuals(ITextViewLine line)
{
// Clear previous adornments
_adornmentLayer.RemoveAllAdornments();
}

void ShowCommitInfo(CommitInfo commitInfo, ITextViewLine line)
{
var container = CommitInfoViewFactory.Get(commitInfo, _adornmentLayer);

Canvas.SetLeft(container, line.Right);
Canvas.SetTop(container, line.Top);

_adornmentLayer.RemoveAllAdornments();
SnapshotSpan span = new SnapshotSpan(_adornmentLayer.TextView.TextSnapshot, Span.FromBounds(line.Start, line.End));
_adornmentLayer.AddAdornment(AdornmentPositioningBehavior.TextRelative, span, null, container, null);
}
Expand Down
23 changes: 18 additions & 5 deletions VSGitBlame/CommitInfoViewFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static class CommitInfoViewFactory

static bool _firstMouseMoveFired = false;
static bool _isDetailsVisible = false;
static bool _showDetails = false;
static IAdornmentLayer _adornmentLayer;

private static VSGitBlamePackage _package;
Expand Down Expand Up @@ -153,7 +154,7 @@ static CommitInfoViewFactory()
return;
}

if (_isDetailsVisible)
if (_isDetailsVisible || _showDetails == false)
return;

_detailsViewContainer.Visibility = Visibility.Visible;
Expand Down Expand Up @@ -190,14 +191,26 @@ public static Border Get(CommitInfo commitInfo, IAdornmentLayer adornmentLayer)
_adornmentLayer = adornmentLayer;
}

_summaryView.Text = $"{commitInfo.AuthorName}, {commitInfo.Time:yyyy/MM/dd HH:mm} • {commitInfo.Summary}";
_profileIcon.Source = new BitmapImage(new Uri(GetGravatarUrl(commitInfo.AuthorEmail), UriKind.Absolute));
_commitDetailsView.Text =
$"""
if (commitInfo.ShowDetails == false)
{
_summaryView.Text = commitInfo.Summary;
_profileIcon.Source = null;
_commitDetailsView.Text = string.Empty;
_detailsViewContainer.Visibility = Visibility.Hidden;
}
else
{
_summaryView.Text = $"{commitInfo.AuthorName}, {commitInfo.Time:yyyy/MM/dd HH:mm} • {commitInfo.Summary}";
_profileIcon.Source = new BitmapImage(new Uri(GetGravatarUrl(commitInfo.AuthorEmail), UriKind.Absolute));
_commitDetailsView.Text =
$"""
{commitInfo.AuthorName} | {commitInfo.Time:f}
{commitInfo.Summary}
Commit: {commitInfo.Hash.Substring(7)}
""";
}

_showDetails = commitInfo.ShowDetails;
_container.Visibility = Visibility.Visible;

return _container;
Expand Down
49 changes: 30 additions & 19 deletions VSGitBlame/GitBlamer.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
using System.Collections.Generic;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

using VSGitBlame.Core;

namespace VSGitBlame;

internal static class GitBlamer
{
static Dictionary<string, FileBlameInfo> _gitBlameCache = new();
private static ConcurrentDictionary<string, FileBlameInfo> _gitBlameCache = new();
public static EventHandler<string> OnBlameFinished;

public static void InvalidateCache(string filePath)
{
_gitBlameCache.TryRemove(filePath, out _);
}

public static void InvalidateCache(string filePath) =>
_gitBlameCache.Remove(filePath);

public static CommitInfo GetBlame(string filePath, int line)
{
if (_gitBlameCache.TryGetValue(filePath, out var fileBlameInfo) == false)
fileBlameInfo = InitialiseFile(filePath);
if (_gitBlameCache.TryGetValue(filePath, out FileBlameInfo fileBlameInfo))
return fileBlameInfo != null ? fileBlameInfo.GetAt(line) : CommitInfo.InProgress;

if (fileBlameInfo == null)
return null;
var curContext = SynchronizationContext.Current;
_ = InitialiseFileAsync(filePath);

return fileBlameInfo.GetAt(line);
return CommitInfo.InProgress;
}


static FileBlameInfo InitialiseFile(string filePath)
static async Task InitialiseFileAsync(string filePath)
{
_gitBlameCache[filePath] = null;

string command = $"git blame {filePath} --porcelain";

using Process process = new Process();
Expand All @@ -38,18 +48,19 @@ static FileBlameInfo InitialiseFile(string filePath)
WorkingDirectory = Path.GetDirectoryName(filePath)
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit();

if (string.IsNullOrEmpty(result) || process.ExitCode != 0)
{
_gitBlameCache[filePath] = null;
return null;
}
string result = await process.StandardOutput.ReadToEndAsync();

var blameInfo = new FileBlameInfo(result);
// We invalidated the file during the process, so we don't need the output anymore
if (_gitBlameCache.ContainsKey(filePath) == false)
return;

var blameInfo = new FileBlameInfo();
_gitBlameCache[filePath] = blameInfo;

return blameInfo;
if (process.ExitCode == 0 && string.IsNullOrEmpty(result) == false)
blameInfo.Parse(result);

OnBlameFinished.Invoke(null, filePath);
}
}