Skip to content

Commit cbc45d1

Browse files
authored
Merge pull request #561 from LogExperts/112-bug-bookmarks-are-not-working-for-highlight-triggers
Optimisations and Bookmark Trigger on Static Files
2 parents 003ee1c + f45fa97 commit cbc45d1

25 files changed

Lines changed: 2487 additions & 837 deletions

src/LogExpert.Core/Classes/Bookmark/BookmarkDataProvider.cs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,55 @@ public Entities.Bookmark GetBookmarkForLine (int lineNum)
9797

9898
#region Internals
9999

100+
/// <summary>
101+
/// Removes all bookmarks where <see cref="Entities.Bookmark.IsAutoGenerated"/> is <c>true</c>.
102+
/// Manual bookmarks are not affected. Fires <see cref="BookmarkRemoved"/> if any were removed.
103+
/// </summary>
104+
public void RemoveAutoGeneratedBookmarks ()
105+
{
106+
var removed = false;
107+
108+
lock (_bookmarkListLock)
109+
{
110+
List<int> keysToRemove = [.. BookmarkList
111+
.Where(kvp => kvp.Value.IsAutoGenerated)
112+
.Select(kvp => kvp.Key)];
113+
114+
foreach (var key in keysToRemove)
115+
{
116+
_ = BookmarkList.Remove(key);
117+
removed = true;
118+
}
119+
}
120+
121+
if (removed)
122+
{
123+
OnBookmarkRemoved();
124+
}
125+
}
126+
127+
/// <summary>
128+
/// Converts an auto-generated bookmark at the given line to a manual bookmark.
129+
/// Sets <see cref="Entities.Bookmark.IsAutoGenerated"/> to <c>false</c> and clears
130+
/// <see cref="Entities.Bookmark.SourceHighlightText"/>.
131+
/// The bookmark will then survive re-scans.
132+
/// </summary>
133+
/// <returns><c>true</c> if a bookmark was found and converted; <c>false</c> otherwise.</returns>
134+
public bool ConvertToManualBookmark (int lineNum)
135+
{
136+
lock (_bookmarkListLock)
137+
{
138+
if (BookmarkList.TryGetValue(lineNum, out var bookmark) && bookmark.IsAutoGenerated)
139+
{
140+
bookmark.IsAutoGenerated = false;
141+
bookmark.SourceHighlightText = null;
142+
return true;
143+
}
144+
}
145+
146+
return false;
147+
}
148+
100149
public void ShiftBookmarks (int offset)
101150
{
102151
SortedList<int, Entities.Bookmark> newBookmarkList = [];
@@ -169,7 +218,6 @@ public void RemoveBookmarksForLines (IEnumerable<int> lineNumList)
169218
OnBookmarkRemoved();
170219
}
171220

172-
//TOOD: check if the callers are checking for null before calling
173221
public void AddBookmark (Entities.Bookmark bookmark)
174222
{
175223
ArgumentNullException.ThrowIfNull(bookmark, nameof(bookmark));
@@ -181,6 +229,37 @@ public void AddBookmark (Entities.Bookmark bookmark)
181229
OnBookmarkAdded();
182230
}
183231

