private/output/Find-ConsoleText.ps1
|
function Find-ConsoleText { <# .SYNOPSIS Performs text search with highlighting in a RichTextBox. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Windows.Controls.RichTextBox]$RichTextBox, [Parameter(Mandatory)] [AllowEmptyString()] [string]$SearchText, [int]$MaxMatches = 200 ) $state = $RichTextBox.Tag if (!$state) { return } # Clear previous highlights foreach ($prevRange in $state.Matches) { try { $prevRange.ApplyPropertyValue([System.Windows.Documents.TextElement]::BackgroundProperty, $state.ResetBrush) } catch { Write-Debug "Highlight reset failed: $_" } } $state.Matches.Clear() if ([string]::IsNullOrEmpty($SearchText)) { return } # Get document bounds $docStart = $RichTextBox.Document.ContentStart $docEnd = $RichTextBox.Document.ContentEnd # Get full text and find all match positions $fullRange = [System.Windows.Documents.TextRange]::new($docStart, $docEnd) $fullText = $fullRange.Text # Find all match indices $matchIndices = [System.Collections.Generic.List[int]]::new() $searchIdx = 0 while ($matchIndices.Count -lt $MaxMatches) { $idx = $fullText.IndexOf($SearchText, $searchIdx, [StringComparison]::OrdinalIgnoreCase) if ($idx -lt 0) { break } [void]$matchIndices.Add($idx) $searchIdx = $idx + 1 } # Convert text indices to TextPointers and highlight # $fullText includes \r\n for LineBreaks, but GetTextInRun doesn't see them. # We must count LineBreak elements as 2 characters (\r\n) to stay in sync. foreach ($textIdx in $matchIndices) { $ptr = $docStart $charCount = 0 $startPtr = $null $endPtr = $null while ($null -ne $ptr -and $ptr.CompareTo($docEnd) -lt 0) { $ctx = $ptr.GetPointerContext([System.Windows.Documents.LogicalDirection]::Forward) if ($ctx -eq [System.Windows.Documents.TextPointerContext]::Text) { $runText = $ptr.GetTextInRun([System.Windows.Documents.LogicalDirection]::Forward) $runLen = $runText.Length # Match starts somewhere in this run if ($null -eq $startPtr -and $charCount + $runLen -gt $textIdx) { $offsetInRun = $textIdx - $charCount $startPtr = $ptr.GetPositionAtOffset($offsetInRun) } # Match ends somewhere in this run $matchEnd = $textIdx + $SearchText.Length if ($null -ne $startPtr -and $null -eq $endPtr -and $charCount + $runLen -ge $matchEnd) { $offsetInRun = $matchEnd - $charCount $endPtr = $ptr.GetPositionAtOffset($offsetInRun) break } $charCount += $runLen } elseif ($ctx -eq [System.Windows.Documents.TextPointerContext]::ElementEnd) { # LineBreaks count as \r\n (2 chars) in TextRange.Text $adjacent = $ptr.GetAdjacentElement([System.Windows.Documents.LogicalDirection]::Forward) if ($adjacent -is [System.Windows.Documents.LineBreak]) { $charCount += 2 } } $ptr = $ptr.GetNextContextPosition([System.Windows.Documents.LogicalDirection]::Forward) } if ($startPtr -and $endPtr) { $range = [System.Windows.Documents.TextRange]::new($startPtr, $endPtr) $range.ApplyPropertyValue([System.Windows.Documents.TextElement]::BackgroundProperty, $state.HighlightBrush) [void]$state.Matches.Add($range) } } # Scroll to first match if ($state.Matches.Count -gt 0) { try { $rect = $state.Matches[0].Start.GetCharacterRect([System.Windows.Documents.LogicalDirection]::Forward) $RichTextBox.ScrollToVerticalOffset($RichTextBox.VerticalOffset + $rect.Top - 50) } catch { Write-Debug "Suppressed scroll to match error: $_" } } } |