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
12 changes: 11 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ split is two *apply phases* of the **Session Snapshot**, not a partial read).
results/history → trim history). Used inside the Serial engine, per line by
the Log Window's tail filter path, and per hit by Filter Pipes — all three
adopt existing lists, so a Filter Run's history is continued in place.
- **Window Filter** (`LogWindow._filterParams`) — The one filter a Log Window
currently holds in its filter panel. Per-window state: it is what a **Session
File** saves and restores, and it is not an entry of the **Saved Filter
List**. A Filter Pipe tab gets its own, cloned when the tab is created.
- **Saved Filter List** (`Settings.FilterList`) — The application-wide list of
filters the user explicitly saved from the filter panel, shown in the filter
save list. Global, never per-window, and never restored from a Session File.
(Session Files written before the Window Filter change stored a copy of it
instead, of which only the first entry was ever restored.)
- **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
Expand All @@ -146,7 +155,8 @@ split is two *apply phases* of the **Session Snapshot**, not a partial read).
Spread**), "additional filter results" (the old internal name — use
**Filter Spread**), "multi-threaded filter" as a concept name (it is a
preference selecting the Parallel **Filter Engine**), "FilterFx" (legacy
delegate name, deleted).
delegate name, deleted), "the filter list" when the **Window Filter** is meant
(the list is the **Saved Filter List**).

## Log Search

Expand Down
5 changes: 5 additions & 0 deletions src/LogExpert.Core/Classes/Persister/PersistenceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public class PersistenceData

public bool FilterAdvanced { get; set; }

/// <summary>
/// The Log Window's own filter, as the single entry of a list. A list for backward
/// compatibility only — <see cref="SessionFileComposer"/> owns the mapping and documents what
/// older Session Files carry here.
/// </summary>
public List<FilterParams> FilterParamsList { get; set; } = [];