232+
/// <summary>
233+
/// Adds multiple bookmarks in a single batch operation. Fires <see cref="BookmarkAdded"/>
234+
/// only once at the end, avoiding per-item event overhead.
235+
/// Bookmarks whose <see cref="Entities.Bookmark.LineNum"/> already exists in the list are skipped.
236+
/// </summary>
237+
public int AddBookmarks (IEnumerable<Entities.Bookmark> bookmarks)
238+
{
239+
ArgumentNullException.ThrowIfNull(bookmarks, nameof(bookmarks));
240+
241+
var added = 0;
242+
243+
lock (_bookmarkListLock)
244+
{
245+
foreach (var bookmark in bookmarks)
246+
{
247+
if (!BookmarkList.ContainsKey(bookmark.LineNum))
248+
{
249+
BookmarkList.Add(bookmark.LineNum, bookmark);
250+
added++;
251+
}
252+
}
253+
}
254+
255+
if (added > 0)
256+
{
257+
OnBookmarkAdded();
258+
}
259+
260+
return added;
261+
}
262+
184263
public void ClearAllBookmarks ()
185264
{
186265
#if DEBUG
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System.Text;
2+
3+
using ColumnizerLib;
4+
5+
using LogExpert.Core.Classes.Highlight;
6+
7+
namespace LogExpert.Core.Classes.Bookmark;
8+
9+
/// <summary>
10+
/// Scans all lines of a log file against highlight entries that have <see cref="HighlightEntry.IsSetBookmark"/> set,
11+
/// producing a list of auto-generated bookmarks. This is a pure computation unit with no UI dependencies.
12+
/// </summary>
13+
public static class HighlightBookmarkScanner
14+
{
15+
/// <summary>
16+
/// Scans lines [0..lineCount) for highlight matches and returns bookmarks for matching lines.
17+
/// </summary>
18+
/// <param name="lineCount">Total number of lines in the file.</param>
19+
/// <param name="getLine">
20+
/// Delegate that returns the log line at a given index. May return null for unavailable lines.
21+
/// </param>
22+
/// <param name="entries">
23+
/// The highlight entries to check. Only entries with <see cref="HighlightEntry.IsSetBookmark"/> == true produce
24+
/// bookmarks.
25+
/// </param>
26+
/// <param name="fileName">
27+
/// The file name, passed to <see cref="ParamParser"/> for bookmark comment template resolution.
28+
/// </param>
29+
/// <param name="progressBarModulo">
30+
/// Interval of lines for reporting progress via the <paramref name="progress"/> callback.
31+
/// </param>
32+
/// <param name="cancellationToken">Token to support cooperative cancellation.</param>
33+
/// <param name="progress">
34+
/// Optional progress callback receiving the current line index (for progress reporting).
35+
/// </param>
36+
/// <returns>List of auto-generated bookmarks for all matched lines.</returns>
37+
public static List<Entities.Bookmark> Scan (int lineCount, Func<int, ILogLineMemory> getLine, IList<HighlightEntry> entries, string fileName, int progressBarModulo = 1000, IProgress<int> progress = null, CancellationToken cancellationToken = default)
38+
{
39+
ArgumentNullException.ThrowIfNull(getLine);
40+
41+
List<Entities.Bookmark> result = [];
42+
43+
// Pre-filter: only entries with IsSetBookmark matter
44+
var bookmarkEntries = entries.Where(e => e.IsSetBookmark).ToList();
45+
if (bookmarkEntries.Count == 0)
46+
{
47+
return result;
48+
}
49+
50+
for (var i = 0; i < lineCount; i++)
51+
{
52+
cancellationToken.ThrowIfCancellationRequested();
53+
54+
var line = getLine(i);
55+
if (line == null)
56+
{
57+
continue;
58+
}
59+
60+
var (setBookmark, bookmarkComment, sourceHighlightText) = GetBookmarkAction(line, bookmarkEntries);
61+
62+
if (setBookmark)
63+
{
64+
var comment = ResolveComment(bookmarkComment, line, i, fileName);
65+
result.Add(Entities.Bookmark.CreateAutoGenerated(i, comment, sourceHighlightText));
66+
}
67+
68+
if (i % progressBarModulo == 0)
69+
{
70+
progress?.Report(i);
71+
}
72+
}
73+
74+
return result;
75+
}
76+
77+
/// <summary>
78+
/// Checks a single line against the bookmark-producing highlight entries. Returns whether a bookmark should be set,
79+
/// the concatenated comment template, and the source highlight text.
80+
/// </summary>
81+
private static (bool SetBookmark, string BookmarkComment, string SourceHighlightText) GetBookmarkAction (ITextValueMemory line, List<HighlightEntry> bookmarkEntries)
82+
{
83+
var setBookmark = false;
84+
var bookmarkCommentBuilder = new StringBuilder();
85+
var sourceHighlightText = string.Empty;
86+
87+
foreach (var entry in bookmarkEntries.Where(entry => CheckHighlightEntryMatch(entry, line)))
88+
{
89+
setBookmark = true;
90+
sourceHighlightText = entry.SearchText;
91+
92+
if (!string.IsNullOrEmpty(entry.BookmarkComment))
93+
{
94+
_ = bookmarkCommentBuilder.Append(entry.BookmarkComment).Append("\r\n");
95+
}
96+
}
97+
98+
return (setBookmark, bookmarkCommentBuilder.ToString().TrimEnd('\r', '\n'), sourceHighlightText);
99+
}
100+
101+
/// <summary>
102+
/// Matches a highlight entry against a line. Replicates the logic from LogWindow.CheckHighlightEntryMatch so the
103+
/// scanner works identically to the existing tail-mode matching.
104+
/// </summary>
105+
private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValueMemory column)
106+
{
107+
if (entry.IsRegex)
108+
{
109+
if (entry.Regex.IsMatch(column.Text.ToString()))
110+
{
111+
return true;
112+
}
113+
}
114+
else
115+
{
116+
if (entry.IsCaseSensitive)
117+
{
118+
if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.Ordinal))
119+
{
120+
return true;
121+
}
122+
}
123+
else
124+
{
125+
if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.OrdinalIgnoreCase))
126+
{
127+
return true;
128+
}
129+
}
130+
}
131+
132+
return false;
133+
}
134+
135+
/// <summary>
136+
/// Resolves the bookmark comment template using ParamParser, matching SetBookmarkFromTrigger behavior.
137+
/// </summary>
138+
private static string ResolveComment (string commentTemplate, ILogLineMemory line, int lineNum, string fileName)
139+
{
140+
if (string.IsNullOrEmpty(commentTemplate))
141+
{
142+
return commentTemplate;
143+
}
144+
145+
try
146+
{
147+
var paramParser = new ParamParser(commentTemplate);
148+
return paramParser.ReplaceParams(line, lineNum, fileName);
149+
}
150+
catch (ArgumentException)
151+
{
152+
// Invalid regex in template — return raw template (matches SetBookmarkFromTrigger behavior)
153+
return commentTemplate;
154+
}
155+
}
156+
}

