From fe54d30b7010f7e10ebcc1f7b92dc032cb4a7bbb Mon Sep 17 00:00:00 2001 From: Sina Hinderks Date: Sun, 8 Jun 2025 02:47:03 +0200 Subject: [PATCH] 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. --- src/Models/Commit.cs | 2 +- src/Models/InlineElementCollector.cs | 85 ++++++++++++++++++++++++++++ src/Models/IssueTrackerRule.cs | 17 +----- src/ViewModels/CommitDetail.cs | 31 +--------- src/Views/CommitMessagePresenter.cs | 37 ++++++------ src/Views/CommitSubjectPresenter.cs | 16 +----- 6 files changed, 109 insertions(+), 79 deletions(-) create mode 100644 src/Models/InlineElementCollector.cs diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index ced7597e..6bf2655a 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -121,6 +121,6 @@ namespace SourceGit.Models public class CommitFullMessage { public string Message { get; set; } = string.Empty; - public List Inlines { get; set; } = []; + public InlineElementCollector Inlines { get; set; } = []; } } diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs new file mode 100644 index 00000000..b4b2e9e1 --- /dev/null +++ b/src/Models/InlineElementCollector.cs @@ -0,0 +1,85 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SourceGit.Models +{ + public class InlineElementCollector : IEnumerable + { + private readonly List _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 GetEnumerator() => _implementation.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index fe0fe8e0..30bce596 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; @@ -46,7 +45,7 @@ namespace SourceGit.Models set => SetProperty(ref _urlTemplate, value); } - public void Matches(List outs, string message) + public void Matches(InlineElementCollector outs, string message) { if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) return; @@ -60,18 +59,6 @@ namespace SourceGit.Models var start = match.Index; 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; for (var j = 1; j < match.Groups.Count; j++) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 02db9b4f..8cc18152 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -620,9 +620,9 @@ namespace SourceGit.ViewModels }); } - private List ParseInlinesInMessage(string message) + private Models.InlineElementCollector ParseInlinesInMessage(string message) { - var inlines = new List(); + var inlines = new Models.InlineElementCollector(); if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules) { foreach (var rule in rules) @@ -638,18 +638,6 @@ namespace SourceGit.ViewModels var start = match.Index; 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); if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) @@ -665,18 +653,6 @@ namespace SourceGit.ViewModels var start = match.Index; 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 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)); } - if (inlines.Count > 0) - inlines.Sort((l, r) => l.Start - r.Start); - return inlines; } diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs index 0858640b..87433c45 100644 --- a/src/Views/CommitMessagePresenter.cs +++ b/src/Views/CommitMessagePresenter.cs @@ -95,26 +95,11 @@ namespace SourceGit.Views point = new Point(x, y); var pos = TextLayout.HitTestPoint(point).TextPosition; - foreach (var link in links) - { - if (!link.Intersect(pos, 1)) - continue; - 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); - - return; - } - - ClearHoveredIssueLink(); + if (links.Lookup(pos) is { } link) + SetHoveredIssueLink(link); + else + 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() { if (_lastHover != null) diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index 18902462..b860ec1d 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -167,19 +167,6 @@ namespace SourceGit.Views var start = match.Index; 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)); } @@ -187,7 +174,6 @@ namespace SourceGit.Views foreach (var rule in rules) rule.Matches(_elements, subject); - _elements.Sort((l, r) => l.Start - r.Start); _needRebuildInlines = true; InvalidateVisual(); } @@ -364,7 +350,7 @@ namespace SourceGit.Views } } - private List _elements = []; + private Models.InlineElementCollector _elements = []; private List _inlines = []; private Models.InlineElement _lastHover = null; private bool _needRebuildInlines = false;