Desktop/Private/Traversal/Invoke-TreeTraversal.ps1

# src/Private/Traversal/Invoke-TreeTraversal.ps1

<#
.SYNOPSIS
    Streams tree traversal records for a path.
 
.DESCRIPTION
    Invoke-TreeTraversal is the internal recursive engine for ShowTree. It performs a
    depth-first search of the file system using a provided TreeChildProvider. It emits
    ShowTree.TreeRecord objects for every item and "gap" (structural separator) found.
 
    It handles recursion depth, visibility logic (via predicates), and layout state
    tracking (e.g., whether an ancestor was a last sibling).
 
.PARAMETER Path
    The path to traverse. Defaults to the current directory ('.'). Supports both relative and absolute paths.
    If the path represents a file, only that file will be emitted.
 
.PARAMETER Mode
    The formatting mode ('Normal', 'Tree', 'List'). This influence how layout metadata (gaps, connectors)
    is computed during traversal.
 
.PARAMETER Depth
    The maximum depth to traverse.
    - Use -1 for unlimited traversal.
    - Use 0 for the root item only.
    - Defaults to -1.
 
.PARAMETER ProviderMode
    The enumeration provider to use ('PowerShell' or 'Win32').
    'Win32' is significantly faster on Windows for deep trees but may exhibit different behavior for
    certain virtualized or specialized file types.
 
.PARAMETER GapPolicy
    The policy for rendering gap lines ('None', 'Tree', 'Show').
    - 'None': No gaps are emitted.
    - 'Show': Emits gap lines between logical groups (e.g., between a set of files and the next directory).
    - 'Tree': A legacy-compatible mode specifically for Tree.com behavior.
 
.PARAMETER Include
    An array of glob patterns to include. If specified, only items matching these patterns (or their
    ancestors required for structural integrity) will be emitted.
 
.PARAMETER Exclude
    An array of glob patterns to exclude. Items matching these patterns and their descendants will be
    pruned from the traversal.
 
.PARAMETER HideHidden
    If specified, hides items marked with the Hidden attribute (Windows) or dot-prefixed items (Unix).
 
.PARAMETER HideSystem
    If specified, hides items marked with the System attribute.
 
.PARAMETER DirectoryOnly
    If specified, only directories are included in the traversal output.
     
.PARAMETER FollowLinks
    If specified, the cmdlet will follow symbolic links and directory junctions during traversal.
#>

