Skip to content
Merged
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
22 changes: 22 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ meaning; do not redefine them locally.
construction, since the bulk `HighlightBookmarkScanner` has no access to
the side-effecting triggers.

## Marker Bar

- **Marker Bar** — A compact overview beside a Log Window that shows where
highlights, bookmarks, Log Search hits, and Window Filter hits occur across
the loaded file.
- **Marker** — An indicator in the Marker Bar representing one or more matching
Log Lines. Markers are grouped by category; they do not create or replace
Bookmarks.
- **Marker Snapshot** — An immutable collection of highlight or Log Search
matches discovered for the Marker Bar, together with how many Log Lines
have been scanned and any discovery failure.
- **Bookmark** — A user- or auto-generated annotation attached to a specific
Log Line, carrying optional comment text and an overlay. A Bookmark can be
represented by a Marker in the bookmark category; the two terms are not synonyms.

*Avoid*: "marker" when referring to the underlying Bookmark annotation;
bare "snapshot" when the distinction between **Marker Snapshot**,
**Columnizer Snapshot**, and **Session Snapshot** matters.

## Audio alerts

- **Audio Alert** — A sound played when a tail-only highlight match occurs.
Expand Down Expand Up @@ -293,6 +312,9 @@ layer).
line into columns. Each loaded log window has exactly one active
columnizer at a time. The set of available columnizers is owned by
`PluginRegistry`.
- **Columnizer Snapshot** — An independent Columnizer with the active
Columnizer's configuration and detected column layout, ready for Marker Bar
discovery. Its parsing state is separate from the active Columnizer's.
- **Columnizer Mask Entry** (`ColumnizerMaskEntry`) — One user-configured
row on the Settings → Columnizers tab. Pairs a **Mask**, a **Mask Type**,
and a **Columnizer Name**. Stored in `Preferences.ColumnizerMaskList`.
Expand Down
19 changes: 19 additions & 0 deletions src/ColumnizerLib/IColumnizerSnapshotMemory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace ColumnizerLib;

/// <summary>
/// Optional capability for capturing a columnizer's current parsing state for background use.
/// </summary>
/// <remarks>
/// The snapshot must be initialized with the current configuration and detected column layout,
/// without sharing mutable parsing state with the original columnizer. The Marker Bar captures
/// the snapshot on the UI thread and uses it on a worker thread without calling
/// <see cref="IInitColumnizerMemory.Selected"/>. Capture must be quick and perform no file I/O.
/// </remarks>
public interface IColumnizerSnapshotMemory
{
/// <summary>
/// Creates an independent, initialized columnizer with the current configuration and detected layout.
/// </summary>
/// <returns>A columnizer ready to parse log lines on a worker thread.</returns>
ILogLineMemoryColumnizer CreateSnapshot ();
}
31 changes: 30 additions & 1 deletion src/CsvColumnizer/CsvColumnizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace CsvColumnizer;
/// The IPreProcessColumnizer is implemented to read field names from the very first line of the file. Then
/// the line is dropped. So it's not seen by LogExpert. The field names will be used as column names.
/// </summary>
public class CsvColumnizer : ILogLineMemoryColumnizer, IInitColumnizerMemory, IColumnizerConfiguratorMemory, IPreProcessColumnizerMemory, IColumnizerPriorityMemory
public class CsvColumnizer : ILogLineMemoryColumnizer, IInitColumnizerMemory, IColumnizerConfiguratorMemory, IPreProcessColumnizerMemory, IColumnizerPriorityMemory, IColumnizerSnapshotMemory
{
#region Fields

Expand Down Expand Up @@ -220,6 +220,35 @@ public void DeSelected (ILogLineMemoryColumnizerCallback callback)
// nothing to do
}

public ILogLineMemoryColumnizer CreateSnapshot ()
{
CsvColumnizerConfig config = new()
Comment thread
Hirogen marked this conversation as resolved.
Dismissed
{
CommentChar = _config.CommentChar,
DelimiterChar = _config.DelimiterChar,
EscapeChar = _config.EscapeChar,
HasFieldNames = _config.HasFieldNames,
MinColumns = _config.MinColumns,
QuoteChar = _config.QuoteChar,
VersionBuild = _config.VersionBuild
};
config.ConfigureReaderConfiguration();

CsvColumnizer clone = new()
{
_config = config,
_isValidCsv = _isValidCsv,
_firstLine = _firstLine == null ? null : new CsvLogLine(_firstLine.FullLine.ToString(), _firstLine.LineNumber)
};

foreach (var column in _columnList)
{
clone._columnList.Add(new CsvColumn(column.Name));
}

return clone;
}

