Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
81 changes: 80 additions & 1 deletion src/LogExpert.Core/Classes/Bookmark/BookmarkDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,55 @@ public Entities.Bookmark GetBookmarkForLine (int lineNum)

#region Internals

/// <summary>
/// Removes all bookmarks where <see cref="Entities.Bookmark.IsAutoGenerated"/> is <c>true</c>.
/// Manual bookmarks are not affected. Fires <see cref="BookmarkRemoved"/> if any were removed.
/// </summary>
public void RemoveAutoGeneratedBookmarks ()
{
var removed = false;

lock (_bookmarkListLock)
{
List<int> keysToRemove = [.. BookmarkList
.Where(kvp => kvp.Value.IsAutoGenerated)
.Select(kvp => kvp.Key)];

foreach (var key in keysToRemove)
{
_ = BookmarkList.Remove(key);
removed = true;
}
}

if (removed)
{
OnBookmarkRemoved();
}
}

/// <summary>
/// Converts an auto-generated bookmark at the given line to a manual bookmark.
/// Sets <see cref="Entities.Bookmark.IsAutoGenerated"/> to <c>false</c> and clears
/// <see cref="Entities.Bookmark.SourceHighlightText"/>.
/// The bookmark will then survive re-scans.
/// </summary>
/// <returns><c>true</c> if a bookmark was found and converted; <c>false</c> otherwise.</returns>
public bool ConvertToManualBookmark (int lineNum)
{
lock (_bookmarkListLock)
{
if (BookmarkList.TryGetValue(lineNum, out var bookmark) && bookmark.IsAutoGenerated)
{
bookmark.IsAutoGenerated = false;
bookmark.SourceHighlightText = null;
return true;
}
}

return false;
}

public void ShiftBookmarks (int offset)
{
SortedList<int, Entities.Bookmark> newBookmarkList = [];
Expand Down Expand Up @@ -169,7 +218,6 @@ public void RemoveBookmarksForLines (IEnumerable<int> lineNumList)
OnBookmarkRemoved();
}

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

/// <summary>
/// Adds multiple bookmarks in a single batch operation. Fires <see cref="BookmarkAdded"/>
/// only once at the end, avoiding per-item event overhead.
/// Bookmarks whose <see cref="Entities.Bookmark.LineNum"/> already exists in the list are skipped.
/// </summary>
public int AddBookmarks (IEnumerable<Entities.Bookmark> bookmarks)
{
ArgumentNullException.ThrowIfNull(bookmarks, nameof(bookmarks));

var added = 0;

lock (_bookmarkListLock)
{
foreach (var bookmark in bookmarks)
{
if (!BookmarkList.ContainsKey(bookmark.LineNum))
{
BookmarkList.Add(bookmark.LineNum, bookmark);
added++;
}
}
Comment on lines +245 to +252
}

if (added > 0)
{
OnBookmarkAdded();
}

return added;
}

public void ClearAllBookmarks ()
{
#if DEBUG
Expand Down
156 changes: 156 additions & 0 deletions src/LogExpert.Core/Classes/Bookmark/HighlightBookmarkScanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System.Text;

using ColumnizerLib;

using LogExpert.Core.Classes.Highlight;

namespace LogExpert.Core.Classes.Bookmark;

/// <summary>
/// Scans all lines of a log file against highlight entries that have <see cref="HighlightEntry.IsSetBookmark"/> set,
/// producing a list of auto-generated bookmarks. This is a pure computation unit with no UI dependencies.
/// </summary>
public static class HighlightBookmarkScanner
{
/// <summary>
/// Scans lines [0..lineCount) for highlight matches and returns bookmarks for matching lines.
/// </summary>
/// <param name="lineCount">Total number of lines in the file.</param>
/// <param name="getLine">
/// Delegate that returns the log line at a given index. May return null for unavailable lines.
/// </param>
/// <param name="entries">
/// The highlight entries to check. Only entries with <see cref="HighlightEntry.IsSetBookmark"/> == true produce
/// bookmarks.
/// </param>
/// <param name="fileName">
/// The file name, passed to <see cref="ParamParser"/> for bookmark comment template resolution.
/// </param>
/// <param name="progressBarModulo">
/// Interval of lines for reporting progress via the <paramref name="progress"/> callback.
/// </param>
/// <param name="cancellationToken">Token to support cooperative cancellation.</param>
/// <param name="progress">
/// Optional progress callback receiving the current line index (for progress reporting).
/// </param>
/// <returns>List of auto-generated bookmarks for all matched lines.</returns>
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)
{
ArgumentNullException.ThrowIfNull(getLine);

List<Entities.Bookmark> result = [];

// Pre-filter: only entries with IsSetBookmark matter
var bookmarkEntries = entries.Where(e => e.IsSetBookmark).ToList();
if (bookmarkEntries.Count == 0)
{
return result;
}

for (var i = 0; i < lineCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();

var line = getLine(i);
if (line == null)
{
continue;
}

var (setBookmark, bookmarkComment, sourceHighlightText) = GetBookmarkAction(line, bookmarkEntries);

if (setBookmark)
{
var comment = ResolveComment(bookmarkComment, line, i, fileName);
result.Add(Entities.Bookmark.CreateAutoGenerated(i, comment, sourceHighlightText));
}

if (i % progressBarModulo == 0)
{
progress?.Report(i);
}
}

return result;
}

/// <summary>
/// Checks a single line against the bookmark-producing highlight entries. Returns whether a bookmark should be set,
/// the concatenated comment template, and the source highlight text.
/// </summary>
private static (bool SetBookmark, string BookmarkComment, string SourceHighlightText) GetBookmarkAction (ITextValueMemory line, List<HighlightEntry> bookmarkEntries)
{
var setBookmark = false;
var bookmarkCommentBuilder = new StringBuilder();
var sourceHighlightText = string.Empty;

foreach (var entry in bookmarkEntries.Where(entry => CheckHighlightEntryMatch(entry, line)))
{
setBookmark = true;
sourceHighlightText = entry.SearchText;

if (!string.IsNullOrEmpty(entry.BookmarkComment))
{
_ = bookmarkCommentBuilder.Append(entry.BookmarkComment).Append("\r\n");
}
}

return (setBookmark, bookmarkCommentBuilder.ToString().TrimEnd('\r', '\n'), sourceHighlightText);
}

/// <summary>
/// Matches a highlight entry against a line. Replicates the logic from LogWindow.CheckHighlightEntryMatch so the
/// scanner works identically to the existing tail-mode matching.
/// </summary>
private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValueMemory column)
{
if (entry.IsRegex)
{
if (entry.Regex.IsMatch(column.Text.ToString()))
{
return true;
}
}
else
{
if (entry.IsCaseSensitive)
{
if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.Ordinal))
{
return true;
}
}
else
{
if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}