public int FilterPosition { get; set; } = 222;
Expand Down
13 changes: 11 additions & 2 deletions src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public static class SessionFileComposer
/// (the dead fields and <c>SessionFileName</c> named in the spec) are left at their type
/// defaults. Note: <see cref="Persister"/> rewrites <c>FileName</c> on SameDir saves *after*
/// compose — transformed by design, outside this contract.
/// <para>
/// <c>FilterParamsList</c> is a list only for backward compatibility: the window's own filter
/// goes in as its single entry, and a snapshot without one composes to an empty list.
/// </para>
/// </summary>
public static PersistenceData Compose (SessionSnapshot snapshot)
{
Expand All @@ -39,7 +43,7 @@ public static PersistenceData Compose (SessionSnapshot snapshot)
BookmarkList = snapshot.BookmarkList,
RowHeightList = snapshot.RowHeightList,
MultiFileNames = snapshot.MultiFileNames,
FilterParamsList = snapshot.FilterParamsList,
FilterParamsList = snapshot.FilterParams is null ? [] : [snapshot.FilterParams],
Columnizer = snapshot.Columnizer,
FilterTabDataList =
[
Expand All @@ -55,6 +59,11 @@ public static PersistenceData Compose (SessionSnapshot snapshot)
/// <summary>
/// Maps a loaded Session File back to a snapshot. Fields the snapshot does not carry are
/// ignored.
/// <para>
/// The window's filter is <c>FilterParamsList[0]</c>, which is what load has always used.
/// Session Files written before the per-window filter change carry a copy of the whole global
/// filter list there; the trailing entries were never restored by anything and are dropped.
/// </para>
/// </summary>
public static SessionSnapshot Decompose (PersistenceData persistenceData)
{
Expand All @@ -81,7 +90,7 @@ public static SessionSnapshot Decompose (PersistenceData persistenceData)
BookmarkList = persistenceData.BookmarkList,
RowHeightList = persistenceData.RowHeightList,
MultiFileNames = persistenceData.MultiFileNames,
FilterParamsList = persistenceData.FilterParamsList,
FilterParams = persistenceData.FilterParamsList.Count > 0 ? persistenceData.FilterParamsList[0] : null,
Columnizer = persistenceData.Columnizer,
FilterTabs =
[
Expand Down
7 changes: 6 additions & 1 deletion src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ public class SessionSnapshot

public List<string> MultiFileNames { get; set; } = [];

public List<FilterParams> FilterParamsList { get; set; } = [];
/// <summary>
/// The window's own filter, or <c>null</c> when there is none to save (Save Filters off, or a
/// Session File that carries no filter). Its serialized form is a one-entry list — see
/// <see cref="SessionFileComposer"/>.
/// </summary>
public FilterParams FilterParams { get; set; }

public ILogLineMemoryColumnizer Columnizer { get; set; }

Expand Down
62 changes: 61 additions & 1 deletion src/LogExpert.Persister.Tests/SessionFileComposerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ private static SessionSnapshot CreateFullSnapshot (int filterTabDepth = 2)
BookmarkList = new SortedList<int, Bookmark> { [5] = new Bookmark(5, "a manual bookmark") },
RowHeightList = new SortedList<int, RowHeightEntry> { [3] = new RowHeightEntry(3, 60) },
MultiFileNames = ["app.1.log", "app.2.log"],
FilterParamsList = [CreateFilterParams("ERROR")],
FilterParams = CreateFilterParams("ERROR"),
Columnizer = new DefaultLogfileColumnizer(),
FilterTabs = filterTabDepth > 0
?
Expand Down Expand Up @@ -369,6 +369,66 @@ public void DiskTrip_ComposePersistLoadDecompose_ReturnsEqualSnapshot ()

#endregion

#region Window filter (issue #666)

// A Session File carries the window's OWN filter, not a copy of the global filter list. The
// on-disk shape stays a list so existing .lxp files keep loading — new saves just write
// exactly one entry into it.
[Test]
public void Compose_WritesTheWindowFilterAsTheSingleFilterParamsListEntry ()
{
var snapshot = CreateFullSnapshot(filterTabDepth: 0);
snapshot.FilterParams = CreateFilterParams("the window's own filter");

var composed = SessionFileComposer.Compose(snapshot);

Assert.That(composed.FilterParamsList, Has.Count.EqualTo(1));
Assert.That(composed.FilterParamsList[0].SearchText, Is.EqualTo("the window's own filter"));
}

// SaveFilters off (and filter-less windows) leave the snapshot without a filter — the field
// must not serialize a null element into the list.
[Test]
public void Compose_WithoutAWindowFilter_WritesAnEmptyFilterParamsList ()
{
var snapshot = CreateFullSnapshot(filterTabDepth: 0);
snapshot.FilterParams = null;

Assert.That(SessionFileComposer.Compose(snapshot).FilterParamsList, Is.Empty);
}

// The compatibility story: a pre-#666 Session File carries a copy of the whole global filter
// list, and load has always taken [0] as the window's filter. That stays true — and the
// trailing entries, which nothing ever restored, are dropped when such a file is re-saved.
[Test]
public void Decompose_LegacySessionFileCarryingTheGlobalList_KeepsTheFirstEntryAndDropsTheRest ()
{
var legacy = CreateCarriedPersistenceData(filterTabDepth: 0);
legacy.FilterParamsList =
[
CreateFilterParams("the first global filter"),
CreateFilterParams("another global filter"),
];

var snapshot = SessionFileComposer.Decompose(legacy);
var recomposed = SessionFileComposer.Compose(snapshot);

Assert.That(snapshot.FilterParams.SearchText, Is.EqualTo("the first global filter"));
Assert.That(recomposed.FilterParamsList, Has.Count.EqualTo(1));
Assert.That(recomposed.FilterParamsList[0].SearchText, Is.EqualTo("the first global filter"));
}

[Test]
public void Decompose_SessionFileWithoutFilters_LeavesTheWindowFilterUnset ()
{
var data = CreateCarriedPersistenceData(filterTabDepth: 0);
data.FilterParamsList = [];

Assert.That(SessionFileComposer.Decompose(data).FilterParams, Is.Null);
}

#endregion

#region Omitted fields (the ✖ rows of the mapping table)

// The snapshot does not carry the dead fields or SessionFileName, so Compose must leave them
Expand Down
16 changes: 10 additions & 6 deletions src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2660,9 +2660,9 @@ private void LoadPersistenceData ()
[SupportedOSPlatform("windows")]
private void RestoreFilters (SessionSnapshot snapshot)
{
if (snapshot.FilterParamsList.Count > 0)
if (snapshot.FilterParams != null)
{
_filterParams = snapshot.FilterParamsList[0];
_filterParams = snapshot.FilterParams;
ReInitFilterParams(_filterParams);
}

Expand Down Expand Up @@ -5832,8 +5832,8 @@ public string SavePersistenceDataAndReturnFileName (bool force)
try
{
// Relocated from the old composing getter (the composer must stay side-effect-free):
// this option doesn't need a press on 'search'. The write must precede the gather
// the gather copies the global FilterList, and this write may alias into it.
// this option doesn't need a press on 'search'. The write must precede the gather,
// which is what copies _filterParams into the snapshot.
_filterParams.IsFilterTail = filterTailCheckBox.Checked;

var persistenceData = SessionFileComposer.Compose(GatherSessionSnapshot());
Expand Down Expand Up @@ -5893,8 +5893,12 @@ public SessionSnapshot GatherSessionSnapshot ()

if (Preferences.SaveFilters)
{
// Persist this window's active filter; FilterList contains global saved presets.
snapshot.FilterParamsList = [_filterParams.Clone()];
// The window's own filter, not the global filter list: a Session File is per-window
// state, and the global list is never restored from one anyway. Clone() is a shallow
// copy (ColumnList and the columnizer stay shared) — enough here, because compose and
// serialize run synchronously right after this gather.
snapshot.FilterParams = _filterParams.Clone();

foreach (var filterPipe in _filterPipeList)
{
snapshot.FilterTabs.Add(new FilterTabSnapshot
Expand Down
Loading