public void Configure (ILogLineMemoryColumnizerCallback callback, string configDir)
{
var configPath = configDir + "\\" + CONFIGFILENAME;
Expand Down
20 changes: 19 additions & 1 deletion src/Log4jXmlColumnizer/Log4jXmlColumnizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[assembly: SupportedOSPlatform("windows")]
namespace Log4jXmlColumnizer;

public class Log4jXmlColumnizer : ILogLineMemoryXmlColumnizer, IColumnizerConfiguratorMemory, IColumnizerPriorityMemory
public class Log4jXmlColumnizer : ILogLineMemoryXmlColumnizer, IColumnizerConfiguratorMemory, IColumnizerPriorityMemory, IColumnizerSnapshotMemory
{
#region Fields

Expand Down Expand Up @@ -278,6 +278,24 @@ public void LoadConfig (string configDir)
}
}

public ILogLineMemoryColumnizer CreateSnapshot ()
{
Log4jXmlColumnizer clone = new()
{
_config = new Log4jXmlColumnizerConfig(GetAllColumnNames())
{
LocalTimestamps = _config.LocalTimestamps,
ColumnList = [.. _config.ColumnList.Select(entry => new Log4jColumnEntry(entry.ColumnName, entry.ColumnIndex, entry.MaxLen)
{
Visible = entry.Visible
})]
},
_timeOffset = _timeOffset
};

return clone;
}

public Priority GetPriority (string fileName, IEnumerable<ILogLineMemory> samples)
{
ArgumentNullException.ThrowIfNull(fileName);
Expand Down
29 changes: 20 additions & 9 deletions src/LogExpert.Core/Classes/Bookmark/BookmarkDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ public int GetBookmarkIndexForLine (int lineNum)
}
}

public int[] GetBookmarkLineNumbers ()
{
lock (_bookmarkListLock)
{
return [.. BookmarkList.Keys];
}
}

public Entities.Bookmark GetBookmarkForLine (int lineNum)
{
lock (_bookmarkListLock)
Expand Down Expand Up @@ -148,19 +156,22 @@ public bool ConvertToManualBookmark (int lineNum)

public void ShiftBookmarks (int offset)
{
SortedList<int, Entities.Bookmark> newBookmarkList = [];

foreach (var bookmark in BookmarkList.Values)
lock (_bookmarkListLock)
{
var line = bookmark.LineNum - offset;
if (line >= 0)
SortedList<int, Entities.Bookmark> newBookmarkList = [];

foreach (var bookmark in BookmarkList.Values)
{
bookmark.LineNum = line;
newBookmarkList.Add(line, bookmark);
var line = bookmark.LineNum - offset;
if (line >= 0)
{
bookmark.LineNum = line;
newBookmarkList.Add(line, bookmark);
}
}
}

BookmarkList = newBookmarkList;
BookmarkList = newBookmarkList;
}
}

public int FindPrevBookmarkIndex (int lineNum)
Expand Down
15 changes: 14 additions & 1 deletion src/LogExpert.Core/Classes/Columnizer/SquareBracketColumnizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace LogExpert.Core.Classes.Columnizer;
/// memory-efficient log line processing and columnizer prioritization, making it suitable for integration with log
/// viewers or analysis tools that require flexible column extraction.
/// </remarks>
public class SquareBracketColumnizer : ILogLineMemoryColumnizer, IColumnizerPriorityMemory
public class SquareBracketColumnizer : ILogLineMemoryColumnizer, IColumnizerPriorityMemory, IColumnizerSnapshotMemory
{
#region ILogLineMemoryColumnizer implementation

Expand Down Expand Up @@ -41,6 +41,19 @@ public SquareBracketColumnizer (int columnCount, bool isTimeExists) : this()
}
}

/// <summary>
/// Creates an independent copy of the detected column layout and time offset.
/// </summary>
public ILogLineMemoryColumnizer CreateSnapshot ()
{
return new SquareBracketColumnizer
{
_columnCount = _columnCount,
_isTimeExists = _isTimeExists,
_timeOffset = _timeOffset
};
}

/// <summary>
/// Determines whether timeshift functionality is implemented.
/// </summary>
Expand Down
57 changes: 57 additions & 0 deletions src/LogExpert.Core/Classes/Marker/MarkerBucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace LogExpert.Core.Classes.Marker;

