refactor: collecting inlines for subjects (#1402)

Instead of checking intersections of inline elements yourself before adding an inline element, the new class `InlineElementCollector` prevents intersections internally.

Additionally the inline elements are sorted by the new class, so it's no longer necessary to do this after adding the inline elements.
This commit is contained in:
Sina Hinderks 2025-06-08 02:47:03 +02:00 committed by GitHub
parent ba4c0f0cd2
commit fe54d30b70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 79 deletions

View file

@ -121,6 +121,6 @@ namespace SourceGit.Models
public class CommitFullMessage public class CommitFullMessage
{ {
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
public List<InlineElement> Inlines { get; set; } = []; public InlineElementCollector Inlines { get; set; } = [];
} }
} }

View file

@ -0,0 +1,85 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGit.Models
{
public class InlineElementCollector : IEnumerable<InlineElement>
{
private readonly List<InlineElement> _implementation = [];
public void Clear()
{
_implementation.Clear();
AssertInvariant();
}
public int Count => _implementation.Count;
public void Add(InlineElement element)
{
var index = FindIndex(element.Start);
if (!IsIntersection(index, element.Start, element.Length))
_implementation.Insert(index, element);
AssertInvariant();
}
[Conditional("DEBUG")]
private void AssertInvariant()
{
if (_implementation.Count == 0)
return;
for (var index = 1; index < _implementation.Count; index++)
{
var prev = _implementation[index - 1];
var curr = _implementation[index];
Debug.Assert(prev.Start + prev.Length <= curr.Start);
}
}
public InlineElement Lookup(int position)
{
var index = FindIndex(position);
return IsIntersection(index, position, 1)
? _implementation[index]
: null;
}
private int FindIndex(int start)
{
var index = 0;
while (index < _implementation.Count && _implementation[index].Start <= start)
index++;
return index;
}
private bool IsIntersection(int index, int start, int length)
{
if (index > 0)
{
var predecessor = _implementation[index - 1];
if (predecessor.Start + predecessor.Length >= start)
return true;
}
if (index < _implementation.Count)
{
var successor = _implementation[index];
if (start + length >= successor.Start)
return true;
}
return false;
}
public IEnumerator<InlineElement> GetEnumerator() => _implementation.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -46,7 +45,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value); set => SetProperty(ref _urlTemplate, value);
} }
public void Matches(List<InlineElement> outs, string message) public void Matches(InlineElementCollector outs, string message)
{ {
if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return; return;
@ -60,18 +59,6 @@ namespace SourceGit.Models
var start = match.Index; var start = match.Index;
var len = match.Length; var len = match.Length;
var intersect = false;
foreach (var exist in outs)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
var link = _urlTemplate; var link = _urlTemplate;
for (var j = 1; j < match.Groups.Count; j++) for (var j = 1; j < match.Groups.Count; j++)

View file

@ -620,9 +620,9 @@ namespace SourceGit.ViewModels
}); });
} }
private List<Models.InlineElement> ParseInlinesInMessage(string message) private Models.InlineElementCollector ParseInlinesInMessage(string message)
{ {
var inlines = new List<Models.InlineElement>(); var inlines = new Models.InlineElementCollector();
if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules) if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules)
{ {
foreach (var rule in rules) foreach (var rule in rules)
@ -638,18 +638,6 @@ namespace SourceGit.ViewModels
var start = match.Index; var start = match.Index;
var len = match.Length; var len = match.Length;
var intersect = false;
foreach (var link in inlines)
{
if (link.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
var url = message.Substring(start, len); var url = message.Substring(start, len);
if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
@ -665,18 +653,6 @@ namespace SourceGit.ViewModels
var start = match.Index; var start = match.Index;
var len = match.Length; var len = match.Length;
var intersect = false;
foreach (var link in inlines)
{
if (link.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
var sha = match.Groups[1].Value; var sha = match.Groups[1].Value;
var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result(); var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result();
@ -684,9 +660,6 @@ namespace SourceGit.ViewModels
inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha)); inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha));
} }
if (inlines.Count > 0)
inlines.Sort((l, r) => l.Start - r.Start);
return inlines; return inlines;
} }

View file

@ -95,26 +95,11 @@ namespace SourceGit.Views
point = new Point(x, y); point = new Point(x, y);
var pos = TextLayout.HitTestPoint(point).TextPosition; var pos = TextLayout.HitTestPoint(point).TextPosition;
foreach (var link in links)
{
if (!link.Intersect(pos, 1))
continue;
if (link == _lastHover) if (links.Lookup(pos) is { } link)
return; SetHoveredIssueLink(link);
else
SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); ClearHoveredIssueLink();
_lastHover = link;
if (link.Type == Models.InlineElementType.Link)
ToolTip.SetTip(this, link.Link);
else
ProcessHoverCommitLink(link);
return;
}
ClearHoveredIssueLink();
} }
} }
@ -291,6 +276,20 @@ namespace SourceGit.Views
} }
} }
private void SetHoveredIssueLink(Models.InlineElement link)
{
if (link == _lastHover)
return;
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
_lastHover = link;
if (link.Type == Models.InlineElementType.Link)
ToolTip.SetTip(this, link.Link);
else
ProcessHoverCommitLink(link);
}
private void ClearHoveredIssueLink() private void ClearHoveredIssueLink()
{ {
if (_lastHover != null) if (_lastHover != null)

View file

@ -167,19 +167,6 @@ namespace SourceGit.Views
var start = match.Index; var start = match.Index;
var len = match.Length; var len = match.Length;
var intersect = false;
foreach (var exist in _elements)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
_elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty)); _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty));
} }
@ -187,7 +174,6 @@ namespace SourceGit.Views
foreach (var rule in rules) foreach (var rule in rules)
rule.Matches(_elements, subject); rule.Matches(_elements, subject);
_elements.Sort((l, r) => l.Start - r.Start);
_needRebuildInlines = true; _needRebuildInlines = true;
InvalidateVisual(); InvalidateVisual();
} }
@ -364,7 +350,7 @@ namespace SourceGit.Views
} }
} }
private List<Models.InlineElement> _elements = []; private Models.InlineElementCollector _elements = [];
private List<Inline> _inlines = []; private List<Inline> _inlines = [];
private Models.InlineElement _lastHover = null; private Models.InlineElement _lastHover = null;
private bool _needRebuildInlines = false; private bool _needRebuildInlines = false;