Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Repository instructions

Before inspecting or changing this repository, read [.github\CLAUDE.md](CLAUDE.md), [CONTEXT.md](CONTEXT.md) and
[.github\copilot-instructions.md](copilot-instructions.md) in full. All three files apply to every coding agent, including Codex.
They are the authoritative repository instructions; this file is an entry point,
not a replacement or summary. Follow their links whenever the stated conditions apply.

If either file is missing, unreadable, or truncated, resolve that before changing files.
After context compaction, reload either document if its complete instructions are no
longer available. Re-read a document when it changes during the task.

Before editing C#, read the applicable `.editorconfig` and neighboring handwritten
code. Before declaring work complete, perform the evidence-based standards review
required by CLAUDE.md.
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
54 changes: 54 additions & 0 deletions src/LogExpert.Core/Classes/Marker/MarkerBucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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];
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;
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();
}
}
118 changes: 118 additions & 0 deletions src/LogExpert.Core/Classes/Marker/MarkerCriteria.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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];
var background = (!entry.IsWordMatch || !entry.NoBackground) && entry.BackgroundColor.A > 0;
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)
{
columns ??= getColumns?.Invoke(lineNumber, line)
?? [new Column { FullValue = line.FullLine }];
matched = columns.Any(column => HighlightEvaluator.IsMatch(entry, column));
}
}

if (matched)
{
var color = background ? entry.BackgroundColor
: entry.ForegroundColor.A > 0 ? entry.ForegroundColor : Color.Gray;
return new MarkerLine(lineNumber, color.ToArgb(), priority);
}
}

return null;
}

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

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