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
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@
"git rev-parse": true
},
"dotnet.preferCSharpExtension": true,
"dotnet.defaultSolution": "src/LogExpert.sln"
"dotnet.defaultSolution": "src/LogExpert.sln",
"cSpell.words": [
"Columnizer",
"multifile",
"Respawned",
"timeshift"
]
}
31 changes: 25 additions & 6 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,30 @@ meaning; do not redefine them locally.
field of a Session File is one of several sources that can be selected
by **Columnizer Selection Priority**; the other fields always load when
the Session File exists.
- **Session Snapshot** (`SessionSnapshot`) — A neutral, UI-free capture of
one Log Window's persistable state. Gathered by the Log Window when a
Session File is saved and applied back in two phases when one is loaded
(pre-load: options like encoding, multi-file, columnizer, panel layout;
post-load: content-dependent state like bookmarks, scroll position,
filters). Recursive: carries a child snapshot per filter-pipe tab.
- **Session File Composer** (`SessionFileComposer`) — The pure Core module
beside `Persister` that owns the field mapping between a Session
Snapshot and a Session File's serialized form, in both directions
(compose on save, decompose on load), plus the Rollover staleness rule.
It never does I/O, shows UI, or has side effects; the Log Window owns
gathering, applying, timing, and error display.
- **Rollover staleness rule** — The predicate declaring a Session Snapshot
stale because it was saved against a longer file than the one on disk
(the saved line count exceeds the current one) — i.e. the log file has
rolled over since the save. A stale snapshot's post-load state
(bookmarks, scroll, filters) is discarded; the pre-load options still
apply. Owned by the Session File Composer.

