argument_completer.ps1

if ($PSEdition -eq "Desktop") {
    try { $IsWindows = $true } catch {}
}

# 获取具有补全脚本的 Part 列表。
function Get-FountPartListWithCompleter {
    param([string]$Username, [string]$PartPath)
    # 获取指定用户或所有用户的可用 Part 列表。
    Get-FountPartList -PartPath $PartPath -Username $Username |
    # 筛选出存在 argument_completer.ps1 脚本的 Part。
    Where-Object {
        $partDir = Get-FountPartDirectory -Username $Username -PartPath "$PartPath/$_"
        Test-Path (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") -PathType Leaf
    }
}

# `run` 不带用户名时服务器会挑最后活跃的用户,补全得算出同一个人才能找到他独占的 part。
# 判定与 fount 的 getLastActiveUsername 一致:lastActiveAt 优先,回退 createdAt,全无记录时按用户目录 mtime 兜底。
function Get-FountLastActiveUsername {
    $fountDir = Get-FountDirectory
    try {
        $users = (Get-Content -LiteralPath "$fountDir/data/config.json" -Raw | ConvertFrom-Json).data.users.PSObject.Properties
        $latestName = $null
        $latestAt = 0
        foreach ($user in $users) {
            $activityAt = if ($user.Value.lastActiveAt) { [double]$user.Value.lastActiveAt } else { [double]$user.Value.createdAt }
            if ($activityAt -gt $latestAt) {
                $latestAt = $activityAt
                $latestName = $user.Name
            }
        }
        if ($latestName) { return $latestName }
    }
    catch { }
    Get-ChildItem -LiteralPath "$fountDir/data/users" -Directory |
        Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 -ExpandProperty Name
}

# 补全 partpath:逐级合并用户 part 与公共 part 里同名的子目录。
function Get-FountPartPathCompletion {
    param([string]$Username, [string]$WordToComplete)
    $slash = $WordToComplete.LastIndexOf('/')
    $parentPath = if ($slash -ge 0) { $WordToComplete.Substring(0, $slash) } else { '' }
    $prefix = $WordToComplete.Substring($slash + 1)
    $fountDir = Get-FountDirectory
    $searchRoots = @("$fountDir/src/public/parts/$parentPath")
    if ($Username) { $searchRoots = @("$fountDir/data/users/$Username/$parentPath") + $searchRoots }

    $candidates = $searchRoots |
        ForEach-Object { Get-ChildItem -LiteralPath $_ -Directory -ErrorAction SilentlyContinue } |
        Where-Object { $_.Name.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) } |
        Group-Object Name | Sort-Object Name

    foreach ($candidate in $candidates) {
        $path = if ($parentPath) { "$parentPath/$($candidate.Name)" } else { $candidate.Name }
        # 目录自己是 part 就补完,只在后代里还有 part 时补成能继续输入的路径。
        $isPart = $false
        $hasPartBelow = $false
        foreach ($directory in $candidate.Group) {
            if (Test-Path -LiteralPath (Join-Path $directory.FullName 'fount.json') -PathType Leaf) { $isPart = $true; break }
            if (Get-ChildItem -LiteralPath $directory.FullName -Filter fount.json -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1) { $hasPartBelow = $true }
        }
        if ($isPart) {
            [System.Management.Automation.CompletionResult]::new($path, $candidate.Name, 'ParameterValue', $path)
        }
        elseif ($hasPartBelow) {
            [System.Management.Automation.CompletionResult]::new("$path/", "$($candidate.Name)/", 'ParameterValue', "$path/")
        }
    }
}

# 移除哈希表中的指定键及其子键(递归)。
function Remove-ArgumentNode {
    param([hashtable]$Node, [string]$Key)
    # 如果哈希表中存在指定的键,则移除该键。
    if ($Node.ContainsKey($Key)) { $Node.Remove($Key) }
    # 遍历哈希表中的所有键。
    foreach ($mapperkey in $Node.Keys) {
        # 如果当前键对应的值是哈希表,则递归调用 Remove-ArgumentNode 函数。
        if ($Node[$mapperkey] -is [hashtable]) { Remove-ArgumentNode -Node $Node[$mapperkey] -Key $Key }
    }
}

# 处理 'run' 和 'runas' 命令的参数补全。
function Invoke-RunshellCompletion {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition,
        [int]$RunIndex, # 'run' 或 'runas' 在 CommandAst 中的索引。
        [int]$ArgIndex  # 光标所在参数在 CommandAst 中的索引。
    )

    $commandElements = $CommandAst.CommandElements
    # 相对 run/runas 的参数序号:runas 的 1 是用户名、2 是 partpath,run 的 1 就是 partpath。
    $argumentOffset = $ArgIndex - $RunIndex
    if ($argumentOffset -lt 1) { return }
    $hasExplicitUsername = $commandElements[$RunIndex].Value -eq 'runas'
    if ($hasExplicitUsername -and $argumentOffset -eq 1) {
        return Get-FountUserList | Where-Object { $_.StartsWith($WordToComplete) }
    }
    $partPathOffset = if ($hasExplicitUsername) { 2 } else { 1 }
    # `run` 的用户名由服务器挑,这里替他挑同一个,省得后面再分 run 和 runas。
    $username = if ($hasExplicitUsername) { $commandElements[$RunIndex + 1].Value } else { Get-FountLastActiveUsername }
    if ($argumentOffset -eq $partPathOffset) {
        return Get-FountPartPathCompletion -Username $username -WordToComplete $WordToComplete
    }

    # 更靠后的参数委托给 part 自己的 argument_completer.ps1。
    $partPath = $commandElements[$RunIndex + $partPathOffset].Value
    if (-not $partPath) { return }
    $partDir = Get-FountPartDirectory -Username $username -PartPath $partPath
    if ($partDir -and (Test-Path (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") -PathType Leaf)) {
        & (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") $username $WordToComplete $CommandAst $CursorPosition $RunIndex $ArgIndex
    }
}

# fount 的命令表:path/src/cmd 下的 CLI 子命令(default 与 protocolhandle 不用手输),加上由服务器解析的 run/runas。
$FountCommands = 'background', 'clean', 'debug', 'eval', 'geneexe', 'init', 'keepalive', 'log', 'logo', 'nop', 'open', 'reboot', 'remove', 'run', 'runas', 'server', 'shutdown', 'test', 'update', 'version'

# geneexe 补全当前目录下的文件(只有 Windows 有 geneexe 命令)。
$GeneexeCompleter = { Get-ChildItem -Path "$($WordToComplete)*" -File | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'File', $_.FullName) } }

