Skip to content

Commit 1637c2b

Browse files
authored
Merge pull request #718 from LogExperts/feature/338-hide-line
Feature/338 hide line
2 parents d9845e2 + 3a7972a commit 1637c2b

25 files changed

Lines changed: 3114 additions & 229 deletions

‎CONTEXT.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,35 @@ meaning; do not redefine them locally.
2323
construction, since the bulk `HighlightBookmarkScanner` has no access to
2424
the side-effecting triggers.
2525

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

2857
- **Marker Bar** — A compact overview beside a Log Window that shows where

‎src/LogExpert.Core/Classes/Highlight/HighlightEntry.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ public Regex Regex
9797

9898
public bool NoBackground { get; set; }
9999

100+
/// <summary>
101+
/// When true, lines matching this entry are removed from the Log Window's main grid. Display state only:
102+
/// the lines stay in the reader and remain eligible for search, filtering and triggers.
103+
/// </summary>
104+
public bool IsHideLine { get; set; }
105+
100106
public object Clone ()
101107
{
102108
var highLightEntry = new HighlightEntry
@@ -118,6 +124,7 @@ public object Clone ()
118124
CooldownSeconds = CooldownSeconds,
119125
BookmarkComment = BookmarkComment,
120126
NoBackground = NoBackground,
127+
IsHideLine = IsHideLine,
121128
IsSearchHit = IsSearchHit
122129
};
123130

‎src/LogExpert.Core/Classes/Highlight/HighlightEvaluator.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,25 @@ public static IList<HighlightEntry> FindMatchingEntries (IEnumerable<HighlightEn
5959
return result;
6060
}
6161