*Avoid*: "project" / "project file" / "workspace" (use **Session**),
"persistence file" / "per-file persistence" (use **Session File**),
bare "session file" when you mean the workspace (that's a **Session**).
bare "session file" when you mean the workspace (that's a **Session**),
"options-only load" (a Session File is always read whole; the options/full
split is two *apply phases* of the **Session Snapshot**, not a partial read).

## Control character display

Expand Down Expand Up @@ -239,9 +259,8 @@ _Avoid_: Stdout pipe, output redirect
_Avoid_: Toolbar, tool strip

### Sessions & Persistence

**Session File**: A `.lxj` file that stores the list of log files, per-file settings, and the dock layout for a Session.
_Avoid_: Project file, config file

**Session File Reference** (`.lxp`): An indirection file that maps to one or more actual log files, allowing a Session to track a logical source rather than a fixed path.
_Avoid_: Log pointer, alias
Defined in the **Sessions** section above: **Session** = `.lxj` (workspace),
**Session File** = `.lxp` (per-file state) — see also the flagged-ambiguity
resolution. (This section previously redefined both terms the pre-resolution
way; removed 2026-07-11.)
15 changes: 15 additions & 0 deletions src/LogExpert.Core/Classes/Persister/FilterTabSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using LogExpert.Core.Classes.Filter;

namespace LogExpert.Core.Classes.Persister;

/// <summary>
/// One Filter Pipe tab's persistable state: the filter that feeds the tab plus the tab's own
/// window state as a nested snapshot — the recursion point of <see cref="SessionSnapshot"/>.
/// Mapped to and from <see cref="FilterTabData"/> by <see cref="SessionFileComposer"/>.
/// </summary>
public class FilterTabSnapshot
{
public FilterParams FilterParams { get; set; }

public SessionSnapshot Snapshot { get; set; }
}
109 changes: 109 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
namespace LogExpert.Core.Classes.Persister;

/// <summary>
/// Pure mapping between a <see cref="SessionSnapshot"/> and the Session File's serialized form
/// (<see cref="PersistenceData"/>), in both directions, plus the Rollover staleness rule.
/// No I/O, no UI, no side effects — the Log Window owns gathering, applying, timing, and error
/// display; <see cref="Persister"/> owns serialization.
/// </summary>
public static class SessionFileComposer
{
/// <summary>
/// Maps a snapshot to the Session File's serialized form. Fields the snapshot does not carry
/// (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.
/// </summary>
public static PersistenceData Compose (SessionSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);

return new PersistenceData
{
FollowTail = snapshot.FollowTail,
Encoding = snapshot.Encoding,
LineCount = snapshot.LineCount,
CurrentLine = snapshot.CurrentLine,
FirstDisplayedLine = snapshot.FirstDisplayedLine,
FilterPosition = snapshot.FilterPosition,
FilterVisible = snapshot.FilterVisible,
FilterAdvanced = snapshot.FilterAdvanced,
CellSelectMode = snapshot.CellSelectMode,
FilterSaveListVisible = snapshot.FilterSaveListVisible,
MultiFile = snapshot.MultiFile,
MultiFileMaxDays = snapshot.MultiFileMaxDays,
MultiFilePattern = snapshot.MultiFilePattern,
TabName = snapshot.TabName,
HighlightGroupName = snapshot.HighlightGroupName,
FileName = snapshot.FileName,
BookmarkList = snapshot.BookmarkList,
RowHeightList = snapshot.RowHeightList,
MultiFileNames = snapshot.MultiFileNames,
FilterParamsList = snapshot.FilterParamsList,
Columnizer = snapshot.Columnizer,
FilterTabDataList =
[
.. snapshot.FilterTabs.Select(tab => new FilterTabData
{
FilterParams = tab.FilterParams,
PersistenceData = Compose(tab.Snapshot),
}),
],
};
}

/// <summary>
/// Maps a loaded Session File back to a snapshot. Fields the snapshot does not carry are
/// ignored.
/// </summary>
public static SessionSnapshot Decompose (PersistenceData persistenceData)
{
ArgumentNullException.ThrowIfNull(persistenceData);

return new SessionSnapshot
{
FollowTail = persistenceData.FollowTail,
Encoding = persistenceData.Encoding,
LineCount = persistenceData.LineCount,
CurrentLine = persistenceData.CurrentLine,
FirstDisplayedLine = persistenceData.FirstDisplayedLine,
FilterPosition = persistenceData.FilterPosition,
FilterVisible = persistenceData.FilterVisible,
FilterAdvanced = persistenceData.FilterAdvanced,
CellSelectMode = persistenceData.CellSelectMode,
FilterSaveListVisible = persistenceData.FilterSaveListVisible,
MultiFile = persistenceData.MultiFile,
MultiFileMaxDays = persistenceData.MultiFileMaxDays,
MultiFilePattern = persistenceData.MultiFilePattern,
TabName = persistenceData.TabName,
HighlightGroupName = persistenceData.HighlightGroupName,
FileName = persistenceData.FileName,
BookmarkList = persistenceData.BookmarkList,
RowHeightList = persistenceData.RowHeightList,
MultiFileNames = persistenceData.MultiFileNames,
FilterParamsList = persistenceData.FilterParamsList,
Columnizer = persistenceData.Columnizer,
FilterTabs =
[
.. persistenceData.FilterTabDataList.Select(tab => new FilterTabSnapshot
{
FilterParams = tab.FilterParams,
Snapshot = Decompose(tab.PersistenceData),
}),
],
};
}

/// <summary>
/// Declares a snapshot stale because it was saved against a longer file than the one on disk
/// — i.e. the log file has rolled over since the save. A stale snapshot's post-load state
/// (bookmarks, scroll, filters) must be discarded by the Log Window; the pre-load options
/// still apply.
/// </summary>
public static bool IsStale (SessionSnapshot snapshot, int currentLineCount)
{
ArgumentNullException.ThrowIfNull(snapshot);

return snapshot.LineCount > currentLineCount;
}
}
79 changes: 79 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Text;

using ColumnizerLib;

using LogExpert.Core.Classes.Filter;
using LogExpert.Core.Entities;

namespace LogExpert.Core.Classes.Persister;

/// <summary>
/// A neutral, UI-free capture of one Log Window's persistable state. Gathered by the Log Window when a Session File is saved and applied back in two
/// Log-Window-side phases when one is loaded. Mapped to and from <see cref="PersistenceData"/> by
/// <see cref="SessionFileComposer"/>.
/// </summary>
public class SessionSnapshot
{
public bool FollowTail { get; set; }

public Encoding Encoding { get; set; }

/// <summary>
/// The log file's line count at save time. Never applied to the window — it is the input to
/// the Rollover staleness rule (<see cref="SessionFileComposer.IsStale"/>).
/// </summary>
public int LineCount { get; set; }

public int CurrentLine { get; set; }

public int FirstDisplayedLine { get; set; }

/// <summary>Splitter distance of the filter panel, as a plain value.</summary>
public int FilterPosition { get; set; }

public bool FilterVisible { get; set; }

public bool FilterAdvanced { get; set; }

public bool CellSelectMode { get; set; }

public bool FilterSaveListVisible { get; set; }

public bool MultiFile { get; set; }

public int MultiFileMaxDays { get; set; }

public string MultiFilePattern { get; set; }

public string TabName { get; set; }

public string HighlightGroupName { get; set; }

/// <summary>
/// The log file the session belongs to. Never applied to the window — consumed by
/// <c>PersisterHelpers.FindFilenameForSettings</c> on direct <c>.lxp</c> open. Note:
/// <see cref="Persister"/> rewrites it on SameDir saves *after* compose (transformed by
/// design, named in the round-trip exclusion list).
/// </summary>
public string FileName { get; set; }

/// <summary>
/// Manual bookmarks only — filtering out auto-generated bookmarks is a gather-side rule of
/// the Log Window; the snapshot carries what it is given.
/// </summary>
public SortedList<int, Entities.Bookmark> BookmarkList { get; set; } = [];

public SortedList<int, RowHeightEntry> RowHeightList { get; set; } = [];

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

public List<FilterParams> FilterParamsList { get; set; } = [];

public ILogLineMemoryColumnizer Columnizer { get; set; }

/// <summary>
/// The window's Filter Pipe tabs. Each child carries its own full nested snapshot, so the
/// whole tab tree rides along on save and load.
/// </summary>
public List<FilterTabSnapshot> FilterTabs { get; set; } = [];
}
14 changes: 8 additions & 6 deletions src/LogExpert.Core/Interfaces/ILogWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,19 @@ public interface ILogWindow
void SelectLine (int lineNum, bool triggerSyncCall, bool shouldScroll);

/// <summary>
/// Gets the persistence data for this log window, which can be saved and later restored.
/// Gathers a Session Snapshot of this log window's persistable state, including recursive
/// child snapshots for its filter pipe tabs.
/// </summary>
/// <returns>
/// A <see cref="PersistenceData"/> object containing the current state of the window,
/// including the current line, filters, columnizer configuration, and other settings.
/// A <see cref="SessionSnapshot"/> capturing the current state of the window: current line,
/// filters, columnizer configuration, and other settings.
/// </returns>
/// <remarks>
/// This data is used to restore the log window state between sessions, including
/// the current scroll position, active filters, and columnizer settings.
/// The snapshot is mapped to the Session File's serialized form by
/// <see cref="SessionFileComposer"/> and applied back in two phases when a session is
/// loaded.
/// </remarks>
PersistenceData GetPersistenceData ();
SessionSnapshot GatherSessionSnapshot ();

/// <summary>
/// Creates a new temporary file tab with the specified content.
Expand Down
Loading