From 758c809ffefc5137608511942aeb2c39f54bfd8f Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:12:52 +0200 Subject: [PATCH 01/13] Extract Filter Spread into pure Core module (ticket 1) Move the back/fore spread expansion and duplicate-suppression history trimming out of the multi-threaded filter into FilterSpread, a pure static module in LogExpert.Core, pinned by table-driven NUnit tests. Two deliberate behavior changes, both test-pinned: - Line 0 is now a valid back-spread context line (was excluded by an off-by-one boundary check). - The history window is unified at 2x99 (the UI knob's maximum spread); the Core filter previously used 2x50, so parallel filtering with spread > 50 could emit duplicate context lines. The Log Window's serial filter, Filter Pipe processing and rollover shifting migrate in follow-up tickets (see tickets.md). --- src/LogExpert.Core/Classes/Filter/Filter.cs | 58 +------------- .../Classes/Filter/FilterSpread.cs | 75 +++++++++++++++++++ .../Filter/FilterSpreadTests.cs | 62 +++++++++++++++ tickets.md | 62 +++++++++++++++ 4 files changed, 201 insertions(+), 56 deletions(-) create mode 100644 src/LogExpert.Core/Classes/Filter/FilterSpread.cs create mode 100644 src/LogExpert.Tests/Filter/FilterSpreadTests.cs create mode 100644 tickets.md diff --git a/src/LogExpert.Core/Classes/Filter/Filter.cs b/src/LogExpert.Core/Classes/Filter/Filter.cs index 65720a04..f2b611bf 100644 --- a/src/LogExpert.Core/Classes/Filter/Filter.cs +++ b/src/LogExpert.Core/Classes/Filter/Filter.cs @@ -11,7 +11,6 @@ internal class Filter #region Fields private const int PROGRESS_BAR_MODULO = 1000; - private const int SPREAD_MAX = 50; private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly ColumnizerCallback _callback; @@ -108,66 +107,13 @@ private int DoFilter (FilterParams filterParams, int startLine, int maxCount, Li private void AddFilterLine (int lineNum, FilterParams filterParams, List filterResultLines, List lastFilterLinesList, List filterHitList) { filterHitList.Add(lineNum); - var filterResult = GetAdditionalFilterResults(filterParams, lineNum, lastFilterLinesList); + var filterResult = FilterSpread.Expand(lineNum, filterParams.SpreadBefore, filterParams.SpreadBehind, _callback.GetLineCount(), lastFilterLinesList); filterResultLines.AddRange(filterResult); lastFilterLinesList.AddRange(filterResult); - if (lastFilterLinesList.Count > SPREAD_MAX * 2) - { - lastFilterLinesList.RemoveRange(0, lastFilterLinesList.Count - SPREAD_MAX * 2); - } - } - - /// - /// Returns a list with 'additional filter results'. This is the given line number - /// and (if back spread and/or fore spread is enabled) some additional lines. - /// This function doesn't check the filter condition! - /// - /// - /// - /// - /// - private IList GetAdditionalFilterResults (FilterParams filterParams, int lineNum, IList checkList) - { - IList resultList = []; - - if (filterParams.SpreadBefore == 0 && filterParams.SpreadBehind == 0) - { - resultList.Add(lineNum); - return resultList; - } - - // back spread - for (var i = filterParams.SpreadBefore; i > 0; --i) - { - if (lineNum - i > 0) - { - if (!resultList.Contains(lineNum - i) && !checkList.Contains(lineNum - i)) - { - resultList.Add(lineNum - i); - } - } - } - // direct filter hit - if (!resultList.Contains(lineNum) && !checkList.Contains(lineNum)) - { - resultList.Add(lineNum); - } - // after spread - for (var i = 1; i <= filterParams.SpreadBehind; ++i) - { - if (lineNum + i < _callback.GetLineCount()) - { - if (!resultList.Contains(lineNum + i) && !checkList.Contains(lineNum + i)) - { - resultList.Add(lineNum + i); - } - } - } - - return resultList; + FilterSpread.TrimHistory(lastFilterLinesList); } #endregion diff --git a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs new file mode 100644 index 00000000..4739cb1f --- /dev/null +++ b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs @@ -0,0 +1,75 @@ +namespace LogExpert.Core.Classes.Filter; + +/// +/// Owner of the Filter Spread rules: expanding a filter hit into its back/fore spread +/// context lines and trimming the duplicate-suppression history. +/// Pure — no dependency on controls, callbacks, readers or locks. +/// +public static class FilterSpread +{ + /// + /// Maximum configurable spread. The duplicate-suppression history window derives from + /// this value so it always covers the largest configurable spread. + /// + public const int SPREAD_MAX = 99; + + /// + /// Trims the duplicate-suppression history to the last 2 × entries. + /// + public static void TrimHistory (List history) + { + ArgumentNullException.ThrowIfNull(history); + + if (history.Count > SPREAD_MAX * 2) + { + history.RemoveRange(0, history.Count - (SPREAD_MAX * 2)); + } + } + + /// + /// Returns the lines a filter hit contributes to the filter result: the hit itself, + /// plus (if configured) back-spread and fore-spread context lines. Lines already present + /// in are suppressed. This function does not evaluate + /// the filter condition. + /// + public static IList Expand (int lineNum, int spreadBefore, int spreadBehind, int lineCount, IList alreadyTaken) + { + ArgumentNullException.ThrowIfNull(alreadyTaken); + + IList resultList = []; + + if (spreadBefore == 0 && spreadBehind == 0) + { + resultList.Add(lineNum); + return resultList; + } + + // back spread (ascending, clamped at line 0 — line 0 is a valid context line) + for (var i = spreadBefore; i > 0; --i) + { + var line = lineNum - i; + if (line >= 0 && !resultList.Contains(line) && !alreadyTaken.Contains(line)) + { + resultList.Add(line); + } + } + + // the hit itself + if (!resultList.Contains(lineNum) && !alreadyTaken.Contains(lineNum)) + { + resultList.Add(lineNum); + } + + // fore spread (ascending, clamped at the last line) + for (var i = 1; i <= spreadBehind; ++i) + { + var line = lineNum + i; + if (line < lineCount && !resultList.Contains(line) && !alreadyTaken.Contains(line)) + { + resultList.Add(line); + } + } + + return resultList; + } +} diff --git a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs new file mode 100644 index 00000000..4ab18ce0 --- /dev/null +++ b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs @@ -0,0 +1,62 @@ +using LogExpert.Core.Classes.Filter; + +using NUnit.Framework; + +namespace LogExpert.Tests.Filter; + +[TestFixture] +public class FilterSpreadTests +{ + // Row format: hit line, spread before, spread behind, line count, already-taken history, expected lines. + // Expected values are worked examples from the spec (docs/specs/filter-spread-extraction.md). + [TestCase(5, 0, 0, 100, new int[0], new[] { 5 }, + TestName = "Expand_NoSpreadConfigured_ReturnsOnlyTheHitLine")] + [TestCase(5, 0, 0, 100, new[] { 5 }, new[] { 5 }, + TestName = "Expand_NoSpreadConfigured_IgnoresHistory_HistoricalQuirkPreserved")] + [TestCase(0, 3, 2, 100, new int[0], new[] { 0, 1, 2 }, + TestName = "Expand_HitAtLineZero_EmitsNoNegativeLines")] + [TestCase(3, 5, 0, 100, new int[0], new[] { 0, 1, 2, 3 }, + TestName = "Expand_BackSpreadReachingPastTopOfFile_ClampsAtLineZeroAndIncludesIt")] + [TestCase(97, 0, 5, 100, new int[0], new[] { 97, 98, 99 }, + TestName = "Expand_ForeSpreadReachingPastEndOfFile_ClampsAtLastLine")] + [TestCase(99, 0, 3, 100, new int[0], new[] { 99 }, + TestName = "Expand_HitAtLastLineWithForeSpread_ReturnsOnlyTheHit")] + [TestCase(10, 1, 3, 100, new int[0], new[] { 9, 10, 11, 12, 13 }, + TestName = "Expand_AsymmetricSpread_ReturnsBackLinesHitThenForeLines")] + [TestCase(12, 2, 2, 100, new[] { 8, 9, 10, 11, 12 }, new[] { 13, 14 }, + TestName = "Expand_OverlappingWithEarlierHit_SuppressesLinesAlreadyTaken")] + [TestCase(5, 1, 1, 100, new[] { 5 }, new[] { 4, 6 }, + TestName = "Expand_HitItselfAlreadyTaken_IsNotEmittedAgain")] + public void Expand_Table (int lineNum, int spreadBefore, int spreadBehind, int lineCount, int[] alreadyTaken, int[] expected) + { + var result = FilterSpread.Expand(lineNum, spreadBefore, spreadBehind, lineCount, alreadyTaken); + + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void TrimHistory_LongerThanWindow_DropsOldestEntriesDownToWindowSize () + { + // Window is 2 × 99 (the maximum configurable spread) — unifies the divergent 50/99 constants. + var history = new List(Enumerable.Range(0, 250)); + + FilterSpread.TrimHistory(history); + + Assert.Multiple(() => + { + Assert.That(history, Has.Count.EqualTo(198)); + Assert.That(history[0], Is.EqualTo(52)); + Assert.That(history[^1], Is.EqualTo(249)); + }); + } + + [Test] + public void TrimHistory_WithinWindow_IsUnchanged () + { + var history = new List(Enumerable.Range(0, 198)); + + FilterSpread.TrimHistory(history); + + Assert.That(history, Has.Count.EqualTo(198)); + } +} diff --git a/tickets.md b/tickets.md new file mode 100644 index 00000000..dc3a45f0 --- /dev/null +++ b/tickets.md @@ -0,0 +1,62 @@ +# Tickets: Filter Spread extraction + +Extract the Filter Spread rules (back/fore spread context expansion, duplicate-suppression history, rollover shifting) out of the Log Window into one pure, tested Core module, per `docs/specs/filter-spread-extraction.md`. + +Work the **frontier**: any ticket whose blockers are all done. After the first ticket, tickets 2–4 can proceed in any order (or in parallel); the final ticket waits for all three. + +## Filter Spread module in Core with table tests; multi-threaded filter delegates + +**What to build:** A pure Filter Spread module in Core that owns hit expansion (back-spread lines, the hit, fore-spread lines — deduplicated against an already-emitted history, clamped to the file's line range) and the history-trim rule (window unified at 99, the UI knob's maximum spread). The multi-threaded filter delegates its per-hit accumulation to the module and its private spread copy is deleted. This is the tracer bullet: multi-threaded filtering immediately gains the line-0 fix and stops emitting duplicate context lines at spreads over 50. + +**Blocked by:** None — can start immediately. + +- [ ] Module is pure: no dependency on controls, callbacks, readers, or locks; line count is passed in as a value +- [ ] Expansion output order matches today's: ascending back-spread, hit, ascending fore-spread +- [ ] Line 0 is a valid back-spread line (behavior change, pinned by an explicit test) +- [ ] History window constant is 99 and lives only in the module +- [ ] Table-driven NUnit edge-case tests pass: hit at line 0, hit within back-spread of line 0, hit near/at end-of-file, spread 0/0, asymmetric spreads, overlapping hits, hit already in history, trim at the 2×99 boundary +- [ ] Multi-threaded filter's private spread arithmetic and trim snippet are deleted; it delegates to the module +- [ ] Full solution builds and the existing test suite stays green + +## Log Window serial filter delegates to the module + +**What to build:** Single-threaded filtering in the Log Window produces module-owned spread: its per-hit accumulation delegates expansion and history trimming to the Filter Spread module, keeping its own locking and progress/GUI concerns at the call site. The line-0 fix and the 99 window go live for serial filtering, making it consistent with the parallel path. + +**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. + +- [ ] Serial per-hit accumulation calls the module for expansion and trimming; no spread arithmetic remains inline in that path +- [ ] Equivalence-guard test: an identical hit sequence fed the way the serial and parallel call sites do produces identical result/hit/history lists +- [ ] Filtering a file with spread configured behaves identically whether the single- or multi-threaded filter runs +- [ ] Full solution builds and the test suite stays green + +## Filter Pipe processing delegates to the module + +**What to build:** Lines written to a Filter Pipe tab receive the same module-owned spread expansion and history trimming as the filter result view, so derived tabs stay consistent with their source. The pipe path's inline trim (which today removes one entry at a time against the divergent constant) is replaced by the module's trim rule. + +**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. + +- [ ] Filter Pipe per-hit processing calls the module for expansion and history trimming +- [ ] A pipe tab fed by a filter with spread configured shows the same context lines as the filter view for the same hits +- [ ] Full solution builds and the test suite stays green + +## Rollover shifting moves into the module + +**What to build:** After a multi-file rollover, previously collected filter results still point at the right content: the module gains the shift operation (shift result and hit line numbers by the rollover offset, drop lines that rolled off the start, rebuild the duplicate-suppression history from the tail of the shifted results) with its own test rows. The Log Window's rollover handler delegates, keeping only its lock and GUI refresh. + +**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. + +- [ ] Shift operation is pure and lives in the module alongside expansion and trim +- [ ] Tests cover: plain shift, lines dropping out when the offset exceeds them, history rebuilt from the result tail, results shorter than the history window +- [ ] Log Window rollover handling delegates; no shift arithmetic remains inline +- [ ] Full solution builds and the test suite stays green + +## Contract: delete Log Window spread remnants, pin the knob, add glossary entry + +**What to build:** The Log Window carries no spread logic or constants of its own. Its private spread-expansion copy and local spread constant are deleted; the UI spread knobs' maximum references the module's constant so the knob range and the suppression window can never drift apart again; CONTEXT.md gains a canonical **Filter Spread** glossary entry. + +**Blocked by:** Log Window serial filter delegates to the module; Filter Pipe processing delegates to the module; Rollover shifting moves into the module. + +- [ ] No spread expansion, trim, or shift arithmetic remains anywhere in the Log Window +- [ ] The spread knobs' maximum value references the module's constant (99) +- [ ] CONTEXT.md defines Filter Spread (back spread / fore spread context expansion) in the project's glossary voice +- [ ] Full solution builds and the test suite stays green From 7648452a6b6661ce17addb97ebd18f7c78b236ba Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:22:44 +0200 Subject: [PATCH 02/13] Delegate serial filter spread to FilterSpread module (ticket 2) The Log Window's single-threaded filter path now accumulates per-hit results through FilterSpread.Expand/TrimHistory instead of its inline copy of the spread arithmetic. Locking, hit recording and the count capture stay at the call site. Behavior change on the serial path (sanctioned by the spec, pinned by ticket 1's tests): line 0 is now a valid back-spread context line. The history window is unchanged (was already 2x99 here). Adds an equivalence-guard test pinning the shared accumulation recipe (hit -> Expand -> append -> TrimHistory) with a worked example, so the serial and parallel call sites are held to identical output. The private GetAdditionalFilterResults copy remains for the Filter Pipe path (ticket 3); it and SPREAD_MAX are deleted in ticket 5. --- .../Filter/FilterSpreadTests.cs | 34 +++++++++++++++++++ .../Controls/LogWindow/LogWindow.cs | 7 ++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs index 4ab18ce0..05ca77b4 100644 --- a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs +++ b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs @@ -59,4 +59,38 @@ public void TrimHistory_WithinWindow_IsUnchanged () Assert.That(history, Has.Count.EqualTo(198)); } + + /// + /// Equivalence guard: the single-threaded Log Window filter and the multi-threaded + /// Core filter accumulate per-hit results through the same recipe + /// (hit → Expand → append to results/history → TrimHistory). This pins that shared + /// recipe's output for a hit sequence with overlapping spreads; a call site that + /// deviates from the recipe no longer matches this specification. Expected values are + /// a worked example: hits 5, 7 and 50 with spread 2/2 in a 100-line file. + /// + [Test] + public void AccumulationRecipe_OverlappingHitSequence_ProducesPinnedResultHitAndHistoryLists () + { + int[] hits = [5, 7, 50]; + List resultLines = []; + List history = []; + List hitList = []; + + foreach (var hit in hits) + { + hitList.Add(hit); + var expanded = FilterSpread.Expand(hit, spreadBefore: 2, spreadBehind: 2, lineCount: 100, alreadyTaken: history); + resultLines.AddRange(expanded); + history.AddRange(expanded); + FilterSpread.TrimHistory(history); + } + + Assert.Multiple(() => + { + // Hit 5 contributes 3..7; hit 7 contributes only 8,9 (5..7 already taken); hit 50 contributes 48..52. + Assert.That(resultLines, Is.EqualTo(new[] { 3, 4, 5, 6, 7, 8, 9, 48, 49, 50, 51, 52 })); + Assert.That(hitList, Is.EqualTo(new[] { 5, 7, 50 })); + Assert.That(history, Is.EqualTo(resultLines), "history below the window size mirrors the result lines"); + }); + } } diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index bead496e..ac680c37 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -4685,14 +4685,11 @@ private void AddFilterLine (int lineNum, bool immediate, FilterParams filterPara lock (_filterResultList) { filterHitList.Add(lineNum); - var filterResult = GetAdditionalFilterResults(filterParams, lineNum, lastFilterLinesList); + var filterResult = FilterSpread.Expand(lineNum, filterParams.SpreadBefore, filterParams.SpreadBehind, _logFileReader.LineCount, lastFilterLinesList); filterResultLines.AddRange(filterResult); count = filterResultLines.Count; lastFilterLinesList.AddRange(filterResult); - if (lastFilterLinesList.Count > SPREAD_MAX * 2) - { - lastFilterLinesList.RemoveRange(0, lastFilterLinesList.Count - SPREAD_MAX * 2); - } + FilterSpread.TrimHistory(lastFilterLinesList); } if (immediate) From 9a18d1b247c21462a77b211a9c061926b333816d Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:23:02 +0200 Subject: [PATCH 03/13] Mark tickets 1 and 2 acceptance criteria done --- tickets.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tickets.md b/tickets.md index dc3a45f0..cbced952 100644 --- a/tickets.md +++ b/tickets.md @@ -10,13 +10,13 @@ Work the **frontier**: any ticket whose blockers are all done. After the first t **Blocked by:** None — can start immediately. -- [ ] Module is pure: no dependency on controls, callbacks, readers, or locks; line count is passed in as a value -- [ ] Expansion output order matches today's: ascending back-spread, hit, ascending fore-spread -- [ ] Line 0 is a valid back-spread line (behavior change, pinned by an explicit test) -- [ ] History window constant is 99 and lives only in the module -- [ ] Table-driven NUnit edge-case tests pass: hit at line 0, hit within back-spread of line 0, hit near/at end-of-file, spread 0/0, asymmetric spreads, overlapping hits, hit already in history, trim at the 2×99 boundary -- [ ] Multi-threaded filter's private spread arithmetic and trim snippet are deleted; it delegates to the module -- [ ] Full solution builds and the existing test suite stays green +- [x] Module is pure: no dependency on controls, callbacks, readers, or locks; line count is passed in as a value +- [x] Expansion output order matches today's: ascending back-spread, hit, ascending fore-spread +- [x] Line 0 is a valid back-spread line (behavior change, pinned by an explicit test) +- [x] History window constant is 99 and lives only in the module +- [x] Table-driven NUnit edge-case tests pass: hit at line 0, hit within back-spread of line 0, hit near/at end-of-file, spread 0/0, asymmetric spreads, overlapping hits, hit already in history, trim at the 2×99 boundary +- [x] Multi-threaded filter's private spread arithmetic and trim snippet are deleted; it delegates to the module +- [x] Full solution builds and the existing test suite stays green ## Log Window serial filter delegates to the module @@ -24,10 +24,10 @@ Work the **frontier**: any ticket whose blockers are all done. After the first t **Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. -- [ ] Serial per-hit accumulation calls the module for expansion and trimming; no spread arithmetic remains inline in that path -- [ ] Equivalence-guard test: an identical hit sequence fed the way the serial and parallel call sites do produces identical result/hit/history lists -- [ ] Filtering a file with spread configured behaves identically whether the single- or multi-threaded filter runs -- [ ] Full solution builds and the test suite stays green +- [x] Serial per-hit accumulation calls the module for expansion and trimming; no spread arithmetic remains inline in that path +- [x] Equivalence-guard test: an identical hit sequence fed the way the serial and parallel call sites do produces identical result/hit/history lists +- [x] Filtering a file with spread configured behaves identically whether the single- or multi-threaded filter runs +- [x] Full solution builds and the test suite stays green ## Filter Pipe processing delegates to the module From 45433d52917dfb4f505a4247a71f931b2b827990 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:30:39 +0200 Subject: [PATCH 04/13] Delegate Filter Pipe spread to FilterSpread module (ticket 3) ProcessFilterPipes now expands hits and trims the pipe history through FilterSpread instead of the Log Window's private copy. Pipe sequencing (OpenFile / per-line WriteToPipe / CloseFile) is unchanged; the per-line history trim is observably equivalent (history is only mutated here, so it never exceeds the window by more than one entry per add). Behavior change on the pipe path (sanctioned by the spec, pinned by the module tests): line 0 is now a valid back-spread context line, so pipe tabs receive the same context as the filter view. The trim window is unchanged (this path already used 2x99). TrimHistory widens from List to IList because the pipe history is an IList; trimming now removes from the front in a loop, pinned by a Collection-backed test. Pulled forward from ticket 5: the private GetAdditionalFilterResults copy in LogWindow is deleted - both its callers are now migrated, so it was dead code. SPREAD_MAX stays (knobs + ShiftFilterLines, tickets 4-5). --- .../Classes/Filter/FilterSpread.cs | 6 +- .../Filter/FilterSpreadTests.cs | 21 +++++++ .../Controls/LogWindow/LogWindow.cs | 61 +------------------ 3 files changed, 26 insertions(+), 62 deletions(-) diff --git a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs index 4739cb1f..a8dc88e0 100644 --- a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs +++ b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs @@ -16,13 +16,13 @@ public static class FilterSpread /// /// Trims the duplicate-suppression history to the last 2 × entries. /// - public static void TrimHistory (List history) + public static void TrimHistory (IList history) { ArgumentNullException.ThrowIfNull(history); - if (history.Count > SPREAD_MAX * 2) + while (history.Count > SPREAD_MAX * 2) { - history.RemoveRange(0, history.Count - (SPREAD_MAX * 2)); + history.RemoveAt(0); } } diff --git a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs index 05ca77b4..1802463c 100644 --- a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs +++ b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; + using LogExpert.Core.Classes.Filter; using NUnit.Framework; @@ -60,6 +62,25 @@ public void TrimHistory_WithinWindow_IsUnchanged () Assert.That(history, Has.Count.EqualTo(198)); } + [Test] + public void TrimHistory_WorksOnAnyIListImplementation () + { + // The Filter Pipe history is an IList, not a List — the trim rule must not care. + IList history = new Collection(); + foreach (var i in Enumerable.Range(0, 200)) + { + history.Add(i); + } + + FilterSpread.TrimHistory(history); + + Assert.Multiple(() => + { + Assert.That(history, Has.Count.EqualTo(198)); + Assert.That(history[0], Is.EqualTo(2)); + }); + } + /// /// Equivalence guard: the single-threaded Log Window filter and the multi-threaded /// Core filter accumulate per-hit results through the same recipe diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index ac680c37..34a88c47 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -4624,60 +4624,6 @@ private void Filter (FilterParams filterParams, List filterResultLines, Lis StatusLineText(string.Format(CultureInfo.InvariantCulture, Resources.LogWindow_UI_StatusLineText_Filter_FilterDurationMs, endTime - startTime)); } - /// - /// Returns a list with 'additional filter results'. This is the given line number and (if back spread and/or fore - /// spread is enabled) some additional lines. This function doesn't check the filter condition! - /// - /// - /// - /// - /// - private IList GetAdditionalFilterResults (FilterParams filterParams, int lineNum, IList checkList) - { - IList resultList = []; - //string textLine = this.logFileReader.GetLogLine(lineNum); - //ColumnizerCallback callback = new ColumnizerCallback(this); - //callback.LineNum = lineNum; - - if (filterParams.SpreadBefore == 0 && filterParams.SpreadBehind == 0) - { - resultList.Add(lineNum); - return resultList; - } - - // back spread - for (var i = filterParams.SpreadBefore; i > 0; --i) - { - if (lineNum - i > 0) - { - if (!resultList.Contains(lineNum - i) && !checkList.Contains(lineNum - i)) - { - resultList.Add(lineNum - i); - } - } - } - - // direct filter hit - if (!resultList.Contains(lineNum) && !checkList.Contains(lineNum)) - { - resultList.Add(lineNum); - } - - // after spread - for (var i = 1; i <= filterParams.SpreadBehind; ++i) - { - if (lineNum + i < _logFileReader.LineCount) - { - if (!resultList.Contains(lineNum + i) && !checkList.Contains(lineNum + i)) - { - resultList.Add(lineNum + i); - } - } - } - - return resultList; - } - [SupportedOSPlatform("windows")] private void AddFilterLine (int lineNum, bool immediate, FilterParams filterParams, List filterResultLines, List lastFilterLinesList, List filterHitList) { @@ -5317,16 +5263,13 @@ private void ProcessFilterPipes (int lineNum) //long startTime = Environment.TickCount; if (Util.TestFilterCondition(pipe.FilterParams, searchLine, callback)) { - var filterResult = GetAdditionalFilterResults(pipe.FilterParams, lineNum, pipe.LastLinesHistoryList); + var filterResult = FilterSpread.Expand(lineNum, pipe.FilterParams.SpreadBefore, pipe.FilterParams.SpreadBehind, _logFileReader.LineCount, pipe.LastLinesHistoryList); pipe.OpenFile(); foreach (var line in filterResult) { pipe.LastLinesHistoryList.Add(line); - if (pipe.LastLinesHistoryList.Count > SPREAD_MAX * 2) - { - pipe.LastLinesHistoryList.RemoveAt(0); - } + FilterSpread.TrimHistory(pipe.LastLinesHistoryList); var textLine = _logFileReader.GetLogLineMemory(line); var fileOk = pipe.WriteToPipe(textLine, line); From c2c620abc7e06fe753316b90b1db31550e0b9fd2 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:30:39 +0200 Subject: [PATCH 05/13] Mark ticket 3 acceptance criteria done --- tickets.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tickets.md b/tickets.md index cbced952..dbaa40c6 100644 --- a/tickets.md +++ b/tickets.md @@ -35,9 +35,9 @@ Work the **frontier**: any ticket whose blockers are all done. After the first t **Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. -- [ ] Filter Pipe per-hit processing calls the module for expansion and history trimming -- [ ] A pipe tab fed by a filter with spread configured shows the same context lines as the filter view for the same hits -- [ ] Full solution builds and the test suite stays green +- [x] Filter Pipe per-hit processing calls the module for expansion and history trimming +- [x] A pipe tab fed by a filter with spread configured shows the same context lines as the filter view for the same hits +- [x] Full solution builds and the test suite stays green ## Rollover shifting moves into the module From 59993085e7c4b8c883ddfb0f75747d2dd9c8d24a Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:37:01 +0200 Subject: [PATCH 06/13] Move rollover shifting into FilterSpread module (ticket 4) ShiftFilterLines now delegates to two new pure module operations: - ShiftLines: subtract the rollover offset, drop lines that rolled off the start of the virtual file. - RebuildHistory: rebuild the duplicate-suppression history from the tail of the shifted results, capped at SPREAD_MAX (99, not the 2x99 trim window - preserving the original rollover behavior). The call site keeps its lock scope (result-list shift inside the lock, hit-list shift and history rebuild outside, exactly as before) and the GUI refresh. Both operations are TDD-pinned: plain shift, rolled-off lines dropping out, tail rebuild at the 99 cap, shorter-than-window results, and empty input. Note: four pre-existing commented-out lines inside the rewritten method body were removed along with the rewrite (old grid-refresh experiments). FilterPipe.ShiftLineNums is deliberately not migrated - it has different semantics (-1 placeholders preserve index mapping). --- .../Classes/Filter/FilterSpread.cs | 35 +++++++++++++ .../Filter/FilterSpreadTests.cs | 50 +++++++++++++++++++ .../Controls/LogWindow/LogWindow.cs | 37 ++------------ 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs index a8dc88e0..acf7113b 100644 --- a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs +++ b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs @@ -26,6 +26,41 @@ public static void TrimHistory (IList history) } } + /// + /// Shifts collected line numbers by a multi-file rollover offset. Lines that roll off + /// the start of the virtual file are dropped. + /// + public static List ShiftLines (IEnumerable lines, int offset) + { + ArgumentNullException.ThrowIfNull(lines); + + List shifted = []; + foreach (var lineNum in lines) + { + var line = lineNum - offset; + if (line >= 0) + { + shifted.Add(line); + } + } + + return shifted; + } + + /// + /// Rebuilds the duplicate-suppression history after a rollover shift: the last + /// entries of the shifted result lines (all of them when + /// there are fewer). Capping at SPREAD_MAX — not the 2× trim window — preserves the + /// original rollover behavior. + /// + public static List RebuildHistory (List shiftedResultLines) + { + ArgumentNullException.ThrowIfNull(shiftedResultLines); + + var count = Math.Min(SPREAD_MAX, shiftedResultLines.Count); + return shiftedResultLines.GetRange(shiftedResultLines.Count - count, count); + } + /// /// Returns the lines a filter hit contributes to the filter result: the hit itself, /// plus (if configured) back-spread and fore-spread context lines. Lines already present diff --git a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs index 1802463c..b2bee626 100644 --- a/src/LogExpert.Tests/Filter/FilterSpreadTests.cs +++ b/src/LogExpert.Tests/Filter/FilterSpreadTests.cs @@ -81,6 +81,56 @@ public void TrimHistory_WorksOnAnyIListImplementation () }); } + [Test] + public void ShiftLines_OnRollover_SubtractsOffsetFromEveryLine () + { + var shifted = FilterSpread.ShiftLines([10, 20, 30], offset: 5); + + Assert.That(shifted, Is.EqualTo(new[] { 5, 15, 25 })); + } + + [Test] + public void ShiftLines_LinesRolledOffTheStart_AreDropped () + { + // Offset 15 rolls lines 10 and 14 off the virtual file; 15 shifts to 0 and survives. + var shifted = FilterSpread.ShiftLines([10, 14, 15, 40], offset: 15); + + Assert.That(shifted, Is.EqualTo(new[] { 0, 25 })); + } + + [Test] + public void RebuildHistory_ResultsLongerThanSpreadMax_TakesTheLastSpreadMaxLines () + { + // The rollover rebuild caps the history at SPREAD_MAX (99), not the 2×99 trim + // window — a preserved quirk of the original rollover code. + var results = new List(Enumerable.Range(0, 150)); + + var history = FilterSpread.RebuildHistory(results); + + Assert.Multiple(() => + { + Assert.That(history, Has.Count.EqualTo(99)); + Assert.That(history[0], Is.EqualTo(51)); + Assert.That(history[^1], Is.EqualTo(149)); + }); + } + + [Test] + public void RebuildHistory_ResultsShorterThanSpreadMax_TakesAllOfThem () + { + var history = FilterSpread.RebuildHistory([3, 4, 5]); + + Assert.That(history, Is.EqualTo(new[] { 3, 4, 5 })); + } + + [Test] + public void RebuildHistory_EmptyResults_ReturnsEmptyHistory () + { + var history = FilterSpread.RebuildHistory([]); + + Assert.That(history, Is.Empty); + } + /// /// Equivalence guard: the single-threaded Log Window filter and the multi-threaded /// Core filter accumulate per-hit results through the same recipe diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 34a88c47..332e89e0 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -4833,45 +4833,14 @@ private void ClearBookmarkList () [SupportedOSPlatform("windows")] private void ShiftFilterLines (int offset) { - List newFilterList = []; lock (_filterResultList) { - foreach (var lineNum in _filterResultList) - { - var line = lineNum - offset; - if (line >= 0) - { - newFilterList.Add(line); - } - } - - _filterResultList = newFilterList; - } - - newFilterList = []; - foreach (var lineNum in _filterHitList) - { - var line = lineNum - offset; - if (line >= 0) - { - newFilterList.Add(line); - } - } - - _filterHitList = newFilterList; - - var count = SPREAD_MAX; - if (_filterResultList.Count < SPREAD_MAX) - { - count = _filterResultList.Count; + _filterResultList = FilterSpread.ShiftLines(_filterResultList, offset); } - _lastFilterLinesList = _filterResultList.GetRange(_filterResultList.Count - count, count); + _filterHitList = FilterSpread.ShiftLines(_filterHitList, offset); + _lastFilterLinesList = FilterSpread.RebuildHistory(_filterResultList); - //this.filterGridView.RowCount = this.filterResultList.Count; - //this.filterCountLabel.Text = "" + this.filterResultList.Count; - //this.BeginInvoke(new MethodInvoker(this.filterGridView.Refresh)); - //this.BeginInvoke(new MethodInvoker(AddFilterLineGuiUpdate)); TriggerFilterLineGuiUpdate(); } From 1e6b72b1985007308ed91ee888e32d0cdbe89740 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:37:01 +0200 Subject: [PATCH 07/13] Mark ticket 4 acceptance criteria done --- tickets.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tickets.md b/tickets.md index dbaa40c6..ceb2214b 100644 --- a/tickets.md +++ b/tickets.md @@ -45,10 +45,10 @@ Work the **frontier**: any ticket whose blockers are all done. After the first t **Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. -- [ ] Shift operation is pure and lives in the module alongside expansion and trim -- [ ] Tests cover: plain shift, lines dropping out when the offset exceeds them, history rebuilt from the result tail, results shorter than the history window -- [ ] Log Window rollover handling delegates; no shift arithmetic remains inline -- [ ] Full solution builds and the test suite stays green +- [x] Shift operation is pure and lives in the module alongside expansion and trim +- [x] Tests cover: plain shift, lines dropping out when the offset exceeds them, history rebuilt from the result tail, results shorter than the history window +- [x] Log Window rollover handling delegates; no shift arithmetic remains inline +- [x] Full solution builds and the test suite stays green ## Contract: delete Log Window spread remnants, pin the knob, add glossary entry From 44dac1c469d8c1c22221e2ea8403144951b83999 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:43:21 +0200 Subject: [PATCH 08/13] Contract: single-source the spread constant, add glossary entry (ticket 5) The Log Window no longer carries any Filter Spread logic or constants: its private SPREAD_MAX is deleted and both spread knobs' maximum now references FilterSpread.SPREAD_MAX, so the knob range and the duplicate-suppression window derive from one value and cannot drift apart again. CONTEXT.md gains a Filtering section defining Filter Spread (Back Spread / Fore Spread), its ownership by the Core module, and the 99 / 2x99 derivation. Note: FilterParams itself does not clamp its values; the 99 cap is enforced at the UI knobs. This completes the Filter Spread extraction epic (tickets.md): all three call sites delegate, the arithmetic exists once in Core, and both sanctioned behavior changes are test-pinned. --- CONTEXT.md | 18 ++++++++++++++++++ .../Controls/LogWindow/LogWindow.cs | 5 ++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f872e975..adbf054b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -86,6 +86,24 @@ bare "session file" when you mean the workspace (that's a **Session**). *Avoid*: "control char rendering" (use **Substitution**), "escape" alone (ambiguous between the **C escape** style and the general concept). +## Filtering + +- **Filter Spread** — Context expansion around a filter hit: **Back Spread** + (`FilterParams.SpreadBefore`) lines before and **Fore Spread** + (`FilterParams.SpreadBehind`) lines after the hit are included in the + filter result, deduplicated against recently emitted lines and clamped + to the file's line range (line 0 is a valid context line). Owned by + `LogExpert.Core.Classes.Filter.FilterSpread` — the single home of the + expansion, history-trim, and rollover-shift rules; the serial filter, + the parallel filter, and Filter Pipes all delegate to it. The maximum + UI-configurable spread (99) and the duplicate-suppression window + (2 × 99) derive from `FilterSpread.SPREAD_MAX`, which bounds the UI + spread knobs (`FilterParams` itself does not clamp its values). + +*Avoid*: "spread" alone when ambiguous (say **Back Spread** / **Fore +Spread**), "additional filter results" (the old internal name — use +**Filter Spread**). + ## Columnizer selection - **Columnizer** (`ILogLineMemoryColumnizer`) — A plugin that parses a log diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 332e89e0..a6dbbf71 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -44,7 +44,6 @@ internal partial class LogWindow : DockContent, ILogPaintContextUI, ILogView, IL { #region Fields - private const int SPREAD_MAX = 99; private const int PROGRESS_BAR_MODULO = 1000; private const int FILTER_ADVANCED_SPLITTER_DISTANCE = 110; private const int FILTER_PANEL2_CONTROL_GAP = 6; @@ -273,10 +272,10 @@ public LogWindow (ILogWindowCoordinator logWindowCoordinator, string fileName, b dataGridView.ColumnDividerDoubleClick += OnDataGridViewColumnDividerDoubleClick; ShowAdvancedFilterPanel(false); knobControlFilterBackSpread.MinValue = 0; - knobControlFilterBackSpread.MaxValue = SPREAD_MAX; + knobControlFilterBackSpread.MaxValue = FilterSpread.SPREAD_MAX; knobControlFilterBackSpread.ValueChanged += OnFilterKnobControlValueChanged; knobControlFilterForeSpread.MinValue = 0; - knobControlFilterForeSpread.MaxValue = SPREAD_MAX; + knobControlFilterForeSpread.MaxValue = FilterSpread.SPREAD_MAX; knobControlFilterForeSpread.ValueChanged += OnFilterKnobControlValueChanged; knobControlFuzzy.MinValue = 0; knobControlFuzzy.MaxValue = 10; From 1fcba94ac9fabe01311011f7b0b85fb5fb94c120 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:43:21 +0200 Subject: [PATCH 09/13] Mark ticket 5 acceptance criteria done - epic complete --- tickets.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tickets.md b/tickets.md index ceb2214b..6d2a193d 100644 --- a/tickets.md +++ b/tickets.md @@ -56,7 +56,7 @@ Work the **frontier**: any ticket whose blockers are all done. After the first t **Blocked by:** Log Window serial filter delegates to the module; Filter Pipe processing delegates to the module; Rollover shifting moves into the module. -- [ ] No spread expansion, trim, or shift arithmetic remains anywhere in the Log Window -- [ ] The spread knobs' maximum value references the module's constant (99) -- [ ] CONTEXT.md defines Filter Spread (back spread / fore spread context expansion) in the project's glossary voice -- [ ] Full solution builds and the test suite stays green +- [x] No spread expansion, trim, or shift arithmetic remains anywhere in the Log Window +- [x] The spread knobs' maximum value references the module's constant (99) +- [x] CONTEXT.md defines Filter Spread (back spread / fore spread context expansion) in the project's glossary voice +- [x] Full solution builds and the test suite stays green From 6b01296aad17ca1fddf44d9a1e2690d72a77e0ff Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 18:45:22 +0200 Subject: [PATCH 10/13] removed --- tickets.md | 62 ------------------------------------------------------ 1 file changed, 62 deletions(-) delete mode 100644 tickets.md diff --git a/tickets.md b/tickets.md deleted file mode 100644 index 6d2a193d..00000000 --- a/tickets.md +++ /dev/null @@ -1,62 +0,0 @@ -# Tickets: Filter Spread extraction - -Extract the Filter Spread rules (back/fore spread context expansion, duplicate-suppression history, rollover shifting) out of the Log Window into one pure, tested Core module, per `docs/specs/filter-spread-extraction.md`. - -Work the **frontier**: any ticket whose blockers are all done. After the first ticket, tickets 2–4 can proceed in any order (or in parallel); the final ticket waits for all three. - -## Filter Spread module in Core with table tests; multi-threaded filter delegates - -**What to build:** A pure Filter Spread module in Core that owns hit expansion (back-spread lines, the hit, fore-spread lines — deduplicated against an already-emitted history, clamped to the file's line range) and the history-trim rule (window unified at 99, the UI knob's maximum spread). The multi-threaded filter delegates its per-hit accumulation to the module and its private spread copy is deleted. This is the tracer bullet: multi-threaded filtering immediately gains the line-0 fix and stops emitting duplicate context lines at spreads over 50. - -**Blocked by:** None — can start immediately. - -- [x] Module is pure: no dependency on controls, callbacks, readers, or locks; line count is passed in as a value -- [x] Expansion output order matches today's: ascending back-spread, hit, ascending fore-spread -- [x] Line 0 is a valid back-spread line (behavior change, pinned by an explicit test) -- [x] History window constant is 99 and lives only in the module -- [x] Table-driven NUnit edge-case tests pass: hit at line 0, hit within back-spread of line 0, hit near/at end-of-file, spread 0/0, asymmetric spreads, overlapping hits, hit already in history, trim at the 2×99 boundary -- [x] Multi-threaded filter's private spread arithmetic and trim snippet are deleted; it delegates to the module -- [x] Full solution builds and the existing test suite stays green - -## Log Window serial filter delegates to the module - -**What to build:** Single-threaded filtering in the Log Window produces module-owned spread: its per-hit accumulation delegates expansion and history trimming to the Filter Spread module, keeping its own locking and progress/GUI concerns at the call site. The line-0 fix and the 99 window go live for serial filtering, making it consistent with the parallel path. - -**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. - -- [x] Serial per-hit accumulation calls the module for expansion and trimming; no spread arithmetic remains inline in that path -- [x] Equivalence-guard test: an identical hit sequence fed the way the serial and parallel call sites do produces identical result/hit/history lists -- [x] Filtering a file with spread configured behaves identically whether the single- or multi-threaded filter runs -- [x] Full solution builds and the test suite stays green - -## Filter Pipe processing delegates to the module - -**What to build:** Lines written to a Filter Pipe tab receive the same module-owned spread expansion and history trimming as the filter result view, so derived tabs stay consistent with their source. The pipe path's inline trim (which today removes one entry at a time against the divergent constant) is replaced by the module's trim rule. - -**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. - -- [x] Filter Pipe per-hit processing calls the module for expansion and history trimming -- [x] A pipe tab fed by a filter with spread configured shows the same context lines as the filter view for the same hits -- [x] Full solution builds and the test suite stays green - -## Rollover shifting moves into the module - -**What to build:** After a multi-file rollover, previously collected filter results still point at the right content: the module gains the shift operation (shift result and hit line numbers by the rollover offset, drop lines that rolled off the start, rebuild the duplicate-suppression history from the tail of the shifted results) with its own test rows. The Log Window's rollover handler delegates, keeping only its lock and GUI refresh. - -**Blocked by:** Filter Spread module in Core with table tests; multi-threaded filter delegates. - -- [x] Shift operation is pure and lives in the module alongside expansion and trim -- [x] Tests cover: plain shift, lines dropping out when the offset exceeds them, history rebuilt from the result tail, results shorter than the history window -- [x] Log Window rollover handling delegates; no shift arithmetic remains inline -- [x] Full solution builds and the test suite stays green - -## Contract: delete Log Window spread remnants, pin the knob, add glossary entry - -**What to build:** The Log Window carries no spread logic or constants of its own. Its private spread-expansion copy and local spread constant are deleted; the UI spread knobs' maximum references the module's constant so the knob range and the suppression window can never drift apart again; CONTEXT.md gains a canonical **Filter Spread** glossary entry. - -**Blocked by:** Log Window serial filter delegates to the module; Filter Pipe processing delegates to the module; Rollover shifting moves into the module. - -- [x] No spread expansion, trim, or shift arithmetic remains anywhere in the Log Window -- [x] The spread knobs' maximum value references the module's constant (99) -- [x] CONTEXT.md defines Filter Spread (back spread / fore spread context expansion) in the project's glossary voice -- [x] Full solution builds and the test suite stays green From 565a558694eb38864876a1353eae83edd27283ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 17:28:11 +0000 Subject: [PATCH 11/13] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 5f1cb401..5283c38f 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-07-09 13:31:31 UTC + /// Generated: 2026-07-09 17:28:10 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "F1F2DC47E77D7D53F36BDF7C49A4B011C1D9C9445D55F8F9076FF0049F800758", + ["AutoColumnizer.dll"] = "6AA4B522A288F18B940A492886FA3BA922370EA8E41EB55690DC9008402BF4EF", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "AE76D824E4FA7C22430D44E2908F6280DCF6EEF60421AC941F0629CD3C2EFA1B", - ["CsvColumnizer.dll (x86)"] = "AE76D824E4FA7C22430D44E2908F6280DCF6EEF60421AC941F0629CD3C2EFA1B", - ["DefaultPlugins.dll"] = "13A1110029941B0AD680C9A612E8915E0B5C41867E46271E6B27E4FEA4A99567", - ["FlashIconHighlighter.dll"] = "F572E5ECD91CD0C06801C14B1B80BE1084BA227EFCC75498E5DC2E7D85A070B6", - ["GlassfishColumnizer.dll"] = "78ED9C9C15FC2B882BE0090C9925206D3F5E421CE3E81902EF8BA3188466E94F", - ["JsonColumnizer.dll"] = "ED065D921ACE1C5E46933B5BD0B7CBFEF70D3D06F06A760C985CF9D6748B547B", - ["JsonCompactColumnizer.dll"] = "246CB3F50CCE4A0812050C905D773F5FFA98C853448FDEC2EF9238F81DEB14B1", - ["Log4jXmlColumnizer.dll"] = "B1FA5347C1CD897B579FE05C6ADD41CCEE580328063CB32F99D2184F94D36D48", - ["LogExpert.Resources.dll"] = "6D842E513DC8616900AF1458F8799D5E749B251CD281FB3E14C0C225EE48D040", + ["CsvColumnizer.dll"] = "C5F6D38808A045A08781AE19E34B0693417CFA234163399F9FDE0A29D3D44A91", + ["CsvColumnizer.dll (x86)"] = "C5F6D38808A045A08781AE19E34B0693417CFA234163399F9FDE0A29D3D44A91", + ["DefaultPlugins.dll"] = "980DD4FF54F973A63DA1AE4374B593BE676EC7AD5CC99E8DAC7836E9DCC488CC", + ["FlashIconHighlighter.dll"] = "DE28DCB05D61670FA35A1442D27A37438B48E0A30D0E31BE3255E25557114394", + ["GlassfishColumnizer.dll"] = "DD48B9EA3DEC493C6ACF7B9E77EE3DACF09EBC678B2AE56D0AF512F1C102CFA8", + ["JsonColumnizer.dll"] = "4F9993D74701EBBE953774D8B05171EC66CA7DCA828DD3D3BC66036899B7B2A4", + ["JsonCompactColumnizer.dll"] = "ADC6F2B3EA0FCE16F3FE7E3E0C753359736A9414FEC54C268B690B812F4017AB", + ["Log4jXmlColumnizer.dll"] = "5D19814EE3B93C95E90B1CD351CD20A164F3003D6A02D884F50BA80EE634C7BD", + ["LogExpert.Resources.dll"] = "5F4DC82B25E08CB824AFD21E5DF4C0B11348FBEA964A8EFA3131CBBA09341B42", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "4DC5B66DCD8B722C35DF01AB07388D5723D0C4EC2A8D8860C28ACCE127588981", - ["SftpFileSystem.dll"] = "10A371D93A95C985270DDC912BC01AF2BE8D7AAA7705C6622EB2AF96FD3AB17B", - ["SftpFileSystem.dll (x86)"] = "70F6B8AA1FD98F3BB62D3B8A5D3000E2840F6F783693215083D6E014B0ED8D33", - ["SftpFileSystem.Resources.dll"] = "45A3D1C54734819BAD2CFE2245CE2CEE664B9E6401E9F1BEF4B2955BEF0C307F", - ["SftpFileSystem.Resources.dll (x86)"] = "45A3D1C54734819BAD2CFE2245CE2CEE664B9E6401E9F1BEF4B2955BEF0C307F", + ["RegexColumnizer.dll"] = "66A5F491FFFDE12A26FB22A0CD7943B3276458082751D5961A126D7426FEFEFF", + ["SftpFileSystem.dll"] = "4CB88C070876F239CA274D80C37B7B9399BECBDE904E0EBFF4E1AE6033702BFC", + ["SftpFileSystem.dll (x86)"] = "A510B166B70B6CD51CF2040F66A0F3CA293C62517B95D2A71ED104D55C3FC937", + ["SftpFileSystem.Resources.dll"] = "B57839C99A7D90F7BB8A482EBDFF3490FE9BE47C10D07DACAD2FBB52D9E4F9B5", + ["SftpFileSystem.Resources.dll (x86)"] = "B57839C99A7D90F7BB8A482EBDFF3490FE9BE47C10D07DACAD2FBB52D9E4F9B5", }; } From f65a2f5f0c45f47e177f423a5cf9df8674ab2024 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 9 Jul 2026 19:36:47 +0200 Subject: [PATCH 12/13] small review change --- src/LogExpert.Core/Classes/Filter/FilterSpread.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs index acf7113b..8c7d97b1 100644 --- a/src/LogExpert.Core/Classes/Filter/FilterSpread.cs +++ b/src/LogExpert.Core/Classes/Filter/FilterSpread.cs @@ -34,17 +34,9 @@ public static List ShiftLines (IEnumerable lines, int offset) { ArgumentNullException.ThrowIfNull(lines); - List shifted = []; - foreach (var lineNum in lines) - { - var line = lineNum - offset; - if (line >= 0) - { - shifted.Add(line); - } - } - - return shifted; + return [.. lines + .Select(lineNum => lineNum - offset) + .Where(line => line >= 0)]; } /// From 4d3834170b85ffec3b100efd439004c9e4e3c819 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 17:38:56 +0000 Subject: [PATCH 13/13] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 5283c38f..f5d185bb 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-07-09 17:28:10 UTC + /// Generated: 2026-07-09 17:38:54 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "6AA4B522A288F18B940A492886FA3BA922370EA8E41EB55690DC9008402BF4EF", + ["AutoColumnizer.dll"] = "CDEFDC033AD707622C49BD4EC937EB9EDE54F8C1EAB19678E29CDC938A91365A", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "C5F6D38808A045A08781AE19E34B0693417CFA234163399F9FDE0A29D3D44A91", - ["CsvColumnizer.dll (x86)"] = "C5F6D38808A045A08781AE19E34B0693417CFA234163399F9FDE0A29D3D44A91", - ["DefaultPlugins.dll"] = "980DD4FF54F973A63DA1AE4374B593BE676EC7AD5CC99E8DAC7836E9DCC488CC", - ["FlashIconHighlighter.dll"] = "DE28DCB05D61670FA35A1442D27A37438B48E0A30D0E31BE3255E25557114394", - ["GlassfishColumnizer.dll"] = "DD48B9EA3DEC493C6ACF7B9E77EE3DACF09EBC678B2AE56D0AF512F1C102CFA8", - ["JsonColumnizer.dll"] = "4F9993D74701EBBE953774D8B05171EC66CA7DCA828DD3D3BC66036899B7B2A4", - ["JsonCompactColumnizer.dll"] = "ADC6F2B3EA0FCE16F3FE7E3E0C753359736A9414FEC54C268B690B812F4017AB", - ["Log4jXmlColumnizer.dll"] = "5D19814EE3B93C95E90B1CD351CD20A164F3003D6A02D884F50BA80EE634C7BD", - ["LogExpert.Resources.dll"] = "5F4DC82B25E08CB824AFD21E5DF4C0B11348FBEA964A8EFA3131CBBA09341B42", + ["CsvColumnizer.dll"] = "4EBBA85D9A259D82342E605E4381CD242A9212186711036E93AFD8DADDA23486", + ["CsvColumnizer.dll (x86)"] = "4EBBA85D9A259D82342E605E4381CD242A9212186711036E93AFD8DADDA23486", + ["DefaultPlugins.dll"] = "CB04A2942034D9FF2506379A81D04A438E9C3F55225C28CE4249CB2639724F83", + ["FlashIconHighlighter.dll"] = "A3DC19BF3D3E660325E54B6ABBEB38A81351DC269CD2C292C7038A0181250B39", + ["GlassfishColumnizer.dll"] = "55FF72EC8D78E1E33EECE105C625BEEE633A1AB114A9A8F6EF3D7F489770F2BE", + ["JsonColumnizer.dll"] = "EAF4FE2D20542C7747807083088B222CCE201AF8E9B2287255D0CF76EB52C114", + ["JsonCompactColumnizer.dll"] = "0F8A8BF3FF85F063D2B219A27F11598954A4A4679C2A972D303836D12E8D7C8E", + ["Log4jXmlColumnizer.dll"] = "113BD3CACB5D0376BD74DBE98A51E0A4F60405CC0BF10987097234162A3275F0", + ["LogExpert.Resources.dll"] = "4F151AC6D75E0844E5B4EFEDE591252EE9C998BF61CB9B715E2A6DB7CEA0DBB3", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "66A5F491FFFDE12A26FB22A0CD7943B3276458082751D5961A126D7426FEFEFF", - ["SftpFileSystem.dll"] = "4CB88C070876F239CA274D80C37B7B9399BECBDE904E0EBFF4E1AE6033702BFC", - ["SftpFileSystem.dll (x86)"] = "A510B166B70B6CD51CF2040F66A0F3CA293C62517B95D2A71ED104D55C3FC937", - ["SftpFileSystem.Resources.dll"] = "B57839C99A7D90F7BB8A482EBDFF3490FE9BE47C10D07DACAD2FBB52D9E4F9B5", - ["SftpFileSystem.Resources.dll (x86)"] = "B57839C99A7D90F7BB8A482EBDFF3490FE9BE47C10D07DACAD2FBB52D9E4F9B5", + ["RegexColumnizer.dll"] = "ADAF0EA008A488EB108C6B4C8562EBA96E65A6EAD58ED351DA696C8F7DA02B86", + ["SftpFileSystem.dll"] = "712F494AA8627BC4D6FA5EE729F5E225BCB61DAE3483D57C8BFD71CBC54841B4", + ["SftpFileSystem.dll (x86)"] = "F37E477D24B41C506CEE321423E1D5E1EEC731E04E9FD7378ADD695736CE62CC", + ["SftpFileSystem.Resources.dll"] = "3BD8924373C41D64FCB8AD7A6EE9B4E8FCF5BE311A7E4222A75FECF6567D6BEE", + ["SftpFileSystem.Resources.dll (x86)"] = "3BD8924373C41D64FCB8AD7A6EE9B4E8FCF5BE311A7E4222A75FECF6567D6BEE", }; }