62+
/// <summary>
63+
/// Returns whether any hide-line entry matches the line. Other matching entries cannot cancel the decision,
64+
/// whatever their order.
65+
/// </summary>
66+
public static bool IsHidden (IEnumerable<HighlightEntry> entries, ITextValueMemory line)
67+
{
68+
ArgumentNullException.ThrowIfNull(entries);
69+
ArgumentNullException.ThrowIfNull(line);
70+
71+
return entries.Any(e => IsHideRule(e) && IsMatch(e, line));
72+
}
73+
74+
/// <summary>Returns whether the entry hides the lines it matches; temporary search-hit entries never do.</summary>
75+
public static bool IsHideRule (HighlightEntry entry)
76+
{
77+
ArgumentNullException.ThrowIfNull(entry);
78+
return entry.IsHideLine && !entry.IsSearchHit;
79+
}
80+
6281
/// <summary>
6382
/// Classifies the non-plugin trigger actions implied by a set of matching entries: whether to
6483
/// suppress the dirty LED, stop tailing, set a bookmark, and the concatenated bookmark comment.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
namespace LogExpert.Core.Classes.Highlight;
2+
3+
/// <summary>
4+
/// Immutable mapping between the rows of a Log Window's main grid and the original logical lines of its
5+
/// Logfile Reader, after highlight-based hiding. Memory is proportional to the number of hidden lines.
6+
/// <para>
7+
/// <see cref="Append"/> shares storage with the map it extends, so appending tail lines is cheap. The storage is
8+
/// append-only: a map only ever reads the prefix it was created with, so an older map stays valid.
9+
/// </para>
10+
/// </summary>
11+
public sealed class LineVisibilityMap
12+
{
13+
private readonly HiddenLineStore _store;
14+
15+
private LineVisibilityMap (HiddenLineStore store, int lineCount, int hiddenCount)
16+
{
17+
_store = store;
18+
LineCount = lineCount;
19+
HiddenCount = hiddenCount;
20+
}
21+
22+
/// <summary>A new map covering no lines. Not a shared instance, so unrelated appends never share storage.</summary>
23+
public static LineVisibilityMap Empty => Identity(0);
24+
25+
/// <summary>Total number of original logical lines covered by the map.</summary>
26+
public int LineCount { get; }
27+
28+
public int HiddenCount { get; }
29+
30+
public int VisibleCount => LineCount - HiddenCount;
31+
32+
/// <summary>A map in which every line is visible.</summary>
33+
public static LineVisibilityMap Identity (int lineCount)
34+
{
35+
ArgumentOutOfRangeException.ThrowIfNegative(lineCount);
36+
return new LineVisibilityMap(new HiddenLineStore([], 0, lineCount), lineCount, 0);
37+
}
38+
39+
/// <summary>Returns the original line displayed in <paramref name="row"/>, or -1 when there is no such row.</summary>
40+
public int RowToLine (int row)
41+
{
42+
if (row < 0 || row >= VisibleCount)
43+
{
44+
return -1;
45+
}
46+
47+
if (HiddenCount == 0)
48+
{
49+
return row;
50+
}
51+
52+
// hidden[i] - i is non-decreasing: count the hidden lines that lie before the row's line.
53+
var items = _store.Items;
54+
int low = 0, high = HiddenCount;
55+
while (low < high)
56+
{
57+
var mid = (low + high) >>> 1;
58+
if (items[mid] - mid <= row)
59+
{
60+
low = mid + 1;
61+
}
62+
else
63+
{
64+
high = mid;
65+
}
66+
}
67+
68+
return row + low;
69+
}
70+
71+
/// <summary>Returns the row displaying <paramref name="line"/>, or -1 when the line is hidden or out of range.</summary>
72+
public int LineToRow (int line)
73+
{
74+
if (line < 0 || line >= LineCount)
75+
{
76+
return -1;
77+
}
78+
79+
var index = Array.BinarySearch(_store.Items, 0, HiddenCount, line);
80+
return index >= 0 ? -1 : line - ~index;
81+
}
82+
83+
public bool IsHidden (int line)
84+
{
85+
return line >= 0 && line < LineCount && Array.BinarySearch(_store.Items, 0, HiddenCount, line) >= 0;
86+
}
87+
88+
/// <summary>
89+
/// Returns the row of <paramref name="line"/> when it is visible; otherwise the row of the next visible line,
90+
/// falling back to the previous one. Returns -1 when no line is visible.
91+
/// </summary>
92+
public int NearestRow (int line)
93+
{
94+
if (VisibleCount == 0)
95+
{
96+
return -1;
97+
}
98+
99+
if (line >= LineCount)
100+
{
101+
return VisibleCount - 1;
102+
}
103+
104+
line = Math.Max(line, 0);
105+
var index = Array.BinarySearch(_store.Items, 0, HiddenCount, line);
106+
var hiddenBefore = index >= 0 ? index : ~index;
107+
108+
// For a hidden line, line - hiddenBefore is the row of the next visible line, if there is one.
109+
return Math.Min(line - hiddenBefore, VisibleCount - 1);
110+
}
111+
112+
/// <summary>
113+
/// Returns a map covering <paramref name="lineCount"/> lines, in which <paramref name="hiddenLines"/>
114+
/// (ascending, all within the appended range) are hidden in addition to this map's hidden lines.
115+
/// </summary>
116+
public LineVisibilityMap Append (int lineCount, IReadOnlyList<int> hiddenLines)
117+
{
118+
ArgumentNullException.ThrowIfNull(hiddenLines);
119+
ArgumentOutOfRangeException.ThrowIfLessThan(lineCount, LineCount);
120+
121+
var previous = LineCount - 1;
122+
// Hidden lines must be ascending and within the appended range.
123+
foreach (var line in hiddenLines)
124+
{
125+
if (line <= previous || line >= lineCount)
126+
{
127+
throw new ArgumentOutOfRangeException(nameof(hiddenLines), line, null);
128+
}
129+
130+
previous = line;
131+
}
132+
133+
var store = _store.TryAppend(LineCount, HiddenCount, lineCount, hiddenLines)
134+
?? HiddenLineStore.CopyAppend(_store.Items, HiddenCount, lineCount, hiddenLines);
135+
136+
return new LineVisibilityMap(store, lineCount, HiddenCount + hiddenLines.Count);
137+
}
138+
139+
/// <summary>Drops the first <paramref name="offset"/> lines (rollover) and renumbers the rest.</summary>
140+
public LineVisibilityMap Shift (int offset)
141+
{
142+
ArgumentOutOfRangeException.ThrowIfNegative(offset);
143+
144+
var items = _store.Items;
145+
var first = Array.BinarySearch(items, 0, HiddenCount, offset);
146+
first = first >= 0 ? first : ~first;
147+
148+
var shifted = new int[HiddenCount - first];
149+
for (var i = 0; i < shifted.Length; i++)
150+
{
151+
shifted[i] = items[first + i] - offset;
152+
}
153+
154+
var lineCount = Math.Max(0, LineCount - offset);
155+
return new LineVisibilityMap(new HiddenLineStore(shifted, shifted.Length, lineCount), lineCount, shifted.Length);
156+
}
157+
158+
/// <summary>
159+
/// True when this map extends <paramref name="other"/> without changing any of its lines, so rows that
160+
/// <paramref name="other"/> displays keep their row index.
161+
/// </summary>
162+
public bool IsAppendOf (LineVisibilityMap other)
163+
{
164+
ArgumentNullException.ThrowIfNull(other);
165+
return ReferenceEquals(_store, other._store) && other.LineCount <= LineCount && other.HiddenCount <= HiddenCount;
166+
}
167+
168+
/// <summary>
169+
/// Append-only storage of hidden line numbers shared by a chain of appended maps. It is only extended in place
170+
/// by the map at its committed end; any other append copies, so no map ever observes a changed prefix.
171+
/// </summary>
172+
private sealed class HiddenLineStore (int[] items, int committedHidden, int committedLines)
173+
{
174+
private readonly Lock _lock = new();
175+
private int _committedHidden = committedHidden;
176+
private int _committedLines = committedLines;
177+
private volatile int[] _items = items;
178+
179+
public int[] Items => _items;
180+
181+
public HiddenLineStore? TryAppend (int fromLines, int fromHidden, int lineCount, IReadOnlyList<int> hiddenLines)
182+
{
183+
lock (_lock)
184+
{
185+
if (_committedLines != fromLines || _committedHidden != fromHidden)
186+
{
187+
return null;
188+
}
189+
190+
var required = fromHidden + hiddenLines.Count;
191+
var target = _items;
192+
if (required > target.Length)
193+
{
194+
target = new int[Math.Max(required, Math.Max(16, target.Length * 2))];
195+
Array.Copy(_items, target, fromHidden);
196+
}
197+
198+
for (var i = 0; i < hiddenLines.Count; i++)
199+
{
200+
target[fromHidden + i] = hiddenLines[i];
201+
}
202+
203+
_items = target;
204+
_committedHidden = required;
205+
_committedLines = lineCount;
206+
return this;
207+
}
208+
}
209+
210+
public static HiddenLineStore CopyAppend (int[] items, int hiddenCount, int lineCount, IReadOnlyList<int> hiddenLines)
211+
{
212+
var copy = new int[hiddenCount + hiddenLines.Count];
213+
Array.Copy(items, copy, hiddenCount);
214+
for (var i = 0; i < hiddenLines.Count; i++)
215+
{
216+
copy[hiddenCount + i] = hiddenLines[i];
217+
}
218+
219+
return new HiddenLineStore(copy, copy.Length, lineCount);
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)