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

## Line hiding

- **Hide-line rule** — A Highlight Entry with `IsHideLine` set. A line is a
**Hidden Line** when any matching hide-line rule of the active Highlight Group
matches it (`HighlightEvaluator.IsHidden`); no other entry can cancel that,
whatever the order. Display state only: hidden lines stay in the reader and
remain eligible for Log Search, the Window Filter, Filter Pipes and triggers.
Temporary search-hit entries never hide. Rules match the whole line, as for
triggers, even when word mode paints only matches inside columns.
- **Original logical line** — A line index of the Logfile Reader, before hiding.
Reader access, bookmarks, timestamps, row heights, sessions and markers use it.
- **Visible row** — A row of the Log Window's main grid after hiding. Only grid
code deals in rows; convert with `RowToLine` / `LineToRow`.
- **Line Visibility Map** (`LineVisibilityMap`) — The immutable mapping between
visible rows and original logical lines, owned by the **Line Visibility
Tracker** (`LineVisibilityTracker`), which rebuilds it on a cancellable
background scan and extends it on the tail path. Visibility scans never fire
triggers.
- **Show hidden lines** — The per-window, transient override that displays
hidden lines without changing any rule. Explicit navigation to a hidden line
(Go to Line, bookmarks, Log Search, filter results, markers, the command-line
target, the window a time sync starts from) turns it on; tailing, Stop Tail,
windows following a time sync and restoring saved positions never do — they
select the nearest visible line instead. Explicit navigation made before the
load's first visibility scan finishes waits for it.

*Avoid*: "filtered out" for hidden lines (filtering is the **Window Filter**),
"row" when an original logical line is meant.

## Marker Bar

- **Marker Bar** — A compact overview beside a Log Window that shows where
Expand Down
7 changes: 7 additions & 0 deletions src/LogExpert.Core/Classes/Highlight/HighlightEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ public Regex Regex

public bool NoBackground { get; set; }

/// <summary>
/// When true, lines matching this entry are removed from the Log Window's main grid. Display state only:
/// the lines stay in the reader and remain eligible for search, filtering and triggers.
/// </summary>
public bool IsHideLine { get; set; }

public object Clone ()
{
var highLightEntry = new HighlightEntry
Expand All @@ -118,6 +124,7 @@ public object Clone ()
CooldownSeconds = CooldownSeconds,
BookmarkComment = BookmarkComment,
NoBackground = NoBackground,
IsHideLine = IsHideLine,
IsSearchHit = IsSearchHit
};

Expand Down
19 changes: 19 additions & 0 deletions src/LogExpert.Core/Classes/Highlight/HighlightEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ public static IList<HighlightEntry> FindMatchingEntries (IEnumerable<HighlightEn
return result;
}

/// <summary>
/// Returns whether any hide-line entry matches the line. Other matching entries cannot cancel the decision,
/// whatever their order.
/// </summary>
public static bool IsHidden (IEnumerable<HighlightEntry> entries, ITextValueMemory line)
{
ArgumentNullException.ThrowIfNull(entries);
ArgumentNullException.ThrowIfNull(line);

return entries.Any(e => IsHideRule(e) && IsMatch(e, line));
}

/// <summary>Returns whether the entry hides the lines it matches; temporary search-hit entries never do.</summary>
public static bool IsHideRule (HighlightEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
return entry.IsHideLine && !entry.IsSearchHit;
}

/// <summary>
/// Classifies the non-plugin trigger actions implied by a set of matching entries: whether to
/// suppress the dirty LED, stop tailing, set a bookmark, and the concatenated bookmark comment.
Expand Down
222 changes: 222 additions & 0 deletions src/LogExpert.Core/Classes/Highlight/LineVisibilityMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
namespace LogExpert.Core.Classes.Highlight;