# keepalive 与 debug 都只是把后面的参数转给服务器,run/runas 之后再交给 part 的补全脚本。
$DebugNode = @{
    Parameters = 'run', 'runas'
    run        = ${function:Invoke-RunshellCompletion}
    runas      = ${function:Invoke-RunshellCompletion}
}
$KeepaliveNode = @{
    Parameters = 'debug', 'run', 'runas'
    debug      = $DebugNode
    run        = ${function:Invoke-RunshellCompletion}
    runas      = ${function:Invoke-RunshellCompletion}
}

# 定义 fount 工具的参数结构。
$ArgumentStructure = @{
    Root = @{
        # 根节点包含的参数。
        Parameters = $FountCommands
        # 'background' 把剩下的参数交给新的 fount 进程,但不能再套一层 background。
        background = @{
            Parameters = $FountCommands -ne 'background'
            geneexe    = $GeneexeCompleter
            init       = $null  # 没有补全逻辑。
            keepalive  = $KeepaliveNode
            debug      = $DebugNode
            run        = ${function:Invoke-RunshellCompletion}
            runas      = ${function:Invoke-RunshellCompletion}
        }
        geneexe    = $GeneexeCompleter
        init       = $null
        keepalive  = $KeepaliveNode
        debug      = $DebugNode
        run        = ${function:Invoke-RunshellCompletion}
        runas      = ${function:Invoke-RunshellCompletion}
    }
}

# 如果不是 Windows 系统,则移除 'geneexe' 参数。
if (-not $IsWindows) {
    Remove-ArgumentNode -Node $ArgumentStructure.Root -Key 'geneexe'
}

# fount 工具的主参数补全函数。
function Get-FountArgumentCompletion {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    try {
        # 用 AST 元素范围算出光标落在第几个参数上,引号、空格、光标停在参数中间都算得对。
        $commandElements = $CommandAst.CommandElements
        $ArgIndex = $commandElements.Count
        foreach ($index in 0..($commandElements.Count - 1)) {
            if ($CursorPosition -le $commandElements[$index].Extent.EndOffset) {
                $ArgIndex = $index
                break
            }
            if ($index + 1 -lt $commandElements.Count -and $CursorPosition -lt $commandElements[$index + 1].Extent.StartOffset) {
                $ArgIndex = $index + 1
                break
            }
        }

        # 从参数结构的根节点开始,只走光标之前的元素,免得光标后面的参数改了当前层级。
        $currentLevel = $ArgumentStructure.Root
        $runIndex = -1
        for ($elementIndex = 1; $elementIndex -lt $ArgIndex; $elementIndex++) {
            $element = $commandElements[$elementIndex]
            if ($element -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) { break }
            if ($currentLevel -isnot [hashtable]) { break }
            if ($currentLevel.ContainsKey($element.Value)) {
                $currentLevel = $currentLevel[$element.Value]
                # 走到处理脚本就把它后面的参数交给脚本自己补。
                if ($currentLevel -is [scriptblock]) {
                    $runIndex = $elementIndex
                    break
                }
            }
            elseif ($currentLevel.Parameters -notcontains $element.Value) { break }
        }

        # 处理脚本负责自己的参数补齐。
        if ($currentLevel -is [scriptblock]) {
            return & $currentLevel $WordToComplete $CommandAst $CursorPosition $runIndex $ArgIndex
        }
        # 还停在某个命令节点上时,补全它接受的参数;叶子命令(节点值是 $null)没有可补的。
        if ($currentLevel -is [hashtable] -and $currentLevel.Parameters) {
            $currentLevel.Parameters | Where-Object { $_.StartsWith($WordToComplete) }
        }
    }
    catch {
        Write-Host "Error in Get-FountArgumentCompletion: $_" -ForegroundColor Red
    }
}

# 注册参数补全函数。
# 为 'fount'、'fount.ps1'、'fount.cmd' 和 'fount.exe' 命令注册 Get-FountArgumentCompletion 函数。
Register-ArgumentCompleter -CommandName fount -ScriptBlock ${function:Get-FountArgumentCompletion} -Native
Register-ArgumentCompleter -CommandName fount.ps1 -ScriptBlock ${function:Get-FountArgumentCompletion} -Native
Register-ArgumentCompleter -CommandName fount.cmd -ScriptBlock ${function:Get-FountArgumentCompletion} -Native
Register-ArgumentCompleter -CommandName fount.exe -ScriptBlock ${function:Get-FountArgumentCompletion} -Native