/// <summary>A populated vertical pixel, including its logical range and navigation target.</summary>
public readonly record struct MarkerBucket (int Pixel, int FirstLine, int LastLine, int Count, int TargetLine, int ColorArgb)
{
/// <summary>Aggregates a line-ordered index without sampling and resolves inherited foregrounds; duplicate lines count only once.</summary>
public static IReadOnlyList<MarkerBucket> Aggregate (IEnumerable<MarkerLine> matches, int lineCount, int height, int defaultForegroundArgb)
{
ArgumentNullException.ThrowIfNull(matches);
if (lineCount <= 0 || height <= 0)
{
return [];
}

var buckets = new MarkerBucket[height];
var priorities = new int[height];
Array.Fill(priorities, int.MaxValue);
var previousLine = -1;
foreach (var match in matches)
{
var line = match.LineNumber;
if (line < 0 || line >= lineCount)
{
continue;
}

var pixel = lineCount == 1 ? 0 : (int)((long)line * (height - 1) / (lineCount - 1));
var bucket = buckets[pixel];
// Invert the floor mapping above: a bucket starts at ceil(pixel * (lines - 1) / (height - 1))
// and ends immediately before the next bucket starts. The final pixel includes the last line.
var first = height == 1 ? 0 : (int)(((long)pixel * (lineCount - 1) + height - 2) / (height - 1));
var last = height == 1 || pixel == height - 1 || lineCount == 1 ? lineCount - 1
: (int)(((long)(pixel + 1) * (lineCount - 1) + height - 2) / (height - 1)) - 1;
var target = bucket.Count == 0 ? line : bucket.TargetLine;
// Double distances to compare against the midpoint without rounding half-line ties.
var distance = Math.Abs(2L * line - first - last);
var targetDistance = Math.Abs(2L * target - first - last);
if (distance < targetDistance || (distance == targetDistance && line < target))
{
target = line;
}

var color = bucket.ColorArgb;
if (match.Priority < priorities[pixel])
{
priorities[pixel] = match.Priority;
color = match.ColorArgb ?? defaultForegroundArgb;
}

buckets[pixel] = new MarkerBucket(pixel, first, last,
bucket.Count + (line == previousLine ? 0 : 1), target, color);
previousLine = line;
}

return buckets.Where(bucket => bucket.Count > 0).ToArray();
}
}
121 changes: 121 additions & 0 deletions src/LogExpert.Core/Classes/Marker/MarkerCriteria.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Drawing;

using ColumnizerLib;

using LogExpert.Core.Classes.Highlight;
using LogExpert.Core.Entities;

namespace LogExpert.Core.Classes.Marker;

/// <summary>Snapshot of the matching and visual portions of marker criteria. Never invokes triggers.</summary>
public sealed class MarkerCriteria
{
private readonly HighlightEntry[] _entries;
private HighlightEntry? _search;

public bool IsEmpty => _search == null && !_entries.Any(IsVisual);

private MarkerCriteria (HighlightEntry[] entries)
{
_entries = entries;
}

public static MarkerCriteria ForHighlights (IEnumerable<HighlightEntry> entries)
{
ArgumentNullException.ThrowIfNull(entries);
return new MarkerCriteria(entries.Select(entry => (HighlightEntry)entry.Clone()).ToArray());
}

public static MarkerCriteria ForSearch (SearchParams search, int colorArgb)
{
ArgumentNullException.ThrowIfNull(search);
return new MarkerCriteria([])
{
_search = string.IsNullOrEmpty(search.SearchText) ? null : new HighlightEntry
{
SearchText = search.SearchText,
IsRegex = search.IsRegex,
IsCaseSensitive = search.IsCaseSensitive,
BackgroundColor = Color.FromArgb(colorArgb)
}
};
}

public MarkerLine? Match (int lineNumber, ILogLineMemory line,
Func<int, ILogLineMemory, IReadOnlyList<ITextValueMemory>>? getColumns = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(line);
cancellationToken.ThrowIfCancellationRequested();
if (_search != null)
{
var matched = _search.IsRegex
? _search.Regex.IsMatch(line.FullLine.Span)
: line.FullLine.Span.Contains(_search.SearchText,
_search.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
return matched ? new MarkerLine(lineNumber, _search.BackgroundColor.ToArgb()) : null;
}

IReadOnlyList<ITextValueMemory>? columns = null;
for (var priority = 0; priority < _entries.Length; priority++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = _entries[priority];
if (!IsVisual(entry))
{
continue;
}

bool matched;
if (entry.IsWordMatch)
{
columns ??= getColumns?.Invoke(lineNumber, line)
?? [new Column { FullValue = line.FullLine }];
matched = columns.Any(column => HasVisibleWordMatch(entry, column));
}
else
{
matched = HighlightEvaluator.IsMatch(entry, line);
}

if (matched)
{
int? color = HasBackground(entry) ? entry.BackgroundColor.ToArgb()
: HasForeground(entry) ? entry.ForegroundColor.ToArgb() : null;
return new MarkerLine(lineNumber, color, priority);
}
}

return null;
}

private static bool IsVisual (HighlightEntry entry)
{
return !entry.IsSearchHit && (HasBackground(entry)
|| HasForeground(entry) || entry.IsBold);
}

private static bool HasBackground (HighlightEntry entry)
{
return (!entry.IsWordMatch || !entry.NoBackground) && entry.BackgroundColor.A > 0;
}

private static bool HasForeground (HighlightEntry entry)
{
return entry.ForegroundColor.A > 0;
}

private static bool HasVisibleWordMatch (HighlightEntry entry, ITextValueMemory column)
{
// Word highlighting uses Regex even for literal text, against each displayed column.
foreach (var match in entry.Regex.EnumerateMatches(column.Text.Span))
{
if (match.Length > 0)
{
return true;
}
}

return false;
}
}
Loading
Loading