/// <summary>
/// Immutable mapping between the rows of a Log Window's main grid and the original logical lines of its
/// Logfile Reader, after highlight-based hiding. Memory is proportional to the number of hidden lines.
/// <para>
/// <see cref="Append"/> shares storage with the map it extends, so appending tail lines is cheap. The storage is
/// append-only: a map only ever reads the prefix it was created with, so an older map stays valid.
/// </para>
/// </summary>
public sealed class LineVisibilityMap
{
private readonly HiddenLineStore _store;

private LineVisibilityMap (HiddenLineStore store, int lineCount, int hiddenCount)
{
_store = store;
LineCount = lineCount;
HiddenCount = hiddenCount;
}

/// <summary>A new map covering no lines. Not a shared instance, so unrelated appends never share storage.</summary>
public static LineVisibilityMap Empty => Identity(0);

/// <summary>Total number of original logical lines covered by the map.</summary>
public int LineCount { get; }

public int HiddenCount { get; }

public int VisibleCount => LineCount - HiddenCount;

/// <summary>A map in which every line is visible.</summary>
public static LineVisibilityMap Identity (int lineCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(lineCount);
return new LineVisibilityMap(new HiddenLineStore([], 0, lineCount), lineCount, 0);
}

/// <summary>Returns the original line displayed in <paramref name="row"/>, or -1 when there is no such row.</summary>
public int RowToLine (int row)
{
if (row < 0 || row >= VisibleCount)
{
return -1;
}

if (HiddenCount == 0)
{
return row;
}

// hidden[i] - i is non-decreasing: count the hidden lines that lie before the row's line.
var items = _store.Items;
int low = 0, high = HiddenCount;
while (low < high)
{
var mid = (low + high) >>> 1;
if (items[mid] - mid <= row)
{
low = mid + 1;
}
else
{
high = mid;
}
}

return row + low;
}

/// <summary>Returns the row displaying <paramref name="line"/>, or -1 when the line is hidden or out of range.</summary>
public int LineToRow (int line)
{
if (line < 0 || line >= LineCount)
{
return -1;
}

var index = Array.BinarySearch(_store.Items, 0, HiddenCount, line);
return index >= 0 ? -1 : line - ~index;
}

public bool IsHidden (int line)
{
return line >= 0 && line < LineCount && Array.BinarySearch(_store.Items, 0, HiddenCount, line) >= 0;
}

/// <summary>
/// Returns the row of <paramref name="line"/> when it is visible; otherwise the row of the next visible line,
/// falling back to the previous one. Returns -1 when no line is visible.
/// </summary>
public int NearestRow (int line)
{
if (VisibleCount == 0)
{
return -1;
}

if (line >= LineCount)
{
return VisibleCount - 1;
}

line = Math.Max(line, 0);
var index = Array.BinarySearch(_store.Items, 0, HiddenCount, line);
var hiddenBefore = index >= 0 ? index : ~index;

// For a hidden line, line - hiddenBefore is the row of the next visible line, if there is one.
return Math.Min(line - hiddenBefore, VisibleCount - 1);
}

/// <summary>
/// Returns a map covering <paramref name="lineCount"/> lines, in which <paramref name="hiddenLines"/>
/// (ascending, all within the appended range) are hidden in addition to this map's hidden lines.
/// </summary>
public LineVisibilityMap Append (int lineCount, IReadOnlyList<int> hiddenLines)
{
ArgumentNullException.ThrowIfNull(hiddenLines);
ArgumentOutOfRangeException.ThrowIfLessThan(lineCount, LineCount);

var previous = LineCount - 1;
// Hidden lines must be ascending and within the appended range.
foreach (var line in hiddenLines)
{
if (line <= previous || line >= lineCount)
{
throw new ArgumentOutOfRangeException(nameof(hiddenLines), line, null);
}

previous = line;
}

var store = _store.TryAppend(LineCount, HiddenCount, lineCount, hiddenLines)
?? HiddenLineStore.CopyAppend(_store.Items, HiddenCount, lineCount, hiddenLines);

return new LineVisibilityMap(store, lineCount, HiddenCount + hiddenLines.Count);
}

/// <summary>Drops the first <paramref name="offset"/> lines (rollover) and renumbers the rest.</summary>
public LineVisibilityMap Shift (int offset)
{
ArgumentOutOfRangeException.ThrowIfNegative(offset);

var items = _store.Items;
var first = Array.BinarySearch(items, 0, HiddenCount, offset);
first = first >= 0 ? first : ~first;

var shifted = new int[HiddenCount - first];
for (var i = 0; i < shifted.Length; i++)
{
shifted[i] = items[first + i] - offset;
}

var lineCount = Math.Max(0, LineCount - offset);
return new LineVisibilityMap(new HiddenLineStore(shifted, shifted.Length, lineCount), lineCount, shifted.Length);
}

/// <summary>
/// True when this map extends <paramref name="other"/> without changing any of its lines, so rows that
/// <paramref name="other"/> displays keep their row index.
/// </summary>
public bool IsAppendOf (LineVisibilityMap other)
{
ArgumentNullException.ThrowIfNull(other);
return ReferenceEquals(_store, other._store) && other.LineCount <= LineCount && other.HiddenCount <= HiddenCount;
}

/// <summary>
/// Append-only storage of hidden line numbers shared by a chain of appended maps. It is only extended in place
/// by the map at its committed end; any other append copies, so no map ever observes a changed prefix.
/// </summary>
private sealed class HiddenLineStore (int[] items, int committedHidden, int committedLines)
{
private readonly Lock _lock = new();
private int _committedHidden = committedHidden;
private int _committedLines = committedLines;
private volatile int[] _items = items;

public int[] Items => _items;

public HiddenLineStore? TryAppend (int fromLines, int fromHidden, int lineCount, IReadOnlyList<int> hiddenLines)
{
lock (_lock)
{
if (_committedLines != fromLines || _committedHidden != fromHidden)
{
return null;
}

var required = fromHidden + hiddenLines.Count;
var target = _items;
if (required > target.Length)
{
target = new int[Math.Max(required, Math.Max(16, target.Length * 2))];
Array.Copy(_items, target, fromHidden);
}

for (var i = 0; i < hiddenLines.Count; i++)
{
target[fromHidden + i] = hiddenLines[i];
}

_items = target;
_committedHidden = required;
_committedLines = lineCount;
return this;
}
}

public static HiddenLineStore CopyAppend (int[] items, int hiddenCount, int lineCount, IReadOnlyList<int> hiddenLines)
{
var copy = new int[hiddenCount + hiddenLines.Count];
Array.Copy(items, copy, hiddenCount);
for (var i = 0; i < hiddenLines.Count; i++)
{
copy[hiddenCount + i] = hiddenLines[i];
}

return new HiddenLineStore(copy, copy.Length, lineCount);
}
}
}
Loading
Loading