return false;
}

/// <summary>
/// Resolves the bookmark comment template using ParamParser, matching SetBookmarkFromTrigger behavior.
/// </summary>
private static string ResolveComment (string commentTemplate, ILogLineMemory line, int lineNum, string fileName)
{
if (string.IsNullOrEmpty(commentTemplate))
{
return commentTemplate;
}

try
{
var paramParser = new ParamParser(commentTemplate);
return paramParser.ReplaceParams(line, lineNum, fileName);
}
catch (ArgumentException)
{
// Invalid regex in template — return raw template (matches SetBookmarkFromTrigger behavior)
return commentTemplate;
}
}
}
21 changes: 8 additions & 13 deletions src/LogExpert.Core/Classes/Columnizer/TimestampColumnizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,6 @@ public string[] GetColumnNames ()
return ["Date", "Time", "Message"];
}

/// <summary>
/// Determines the priority level for processing a log file based on the presence of recognizable timestamp formats
/// in the provided log lines.
/// </summary>
/// <param name="fileName">The name of the log file to evaluate. Cannot be null.</param>
/// <param name="samples">A collection of log lines to analyze for timestamp patterns. Cannot be null.</param>
/// <returns>A value indicating the priority for processing the specified log file. Returns Priority.WellSupport if the
/// majority of log lines contain recognizable timestamps; otherwise, returns Priority.NotSupport.</returns>
public Priority GetPriority (string fileName, IEnumerable<ILogLine> samples)
{
return GetPriority(fileName, samples.Cast<ILogLineMemory>());
}

/// <summary>
/// Splits a log line into its constituent columns, typically separating date, time, and the remainder of the line.
/// </summary>
Expand Down Expand Up @@ -228,6 +215,14 @@ public void PushValue (ILogLineMemoryColumnizerCallback callback, int column, st
}
}

/// <summary>
/// Determines the priority level for processing a log file based on the presence of recognizable timestamp formats
/// in the provided log lines.
/// </summary>
/// <param name="fileName">The name of the log file to evaluate. Cannot be null.</param>
/// <param name="samples">A collection of log lines to analyze for timestamp patterns. Cannot be null.</param>
/// <returns>A value indicating the priority for processing the specified log file. Returns Priority.WellSupport if the
/// majority of log lines contain recognizable timestamps; otherwise, returns Priority.NotSupport.</returns>
public Priority GetPriority (string fileName, IEnumerable<ILogLineMemory> samples)
{
ArgumentNullException.ThrowIfNull(samples, nameof(samples));
Expand Down
12 changes: 3 additions & 9 deletions src/LogExpert.Core/Classes/Filter/FilterCancelHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,21 @@

namespace LogExpert.Core.Classes.Filter;

public class FilterCancelHandler : IBackgroundProcessCancelHandler
public class FilterCancelHandler (FilterStarter filterStarter) : IBackgroundProcessCancelHandler
{
private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
#region Fields

private readonly FilterStarter _filterStarter;
private readonly FilterStarter _filterStarter = filterStarter;

#endregion

#region cTor

public FilterCancelHandler(FilterStarter filterStarter)
{
_filterStarter = filterStarter;
}

#endregion

#region Public methods

public void EscapePressed()
public void EscapePressed ()
{
_logger.Info(CultureInfo.InvariantCulture, "FilterCancelHandler called.");
_filterStarter.CancelFilter();
Expand Down
2 changes: 1 addition & 1 deletion src/LogExpert.Core/Classes/Filter/FilterStarter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private Filter DoWork (FilterParams filterParams, int startLine, int maxCount, P

// Give every thread own copies of ColumnizerCallback and FilterParams, because the state of the objects changes while filtering
var threadFilterParams = filterParams.CloneWithCurrentColumnizer();
Filter filter = new((ColumnizerCallback)_callback.Clone());
Filter filter = new(_callback.Clone());
lock (_filterWorkerList)
{
_filterWorkerList.Add(filter);
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpert.Core/Classes/Log/LogBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public long Size
get => _size;
}

public int EndLine => StartLine + LineCount;

public int StartLine { set; get; }

public int LineCount { get; private set; }
Expand Down
Loading
Loading