Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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; }
}
110 changes: 110 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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. Field-by-field contract:
/// docs/specs/session-file-composer.md.
/// </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;
}
}
81 changes: 81 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 (the Session Snapshot of
/// CONTEXT.md). 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"/>; see docs/specs/session-file-composer.md for the full field
/// mapping table.
/// </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