function Invoke-TreeTraversal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [ValidateSet('Normal', 'Tree', 'List')]
        [string] $Mode = 'Normal',
        
        [string] $RootPath,

        [int] $MaxDepth = -1,

        [int] $CurrentDepth = 0,

        [Parameter(Mandatory)]
        [object] $Provider,

        [ValidateSet('None', 'Tree', 'Show')]
        [string] $GapPolicy = 'Show',

        [bool[]] $AncestorIsLastSibling = @(),

        [bool] $HasNextSiblingAfterThisDirectory = $false,

        [string[]] $Include,
        [string[]] $Exclude,

        [switch] $HideHidden,
        [switch] $HideSystem,
        [switch] $DirectoryOnly,
        [switch] $FollowLinks
    )

    if (-not $PSBoundParameters.ContainsKey('Debug') -and $PSCmdlet)
    {
        $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
    }
    if (-not $PSBoundParameters.ContainsKey('Verbose') -and $PSCmdlet)
    {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    if ([string]::IsNullOrWhiteSpace($RootPath)) {
        $RootPath = $Path
    }

    $children = @(
        Get-TreeChild `
            -Path $Path `
            -RootPath $RootPath `
            -Depth $CurrentDepth `
            -Provider $Provider `
            -Include $Include `
            -Exclude $Exclude `
            -HideHidden:$HideHidden `
            -HideSystem:$HideSystem `
            -DirectoryOnly:$DirectoryOnly
    )

    $emittedVisibleChild = $false

    if ($children.Count -eq 0) {
        return
    }

    for ($i = 0; $i -lt $children.Count; $i++) {
        $child = $children[$i]
        $emittedVisibleChild = $true

        $isLastSibling = $i -eq ($children.Count - 1)
        $hasNextSibling = -not $isLastSibling

        $hasLaterSiblingDirectory = $false
        for ($j = $i + 1; $j -lt $children.Count; $j++) {
            if ($children[$j].IsContainer) {
                $hasLaterSiblingDirectory = $true
                break
            }
        }

        $layout = New-TreeLayout `
            -Depth $child.Depth `
            -RelativeDepth $child.Depth `
            -IsLastSibling:$isLastSibling `
            -AncestorIsLastSibling $AncestorIsLastSibling `
            -HasLaterSiblingDirectory:$hasLaterSiblingDirectory

        New-TreeRecord `
            -RecordType Item `
            -TreeItem $child `
            -TreeLayout $layout
        
        $script:lastRecordKind = $child.Kind

        # Sibling Gap Logic
        if (-not $child.IsContainer -and $hasNextSibling -and $children[$i + 1].IsContainer) {
            $fileToDirectoryGapLayout = New-TreeLayout `
                -Depth $CurrentDepth `
                -RelativeDepth $CurrentDepth `
                -AncestorIsLastSibling $AncestorIsLastSibling

            New-TreeRecord `
                -RecordType Gap `
                -TreeLayout $fileToDirectoryGapLayout
        }

        $shouldRecurse =
            $child.IsContainer -and
            ($MaxDepth -eq -1 -or $CurrentDepth -lt $MaxDepth) -and
            (Test-TreeItemRecurse `
                -Item $child `
                -Include $Include `
                -Exclude $Exclude `
                -RootPath $RootPath `
                -HideHidden:$HideHidden `
                -HideSystem:$HideSystem `
                -FollowLinks:$FollowLinks)

        if ($shouldRecurse) {
            $nextAncestorIsLastSibling = (New-Object -TypeName System.Collections.Generic.List[bool])

            foreach ($ancestorIsLast in $AncestorIsLastSibling) {
                [void] $nextAncestorIsLastSibling.Add([bool] $ancestorIsLast)
            }

            [void] $nextAncestorIsLastSibling.Add([bool] $isLastSibling)

            $recurseParams = @{
                Path                             = $child.FullPath
                Mode                             = $Mode
                RootPath                         = $RootPath
                MaxDepth                         = $MaxDepth
                CurrentDepth                     = $CurrentDepth + 1
                Provider                         = $Provider
                GapPolicy                        = $GapPolicy
                HasNextSiblingAfterThisDirectory = $hasNextSibling
                Include                          = $Include
                Exclude                          = $Exclude
                HideHidden                       = $HideHidden
                HideSystem                       = $HideSystem
                DirectoryOnly                    = $DirectoryOnly
                FollowLinks                      = $FollowLinks
            }

            $nextAncestorArray = $nextAncestorIsLastSibling.ToArray()
            if ($nextAncestorArray.Count -gt 0) {
                $recurseParams.AncestorIsLastSibling = $nextAncestorArray
            }

            Invoke-TreeTraversal @recurseParams
        }
    }

    # Only for Tree mode, if the last tree item was a directory and the next tree item is a directory,
    # then regardless of how close they are on the tree, suppress the gap.
    $supressGap = $Mode -eq 'Tree' -and $GapPolicy -eq 'Tree' -and $script:lastRecordKind -ne 'File'
    if ($emittedVisibleChild -and $HasNextSiblingAfterThisDirectory -and -not $supressGap) {
        $gapAncestorIsLastSibling = (New-Object -TypeName System.Collections.Generic.List[bool])

        if ($AncestorIsLastSibling.Count -gt 1) {
            for ($i = 0; $i -lt ($AncestorIsLastSibling.Count - 1); $i++) {
                [void] $gapAncestorIsLastSibling.Add([bool] $AncestorIsLastSibling[$i])
            }
        }

        $gapDepth = (&{if(($CurrentDepth -gt 0)){($CurrentDepth - 1)}else{0}})

        $gapLayoutParams = @{
            Depth          = $gapDepth
            RelativeDepth  = $gapDepth
        }

        $gapAncestorArray = $gapAncestorIsLastSibling.ToArray()
        if ($gapAncestorArray.Count -gt 0) {
            $gapLayoutParams.AncestorIsLastSibling = $gapAncestorArray
        }

        $gapLayout = New-TreeLayout @gapLayoutParams

        New-TreeRecord `
            -RecordType Gap `
            -TreeLayout $gapLayout
    }
}