src/LogExpert.Core/Classes/Columnizer/TimestampColumnizer.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,6 @@ public string[] GetColumnNames ()
4646
return ["Date", "Time", "Message"];
4747
}
4848

49-
/// <summary>
50-
/// Determines the priority level for processing a log file based on the presence of recognizable timestamp formats
51-
/// in the provided log lines.
52-
/// </summary>
53-
/// <param name="fileName">The name of the log file to evaluate. Cannot be null.</param>
54-
/// <param name="samples">A collection of log lines to analyze for timestamp patterns. Cannot be null.</param>
55-
/// <returns>A value indicating the priority for processing the specified log file. Returns Priority.WellSupport if the
56-
/// majority of log lines contain recognizable timestamps; otherwise, returns Priority.NotSupport.</returns>
57-
public Priority GetPriority (string fileName, IEnumerable<ILogLine> samples)
58-
{
59-
return GetPriority(fileName, samples.Cast<ILogLineMemory>());
60-
}
61-
6249
/// <summary>
6350
/// Splits a log line into its constituent columns, typically separating date, time, and the remainder of the line.
6451
/// </summary>
@@ -228,6 +215,14 @@ public void PushValue (ILogLineMemoryColumnizerCallback callback, int column, st
228215
}
229216
}
230217

218+
/// <summary>
219+
/// Determines the priority level for processing a log file based on the presence of recognizable timestamp formats
220+
/// in the provided log lines.
221+
/// </summary>
222+
/// <param name="fileName">The name of the log file to evaluate. Cannot be null.</param>
223+
/// <param name="samples">A collection of log lines to analyze for timestamp patterns. Cannot be null.</param>
224+
/// <returns>A value indicating the priority for processing the specified log file. Returns Priority.WellSupport if the
225+
/// majority of log lines contain recognizable timestamps; otherwise, returns Priority.NotSupport.</returns>
231226
public Priority GetPriority (string fileName, IEnumerable<ILogLineMemory> samples)
232227
{
233228
ArgumentNullException.ThrowIfNull(samples, nameof(samples));

src/LogExpert.Core/Classes/Filter/FilterCancelHandler.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,21 @@
66

77
namespace LogExpert.Core.Classes.Filter;
88

9-
public class FilterCancelHandler : IBackgroundProcessCancelHandler
9+
public class FilterCancelHandler (FilterStarter filterStarter) : IBackgroundProcessCancelHandler
1010
{
1111
private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
1212
#region Fields
1313

14-
private readonly FilterStarter _filterStarter;
14+
private readonly FilterStarter _filterStarter = filterStarter;
1515

1616
#endregion
17-
1817
#region cTor
1918

20-
public FilterCancelHandler(FilterStarter filterStarter)
21-
{
22-
_filterStarter = filterStarter;
23-
}
24-
2519
#endregion
2620

2721
#region Public methods
2822

29-
public void EscapePressed()
23+
public void EscapePressed ()
3024
{
3125
_logger.Info(CultureInfo.InvariantCulture, "FilterCancelHandler called.");
3226
_filterStarter.CancelFilter();

src/LogExpert.Core/Classes/Filter/FilterStarter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ private Filter DoWork (FilterParams filterParams, int startLine, int maxCount, P
147147

148148
// Give every thread own copies of ColumnizerCallback and FilterParams, because the state of the objects changes while filtering
149149
var threadFilterParams = filterParams.CloneWithCurrentColumnizer();
150-
Filter filter = new((ColumnizerCallback)_callback.Clone());
150+
Filter filter = new(_callback.Clone());
151151
lock (_filterWorkerList)
152152
{
153153
_filterWorkerList.Add(filter);

src/LogExpert.Core/Classes/Log/LogBuffer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public long Size
5757
get => _size;
5858
}
5959

60+
public int EndLine => StartLine + LineCount;
61+
6062
public int StartLine { set; get; }
6163

6264
public int LineCount { get; private set; }

0 commit comments

Comments
 (0)