1+ using System . Text ;
2+
3+ using ColumnizerLib ;
4+
5+ using LogExpert . Core . Classes . Highlight ;
6+
7+ namespace LogExpert . Core . Classes . Bookmark ;
8+
9+ /// <summary>
10+ /// Scans all lines of a log file against highlight entries that have <see cref="HighlightEntry.IsSetBookmark"/> set,
11+ /// producing a list of auto-generated bookmarks. This is a pure computation unit with no UI dependencies.
12+ /// </summary>
13+ public static class HighlightBookmarkScanner
14+ {
15+ /// <summary>
16+ /// Scans lines [0..lineCount) for highlight matches and returns bookmarks for matching lines.
17+ /// </summary>
18+ /// <param name="lineCount">Total number of lines in the file.</param>
19+ /// <param name="getLine">
20+ /// Delegate that returns the log line at a given index. May return null for unavailable lines.
21+ /// </param>
22+ /// <param name="entries">
23+ /// The highlight entries to check. Only entries with <see cref="HighlightEntry.IsSetBookmark"/> == true produce
24+ /// bookmarks.
25+ /// </param>
26+ /// <param name="fileName">
27+ /// The file name, passed to <see cref="ParamParser"/> for bookmark comment template resolution.
28+ /// </param>
29+ /// <param name="progressBarModulo">
30+ /// Interval of lines for reporting progress via the <paramref name="progress"/> callback.
31+ /// </param>
32+ /// <param name="cancellationToken">Token to support cooperative cancellation.</param>
33+ /// <param name="progress">
34+ /// Optional progress callback receiving the current line index (for progress reporting).
35+ /// </param>
36+ /// <returns>List of auto-generated bookmarks for all matched lines.</returns>
37+ public static List < Entities . Bookmark > Scan ( int lineCount , Func < int , ILogLineMemory > getLine , IList < HighlightEntry > entries , string fileName , int progressBarModulo = 1000 , IProgress < int > progress = null , CancellationToken cancellationToken = default )
38+ {
39+ ArgumentNullException . ThrowIfNull ( getLine ) ;
40+
41+ List < Entities . Bookmark > result = [ ] ;
42+
43+ // Pre-filter: only entries with IsSetBookmark matter
44+ var bookmarkEntries = entries . Where ( e => e . IsSetBookmark ) . ToList ( ) ;
45+ if ( bookmarkEntries . Count == 0 )
46+ {
47+ return result ;
48+ }
49+
50+ for ( var i = 0 ; i < lineCount ; i ++ )
51+ {
52+ cancellationToken . ThrowIfCancellationRequested ( ) ;
53+
54+ var line = getLine ( i ) ;
55+ if ( line == null )
56+ {
57+ continue ;
58+ }
59+
60+ var ( setBookmark , bookmarkComment , sourceHighlightText ) = GetBookmarkAction ( line , bookmarkEntries ) ;
61+
62+ if ( setBookmark )
63+ {
64+ var comment = ResolveComment ( bookmarkComment , line , i , fileName ) ;
65+ result . Add ( Entities . Bookmark . CreateAutoGenerated ( i , comment , sourceHighlightText ) ) ;
66+ }
67+
68+ if ( i % progressBarModulo == 0 )
69+ {
70+ progress ? . Report ( i ) ;
71+ }
72+ }
73+
74+ return result ;
75+ }
76+
77+ /// <summary>
78+ /// Checks a single line against the bookmark-producing highlight entries. Returns whether a bookmark should be set,
79+ /// the concatenated comment template, and the source highlight text.
80+ /// </summary>
81+ private static ( bool SetBookmark , string BookmarkComment , string SourceHighlightText ) GetBookmarkAction ( ITextValueMemory line , List < HighlightEntry > bookmarkEntries )
82+ {
83+ var setBookmark = false ;
84+ var bookmarkCommentBuilder = new StringBuilder ( ) ;
85+ var sourceHighlightText = string . Empty ;
86+
87+ foreach ( var entry in bookmarkEntries . Where ( entry => CheckHighlightEntryMatch ( entry , line ) ) )
88+ {
89+ setBookmark = true ;
90+ sourceHighlightText = entry . SearchText ;
91+
92+ if ( ! string . IsNullOrEmpty ( entry . BookmarkComment ) )
93+ {
94+ _ = bookmarkCommentBuilder . Append ( entry . BookmarkComment ) . Append ( "\r \n " ) ;
95+ }
96+ }
97+
98+ return ( setBookmark , bookmarkCommentBuilder . ToString ( ) . TrimEnd ( '\r ' , '\n ' ) , sourceHighlightText ) ;
99+ }
100+
101+ /// <summary>
102+ /// Matches a highlight entry against a line. Replicates the logic from LogWindow.CheckHighlightEntryMatch so the
103+ /// scanner works identically to the existing tail-mode matching.
104+ /// </summary>
105+ private static bool CheckHighlightEntryMatch ( HighlightEntry entry , ITextValueMemory column )
106+ {
107+ if ( entry . IsRegex )
108+ {
109+ if ( entry . Regex . IsMatch ( column . Text . ToString ( ) ) )
110+ {
111+ return true ;
112+ }
113+ }
114+ else
115+ {
116+ if ( entry . IsCaseSensitive )
117+ {
118+ if ( column . Text . Span . Contains ( entry . SearchText . AsSpan ( ) , StringComparison . Ordinal ) )
119+ {
120+ return true ;
121+ }
122+ }
123+ else
124+ {
125+ if ( column . Text . Span . Contains ( entry . SearchText . AsSpan ( ) , StringComparison . OrdinalIgnoreCase ) )
126+ {
127+ return true ;
128+ }
129+ }
130+ }
131+
132+ return false ;
133+ }
134+
135+ /// <summary>
136+ /// Resolves the bookmark comment template using ParamParser, matching SetBookmarkFromTrigger behavior.
137+ /// </summary>
138+ private static string ResolveComment ( string commentTemplate , ILogLineMemory line , int lineNum , string fileName )
139+ {
140+ if ( string . IsNullOrEmpty ( commentTemplate ) )
141+ {
142+ return commentTemplate ;
143+ }
144+
145+ try
146+ {
147+ var paramParser = new ParamParser ( commentTemplate ) ;
148+ return paramParser . ReplaceParams ( line , lineNum , fileName ) ;
149+ }
150+ catch ( ArgumentException )
151+ {
152+ // Invalid regex in template — return raw template (matches SetBookmarkFromTrigger behavior)
153+ return commentTemplate ;
154+ }
155+ }
156+ }
0 commit comments