Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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, ICloneable
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
{
#region Fields

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

public object Clone ()
{
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, ICloneable
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
{
#region Fields

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

public object Clone ()
{
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, ICloneable
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
{
#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 object Clone ()
{
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; duplicate lines count only once.</summary>
public static IReadOnlyList<MarkerBucket> Aggregate (IEnumerable<MarkerLine> matches, int lineCount, int height)
{
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;
}

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();
}
}
115 changes: 115 additions & 0 deletions src/LogExpert.Core/Classes/Marker/MarkerCriteria.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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)
{
var color = HasBackground(entry) ? entry.BackgroundColor : entry.ForegroundColor;
return new MarkerLine(lineNumber, color.ToArgb(), priority);
}
}

return null;
}

private static bool IsVisual (HighlightEntry entry)
{
return !entry.IsSearchHit && (HasBackground(entry)
|| entry.ForegroundColor.A > 0 || entry.IsBold);
}

private static bool HasBackground (HighlightEntry entry)
{
return (!entry.IsWordMatch || !entry.NoBackground) && entry.BackgroundColor.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