completers/go_completer/go_completer.ps1

Set-StrictMode -Version 2.0

function Get-GoCompletionCache {
    $existing = Get-Variable -Name GoCompletionCache -Scope Script -ErrorAction Ignore
    if ($existing) {
        return $existing.Value
    }

    $cache = @{
        ExecutableResolved = $false
        ExecutablePath     = $null
        TextByKey          = @{}
        HelpModels         = @{}
        ToolNames          = $null
        EnvNames           = $null
        WorkingDirectory   = $null
    }

    Set-Variable -Name GoCompletionCache -Scope Script -Value $cache
    $cache
}

function New-GoCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ListItemText = $CompletionText,
        [System.Management.Automation.CompletionResultType]$ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue,
        [string]$ToolTip = $CompletionText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $CompletionText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function Get-GoRootCommandMetadata {
    [ordered]@{
        'bug'       = 'start a bug report'
        'help'      = 'show help for commands and topics'
        'build'     = 'compile packages and dependencies'
        'clean'     = 'remove object files and cached files'
        'doc'       = 'show documentation for package or symbol'
        'env'       = 'print Go environment information'
        'fix'       = 'apply fixes suggested by static checkers'
        'fmt'       = 'gofmt (reformat) package sources'
        'generate'  = 'generate Go files by processing source'
        'get'       = 'add dependencies to current module and install them'
        'install'   = 'compile and install packages and dependencies'
        'list'      = 'list packages or modules'
        'mod'       = 'module maintenance'
        'work'      = 'workspace maintenance'
        'run'       = 'compile and run Go program'
        'telemetry' = 'manage telemetry data and settings'
        'test'      = 'test packages'
        'tool'      = 'run specified go tool'
        'version'   = 'print Go version'
        'vet'       = 'report likely mistakes in packages'
    }
}

function Get-GoHelpTopicMetadata {
    [ordered]@{
        'buildconstraint' = 'build constraints'
        'buildjson'       = 'build -json encoding'
        'buildmode'       = 'build modes'
        'c'               = 'calling between Go and C'
        'cache'           = 'build and test caching'
        'environment'     = 'environment variables'
        'filetype'        = 'file types'
        'goauth'          = 'GOAUTH environment variable'
        'go.mod'          = 'the go.mod file'
        'gopath'          = 'GOPATH environment variable'
        'goproxy'         = 'module proxy protocol'
        'importpath'      = 'import path syntax'
        'modules'         = 'modules, module versions, and more'
        'module-auth'     = 'module authentication using go.sum'
        'packages'        = 'package lists and patterns'
        'private'         = 'configuration for downloading non-public code'
        'testflag'        = 'testing flags'
        'testfunc'        = 'testing functions'
        'vcs'             = 'controlling version control with GOVCS'
    }
}

function Get-GoFallbackToolNames {
    @(
        'asm', 'cgo', 'compile', 'covdata', 'cover', 'doc', 'fix', 'link',
        'nm', 'objdump', 'pack', 'pprof', 'preprofile', 'test2json', 'trace', 'vet'
    )
}

function Resolve-GoExecutablePath {
    $cache = Get-GoCompletionCache
    if ($cache.ExecutableResolved) {
        return $cache.ExecutablePath
    }

    $cache.ExecutableResolved = $true

    $command = Get-Command -Name go.exe -ErrorAction Ignore | Select-Object -First 1
    if (-not $command) {
        $command = Get-Command -Name go -ErrorAction Ignore | Select-Object -First 1
    }
    if ($command) {
        $cache.ExecutablePath = if ($command.Path) {
            $command.Path
        } elseif ($command.Source) {
            $command.Source
        } else {
            $command.Name
        }

        return $cache.ExecutablePath
    }

    $fallbackPath = 'C:\Program Files\Go\bin\go.exe'
    if (Test-Path -LiteralPath $fallbackPath) {
        $cache.ExecutablePath = $fallbackPath
    }

    $cache.ExecutablePath
}

function Reset-GoCacheForLocation {
    # go tool output is module-scoped, so a cache built in one module must not be
    # reused in another.
    $cache = Get-GoCompletionCache
    $location = (Get-Location).Path
    if ($cache.WorkingDirectory -ne $location) {
        $cache.WorkingDirectory = $location
        $cache.TextByKey = @{}
        $cache.HelpModels = @{}
        $cache.ToolNames = $null
        $cache.EnvNames = $null
    }
}

function Get-GoText {
    param(
        [string]$CacheKey,
        [string[]]$Arguments
    )

    $cache = Get-GoCompletionCache
    if ($cache.TextByKey.ContainsKey($CacheKey)) {
        return $cache.TextByKey[$CacheKey]
    }

    $goPath = Resolve-GoExecutablePath
    if ([string]::IsNullOrWhiteSpace($goPath)) {
        $cache.TextByKey[$CacheKey] = ''
        return ''
    }

    try {
        # Standard input is closed so a tool that reads it cannot hang the prompt.
        $text = ($null | & $goPath @Arguments 2>&1 | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' } | Out-String -Width 4096)
    } catch {
        $text = ''
    }

    if ($null -eq $text) {
        $text = ''
    }

    $cache.TextByKey[$CacheKey] = $text
    $text
}

function Get-GoBuildModeValues {
    $known = @('archive', 'c-archive', 'c-shared', 'default', 'shared', 'exe', 'pie', 'plugin')
    $text = Get-GoText -CacheKey 'help:buildmode' -Arguments @('help', 'buildmode')
    if ([string]::IsNullOrWhiteSpace($text)) {
        return $known
    }

    $found = @($known | Where-Object { [regex]::IsMatch($text, "(?<![A-Za-z0-9_.-])$([regex]::Escape($_))(?![A-Za-z0-9_.-])") })
    if ($found.Count -eq 0) {
        return $known
    }

    $found
}

function Get-GoEnumValues {
    param([string]$OptionName)

    # go documents these value sets in prose rather than in a machine-readable
    # shape, so they stay explicit; the audit confirmed each one matches go1.27.
    switch ($OptionName) {
        '-buildmode' { return Get-GoBuildModeValues }
        '-buildvcs'  { return @('auto', 'true', 'false') }
        '-compiler'  { return @('gc', 'gccgo') }
        '-covermode' { return @('set', 'count', 'atomic') }
        '-mod'       { return @('readonly', 'vendor', 'mod') }
    }

    return ,@()
}

function Add-GoHelpFlag {
    param(
        [System.Collections.Specialized.OrderedDictionary]$Flags,
        [string]$Name,
        [string]$ValueName,
        [bool]$Authoritative
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return
    }

    # A value spec that is not a plain identifier (a quoted list, a bracketed
    # pattern) is still a value, but its name is not worth showing.
    if (-not [string]::IsNullOrWhiteSpace($ValueName) -and $ValueName -notmatch '^[A-Za-z][A-Za-z0-9_.,-]*$') {
        $ValueName = 'value'
    }

    $values = @(Get-GoEnumValues -OptionName $Name)

    if ($Flags.Contains($Name)) {
        if (-not $Authoritative) {
            return
        }

        $existing = $Flags[$Name]
        if ([string]::IsNullOrWhiteSpace($existing.ValueName) -and -not [string]::IsNullOrWhiteSpace($ValueName)) {
            $existing.ValueName = $ValueName
            $existing.TakesValue = $true
        }

        if (@($existing.Values).Count -eq 0 -and $values.Count -gt 0) {
            $existing.Values = $values
        }

        return
    }

    $Flags[$Name] = [pscustomobject]@{
        Name       = $Name
        TakesValue = (-not [string]::IsNullOrWhiteSpace($ValueName))
        ValueName  = $ValueName
        Values     = $values
    }
}

function ConvertFrom-GoHelpText {
    param(
        [string]$Text,
        [int]$CommandDepth = 0
    )

    $flags = [ordered]@{}
    $commands = [ordered]@{}
    $topics = [ordered]@{}
    $operands = New-Object System.Collections.Generic.List[string]
    $choices = New-Object System.Collections.Generic.List[string]
    $usage = ''
    $includesBuildFlags = $false

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return [pscustomobject]@{
            Usage              = $usage
            IncludesBuildFlags = $includesBuildFlags
            Flags              = $flags
            Commands           = $commands
            Topics             = $topics
            Operands           = @()
            Choices            = @()
        }
    }

    $lines = @([regex]::Split($Text, '\r?\n'))
    $section = ''

    foreach ($line in $lines) {
        if ($line -match '^usage:\s+go\s+(?<rest>.*)$' -and [string]::IsNullOrEmpty($usage)) {
            $usage = $Matches.rest
            continue
        }

        if ($line -match '^The commands are:\s*$' -or $line -match '^The subcommands are:\s*$') {
            $section = 'Commands'
            continue
        }

        if ($line -match '^Additional help topics:\s*$') {
            $section = 'Topics'
            continue
        }

        if ($line -match '^\S' -and $line -notmatch '^usage:') {
            $section = ''
        }

        $trimmed = $line.Trim()
        if ([string]::IsNullOrWhiteSpace($trimmed)) {
            continue
        }

        if ($section -eq 'Commands' -or $section -eq 'Topics') {
            # The gap collapses to a single space when the name is the longest in
            # the table, as "buildconstraint" is under Additional help topics.
            if ($line -match '^\s+(?<name>[a-z][a-z0-9._-]*)\s+(?<description>\S.*)$') {
                if ($section -eq 'Commands') {
                    $commands[$Matches.name] = $Matches.description.Trim()
                } else {
                    $topics[$Matches.name] = $Matches.description.Trim()
                }
            }

            continue
        }

        # An indented flag definition ("\t-a", "\t-p n", "\t-ldflags '[pattern=]arg
        # list'"). go indents these by one tab or two spaces and indents their
        # description lines further, which is what separates them from prose.
        if ($line -match '^[ \t]{1,3}-{1,2}(?<name>[A-Za-z][A-Za-z0-9_-]*)(?<rest>([ =].*)?)$') {
            $flagName = '-' + $Matches.name
            $flagValue = $Matches.rest.Trim().TrimStart('=').Trim()
            if ($flagValue -match '^(?<first>\S+)') {
                $flagValue = $Matches.first
            }

            Add-GoHelpFlag -Flags $flags -Name $flagName -ValueName $flagValue -Authoritative $true
        }
    }

    if (-not [string]::IsNullOrEmpty($usage)) {
        $includesBuildFlags = $usage -match '\[build(?:/test)? flags'

        foreach ($group in [regex]::Matches($usage, '\[(?<body>[^\]]+)\]')) {
            $body = $group.Groups['body'].Value
            if ($body -notmatch '(?<!\S)-') {
                if ($body -match '^\s*flags?\s*$' -or $body -match 'flags\s*$') {
                    continue
                }

                $alternatives = @($body -split '\s*\|\s*' | ForEach-Object { $_.Trim() })
                if ($alternatives.Count -gt 1 -and @($alternatives | Where-Object { $_ -match '^[a-z][a-z0-9._-]*$' }).Count -eq $alternatives.Count) {
                    # A literal alternation such as "go telemetry [off|local|on]".
                    foreach ($choice in $alternatives) {
                        [void]$choices.Add($choice)
                    }

                    continue
                }

                foreach ($operand in $alternatives) {
                    [void]$operands.Add($operand)
                }

                continue
            }

            foreach ($alternative in ($body -split '\s*\|\s*')) {
                if ($alternative -notmatch '^\s*-{1,2}(?<name>[A-Za-z][A-Za-z0-9_-]*)(?<rest>.*)$') {
                    continue
                }

                $flagName = '-' + $Matches.name
                $rest = $Matches.rest.Trim().TrimStart('=').Trim()
                $valueName = ''
                if ($rest -match '^(?<value>\S+)') {
                    $valueName = $Matches.value
                }

                Add-GoHelpFlag -Flags $flags -Name $flagName -ValueName $valueName -Authoritative $false
            }
        }

        # Bare operands outside brackets, minus the command path itself:
        # "go run [build flags] [-exec xprog] package [arguments...]".
        $bare = @([regex]::Replace($usage, '\[[^\]]*\]', ' ') -split '\s+' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        foreach ($token in ($bare | Select-Object -Skip $CommandDepth)) {
            if ($token -notmatch '^-') {
                [void]$operands.Add($token)
            }
        }
    }

    # Prose: "The -n flag prints commands that would be executed." and lists such
    # as "The -require=path@version and -droprequire=path flags", where every
    # option in the list is a flag, not only the one next to the word.
    $proseOption = '-{1,2}[a-z][a-z0-9_-]*(?:=\S+?)?'
    $proseSeparator = '(?:,\s+(?:and\s+|or\s+)?|\s+(?:and|or)\s+)'
    foreach ($list in [regex]::Matches($Text, "(?<!\S)(?<list>$proseOption(?:$proseSeparator$proseOption)*)\s+flag")) {
        foreach ($group in [regex]::Matches($list.Groups['list'].Value, '(?<!\S)-{1,2}(?<name>[a-z][a-z0-9_-]*)(?:=(?<value>\S+?))?(?=,?(?:\s|$))')) {
            $valueName = if ($group.Groups['value'].Success) { $group.Groups['value'].Value } else { '' }
            Add-GoHelpFlag -Flags $flags -Name ('-' + $group.Groups['name'].Value) -ValueName $valueName -Authoritative $false
        }
    }

    [pscustomobject]@{
        Usage              = $usage
        IncludesBuildFlags = $includesBuildFlags
        Flags              = $flags
        Commands           = $commands
        Topics             = $topics
        Operands           = @($operands.ToArray())
        Choices            = @($choices.ToArray())
    }
}

function Get-GoHelpModel {
    param([string[]]$Path)

    $cache = Get-GoCompletionCache
    $segments = @(@($Path) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    $key = if ($segments.Count -gt 0) { $segments -join ' ' } else { '<root>' }

    if ($cache.HelpModels.ContainsKey($key)) {
        return $cache.HelpModels[$key]
    }

    $text = Get-GoText -CacheKey "help:$key" -Arguments (@('help') + $segments)
    if ($segments.Count -eq 1 -and $segments[0] -eq 'test') {
        # go test's own flags live in 'go help testflag', not in 'go help test'.
        $text += "`n" + (Get-GoText -CacheKey 'help:testflag' -Arguments @('help', 'testflag'))
    }

    $model = ConvertFrom-GoHelpText -Text $text -CommandDepth $segments.Count
    $cache.HelpModels[$key] = $model
    $model
}

function Get-GoRootCommands {
    $model = Get-GoHelpModel -Path @()
    $names = New-Object System.Collections.Generic.List[string]
    foreach ($name in @($model.Commands.Keys)) {
        [void]$names.Add($name)
    }

    # 'go help' does not list 'help' itself.
    foreach ($name in @((Get-GoRootCommandMetadata).Keys)) {
        if (-not $names.Contains($name)) {
            [void]$names.Add($name)
        }
    }

    @($names.ToArray())
}

function Get-GoHelpTopics {
    $model = Get-GoHelpModel -Path @()
    $names = @($model.Topics.Keys)
    if ($names.Count -gt 0) {
        return $names
    }

    @((Get-GoHelpTopicMetadata).Keys)
}

function Get-GoCommandDescription {
    param([string]$Name)

    $model = Get-GoHelpModel -Path @()
    if ($model.Commands.Contains($Name)) {
        return $model.Commands[$Name]
    }

    $metadata = Get-GoRootCommandMetadata
    if ($metadata.Contains($Name)) {
        return $metadata[$Name]
    }

    "go $Name"
}

function Get-GoNestedCommands {
    param([string]$CommandName)

    @((Get-GoHelpModel -Path @($CommandName)).Commands.Keys)
}

function Get-GoNestedCommandDescription {
    param(
        [string]$CommandName,
        [string]$Name
    )

    $model = Get-GoHelpModel -Path @($CommandName)
    if ($model.Commands.Contains($Name)) {
        return $model.Commands[$Name]
    }

    "go $CommandName $Name"
}

function Get-GoBuildFlagModel {
    (Get-GoHelpModel -Path @('build')).Flags
}

function Get-GoToolNames {
    $cache = Get-GoCompletionCache
    if ($null -ne $cache.ToolNames) {
        return $cache.ToolNames
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($line in ([regex]::Split((Get-GoText -CacheKey 'tool' -Arguments @('tool')), '\r?\n'))) {
        $candidate = $line.Trim()
        if ($candidate -match '^[a-z][a-z0-9./_-]*$') {
            [void]$names.Add($candidate)
        }
    }

    if ($names.Count -eq 0) {
        $cache.ToolNames = Get-GoFallbackToolNames
    } else {
        $cache.ToolNames = @($names.ToArray())
    }

    $cache.ToolNames
}

function Get-GoEnvNames {
    $cache = Get-GoCompletionCache
    if ($null -ne $cache.EnvNames) {
        return $cache.EnvNames
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($line in ([regex]::Split((Get-GoText -CacheKey 'env' -Arguments @('env')), '\r?\n'))) {
        if ($line -match '^\s*(?:set\s+)?(?<name>[A-Z][A-Z0-9_]*)=') {
            [void]$names.Add($Matches.name)
        }
    }

    if ($names.Count -eq 0) {
        $cache.EnvNames = @(
            'CGO_ENABLED', 'GO111MODULE', 'GOARCH', 'GOAUTH', 'GOBIN', 'GOCACHE', 'GOCOVERDIR',
            'GOENV', 'GOEXE', 'GOFLAGS', 'GOHOSTARCH', 'GOHOSTOS', 'GOINSECURE', 'GOMOD',
            'GOMODCACHE', 'GONOPROXY', 'GONOSUMDB', 'GOOS', 'GOPATH', 'GOPRIVATE', 'GOPROXY',
            'GOROOT', 'GOSUMDB', 'GOTELEMETRY', 'GOTELEMETRYDIR', 'GOTOOLCHAIN', 'GOWORK'
        )
    } else {
        $cache.EnvNames = @($names.ToArray() | Sort-Object -Unique)
    }

    $cache.EnvNames
}

function Get-GoResolvedFlagModel {
    param([string[]]$Path)

    $model = Get-GoHelpModel -Path $Path
    $merged = [ordered]@{}

    foreach ($name in @($model.Flags.Keys)) {
        $merged[$name] = $model.Flags[$name]
    }

    if ($model.IncludesBuildFlags) {
        $buildFlags = Get-GoBuildFlagModel
        foreach ($name in @($buildFlags.Keys)) {
            # -C must be the first argument on the command line, so it belongs to
            # the root position rather than to a command's own flag set.
            if ($name -eq '-C' -or $merged.Contains($name)) {
                continue
            }

            $merged[$name] = $buildFlags[$name]
        }
    }

    $merged
}

function Get-GoOptionBaseName {
    param([string]$Token)

    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $null
    }

    # go's flag package accepts -flag and --flag interchangeably.
    $normalized = if ($Token -match '^--[^-]') { $Token.Substring(1) } else { $Token }

    if ($normalized -match '^(?<name>-[^=]+)=') {
        return $Matches['name']
    }

    $normalized
}

function Get-GoValuePlaceholder {
    param(
        [string]$OptionName,
        [object]$FlagSpec
    )

    if ($null -ne $FlagSpec -and -not [string]::IsNullOrWhiteSpace($FlagSpec.ValueName)) {
        return '<' + $FlagSpec.ValueName + '>'
    }

    switch ($OptionName) {
        '-exec'    { return '<command>' }
        '-timeout' { return '<duration>' }
        default    { return '<value>' }
    }
}

function Get-GoQuoteCharacter {
    param([string]$InputText)

    if ([string]::IsNullOrEmpty($InputText)) {
        return $null
    }

    if ($InputText.StartsWith("'", [System.StringComparison]::Ordinal)) {
        return "'"
    }

    if ($InputText.StartsWith('"', [System.StringComparison]::Ordinal)) {
        return '"'
    }

    $null
}

function Remove-GoOuterQuotes {
    param([string]$InputText)

    if ([string]::IsNullOrEmpty($InputText)) {
        return ''
    }

    $quoteCharacter = Get-GoQuoteCharacter -InputText $InputText
    if ($null -eq $quoteCharacter) {
        return $InputText
    }

    $unquoted = $InputText.Substring(1)
    if ($unquoted.EndsWith($quoteCharacter, [System.StringComparison]::Ordinal)) {
        $unquoted = $unquoted.Substring(0, $unquoted.Length - 1)
    }

    if ($quoteCharacter -eq "'") {
        return $unquoted.Replace("''", "'")
    }

    $unquoted.Replace('`"', '"')
}

function ConvertTo-GoQuotedValue {
    param(
        [string]$Value,
        [string]$QuoteCharacter
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $Value
    }

    if ([string]::IsNullOrWhiteSpace($QuoteCharacter)) {
        if ($Value -notmatch '\s') {
            return $Value
        }

        $QuoteCharacter = '"'
    }

    if (($QuoteCharacter -eq "'") -and $Value.Contains("'")) {
        $QuoteCharacter = '"'
    }

    if ($QuoteCharacter -eq '"') {
        return '"' + $Value.Replace('`', '``').Replace('"', '`"') + '"'
    }

    "'" + $Value.Replace("'", "''") + "'"
}

function Get-GoPathCompletions {
    param(
        [string]$InputText,
        [string]$InlinePrefix = '',
        [bool]$DirectoryOnly = $false
    )

    $quoteCharacter = Get-GoQuoteCharacter -InputText $InputText
    $cleanInput = Remove-GoOuterQuotes -InputText $InputText

    $completions =
        [System.Management.Automation.CompletionCompleters]::CompleteFilename($cleanInput) |
        Where-Object {
            -not $DirectoryOnly -or
            $_.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer
        } |
        ForEach-Object {
            # CompleteFilename applies its own quoting; strip it before applying
            # the quoting style the user actually typed.
            $completionText = ConvertTo-GoQuotedValue -Value (Remove-GoOuterQuotes -InputText $_.CompletionText) -QuoteCharacter $quoteCharacter
            if ($InlinePrefix) {
                $completionText = $InlinePrefix + $completionText
            }

            New-GoCompletionResult -CompletionText $completionText -ListItemText $_.ListItemText -ResultType $_.ResultType -ToolTip $_.ToolTip
        }

    Write-Output -NoEnumerate @($completions)
}

function Split-GoCommandLine {
    param([string]$Text)

    # A quote-aware splitter. An unterminated quote runs to the end of the input,
    # which is exactly what happens while the user is still typing a quoted path
    # that contains a space.
    $tokens = New-Object System.Collections.Generic.List[string]
    if ([string]::IsNullOrEmpty($Text)) {
        return @()
    }

    $builder = New-Object System.Text.StringBuilder
    $started = $false
    $quote = [char]0

    for ($index = 0; $index -lt $Text.Length; $index++) {
        $character = $Text[$index]

        if ($quote -ne [char]0) {
            [void]$builder.Append($character)
            if ($character -eq $quote) {
                $quote = [char]0
            }

            continue
        }

        if ($character -eq '"' -or $character -eq "'") {
            $quote = $character
            $started = $true
            [void]$builder.Append($character)
            continue
        }

        if ([char]::IsWhiteSpace($character)) {
            if ($started) {
                [void]$tokens.Add($builder.ToString())
                [void]$builder.Clear()
                $started = $false
            }

            continue
        }

        $started = $true
        [void]$builder.Append($character)
    }

    if ($started) {
        [void]$tokens.Add($builder.ToString())
    }

    @($tokens.ToArray())
}

function Get-GoCommandState {
    param(
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition,
        [string]$FallbackWordToComplete
    )

    $line = if ($CommandAst.Extent -and $CommandAst.Extent.Text) {
        $CommandAst.Extent.Text
    } else {
        ''
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $line.Length)
    $prefix = $line.Substring(0, $safeCursor)
    $hasTrailingSpace = ($prefix -match '\s$') -or ($CommandAst.Extent -and $CursorPosition -gt $line.Length)
    $allTokens = @(Split-GoCommandLine -Text $prefix)

    if ($hasTrailingSpace) {
        $currentWord = ''
        $priorTokens = if ($allTokens.Count -gt 1) {
            @($allTokens | Select-Object -Skip 1)
        } else {
            @()
        }
    } else {
        $currentWord = if ($allTokens.Count -gt 0) {
            $allTokens[$allTokens.Count - 1]
        } else {
            $FallbackWordToComplete
        }

        $priorTokens = if ($allTokens.Count -gt 2) {
            @($allTokens | Select-Object -Skip 1 -First ($allTokens.Count - 2))
        } else {
            @()
        }
    }

    [pscustomobject]@{
        CurrentWord = $currentWord
        PriorTokens = $priorTokens
    }
}

function Test-GoOptionRequiresValue {
    param(
        [System.Collections.Specialized.OrderedDictionary]$FlagModel,
        [string]$OptionName
    )

    if ([string]::IsNullOrWhiteSpace($OptionName) -or $null -eq $FlagModel) {
        return $false
    }

    if (-not $FlagModel.Contains($OptionName)) {
        return $false
    }

    [bool]$FlagModel[$OptionName].TakesValue
}

function Get-GoCommandContext {
    param([string[]]$Tokens)

    $rootCommands = Get-GoRootCommands
    $pendingOption = $null
    $commandName = $null
    $subcommandName = $null
    $envMode = $null
    $testArgsSeen = $false
    $operandCount = 0
    $flagModel = $null

    foreach ($token in $Tokens) {
        if ($pendingOption) {
            $pendingOption = $null
            continue
        }

        $baseName = Get-GoOptionBaseName -Token $token

        if (-not $commandName) {
            if ($baseName -eq '-C') {
                if ($token -notlike '*=*') {
                    $pendingOption = '-C'
                }

                continue
            }

            if ($token.StartsWith('-', [System.StringComparison]::Ordinal)) {
                continue
            }

            if ($rootCommands -contains $token) {
                $commandName = $token
                $flagModel = Get-GoResolvedFlagModel -Path @($commandName)
            }

            continue
        }

        if ($commandName -eq 'test' -and $testArgsSeen) {
            continue
        }

        if ($commandName -eq 'test' -and $baseName -eq '-args') {
            $testArgsSeen = $true
            continue
        }

        if ($token.StartsWith('-', [System.StringComparison]::Ordinal)) {
            if ($commandName -eq 'env') {
                if ($baseName -eq '-w') { $envMode = 'w' }
                if ($baseName -eq '-u') { $envMode = 'u' }
            }

            if (($token -notlike '*=*') -and (Test-GoOptionRequiresValue -FlagModel $flagModel -OptionName $baseName)) {
                $pendingOption = $baseName
            }

            continue
        }

        if (-not $subcommandName -and @((Get-GoHelpModel -Path @($commandName)).Commands.Keys).Count -gt 0) {
            $subcommandName = $token
            if ($commandName -ne 'help' -and $commandName -ne 'tool') {
                $flagModel = Get-GoResolvedFlagModel -Path @($commandName, $subcommandName)
            }

            continue
        }

        if (-not $subcommandName -and ($commandName -eq 'help' -or $commandName -eq 'tool')) {
            $subcommandName = $token
            continue
        }

        $operandCount++
    }

    if ($null -eq $flagModel) {
        $flagModel = [ordered]@{}
    }

    [pscustomobject]@{
        Command       = $commandName
        Subcommand    = $subcommandName
        PendingOption = $pendingOption
        EnvMode       = $envMode
        TestArgsSeen  = $testArgsSeen
        OperandCount  = $operandCount
        FlagModel     = $flagModel
    }
}

function Get-GoUniqueCompletions {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )

    begin {
        $seen = @{}
    }

    process {
        foreach ($result in @($InputObject)) {
            if ($null -eq $result) {
                continue
            }

            if ($seen.ContainsKey($result.CompletionText)) {
                continue
            }

            $seen[$result.CompletionText] = $true
            $result
        }
    }
}

function Get-GoOptionCompletions {
    param(
        [System.Collections.Specialized.OrderedDictionary]$FlagModel,
        [string]$WordToComplete
    )

    # go accepts --flag as well as -flag, so a typed -- keeps the -- spelling.
    $doubleDash = $WordToComplete -match '^--[^-]|^--$'
    $needle = if ($WordToComplete -match '^--[^-]') { $WordToComplete.Substring(1) } elseif ($WordToComplete -eq '--') { '-' } else { $WordToComplete }

    return ,@(
        foreach ($name in @($FlagModel.Keys)) {
            if ([string]::IsNullOrEmpty($needle) -or $name -clike ([System.Management.Automation.WildcardPattern]::Escape($needle) + '*')) {
                $text = if ($doubleDash) { '-' + $name } else { $name }
                $spec = $FlagModel[$name]
                $toolTip = if ($spec.TakesValue) { "go option $name <$($spec.ValueName)>" } else { "go option $name" }
                New-GoCompletionResult -CompletionText $text -ListItemText $text -ResultType ([System.Management.Automation.CompletionResultType]::ParameterName) -ToolTip $toolTip
            }
        }
    )
}

function Get-GoCommandCompletions {
    param(
        [string[]]$Commands,
        [string]$WordToComplete
    )

    return ,@(
        foreach ($command in $Commands) {
            if ([string]::IsNullOrEmpty($WordToComplete) -or $command -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                New-GoCompletionResult -CompletionText $command -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip (Get-GoCommandDescription -Name $command)
            }
        }
    )
}

function Get-GoTopicCompletions {
    param(
        [string[]]$Topics,
        [string]$WordToComplete
    )

    $metadata = Get-GoHelpTopicMetadata
    $model = Get-GoHelpModel -Path @()
    return ,@(
        foreach ($topic in $Topics) {
            if ([string]::IsNullOrEmpty($WordToComplete) -or $topic -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                $toolTip = if ($model.Topics.Contains($topic)) {
                    $model.Topics[$topic]
                } elseif ($metadata.Contains($topic)) {
                    $metadata[$topic]
                } else {
                    "go help $topic"
                }

                New-GoCompletionResult -CompletionText $topic -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $toolTip
            }
        }
    )
}

function Get-GoNestedCommandCompletions {
    param(
        [string]$CommandName,
        [string[]]$NestedCommands,
        [string]$WordToComplete
    )

    return ,@(
        foreach ($name in $NestedCommands) {
            if ([string]::IsNullOrEmpty($WordToComplete) -or $name -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                New-GoCompletionResult -CompletionText $name -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip (Get-GoNestedCommandDescription -CommandName $CommandName -Name $name)
            }
        }
    )
}

function Get-GoEnvWriteCompletions {
    param([string]$WordToComplete)

    if ($WordToComplete -like '*=*') {
        $equalsIndex = $WordToComplete.IndexOf('=')
        $namePart = $WordToComplete.Substring(0, $equalsIndex)
        if ([string]::IsNullOrWhiteSpace($namePart)) {
            return New-GoCompletionResult -CompletionText '<NAME=VALUE>' -ToolTip 'Environment assignment for go env -w'
        }

        return New-GoCompletionResult -CompletionText "$namePart=<value>" -ToolTip "Set $namePart with go env -w"
    }

    $results = foreach ($name in (Get-GoEnvNames)) {
        if ([string]::IsNullOrEmpty($WordToComplete) -or $name -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-GoCompletionResult -CompletionText "$name=<value>" -ListItemText $name -ToolTip "Set $name with go env -w"
        }
    }

    if ($results) {
        return $results
    }

    New-GoCompletionResult -CompletionText '<NAME=VALUE>' -ToolTip 'Environment assignment for go env -w'
}

function Get-GoEnvNameCompletions {
    param(
        [string]$WordToComplete,
        [string]$ToolTipVerb = 'Read'
    )

    $results = foreach ($name in (Get-GoEnvNames)) {
        if ([string]::IsNullOrEmpty($WordToComplete) -or $name -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-GoCompletionResult -CompletionText $name -ToolTip "$ToolTipVerb $name"
        }
    }

    if ($results) {
        return $results
    }

    New-GoCompletionResult -CompletionText '<NAME>' -ToolTip 'Go environment variable name'
}

function Get-GoPackagePatternCompletions {
    param([string]$WordToComplete)

    # 'go help packages' documents these reserved patterns; everything else is an
    # import path, for which the directory tree is the useful local source.
    $patterns = [ordered]@{
        './...' = 'the package in the current directory and all subdirectories'
        '.'     = 'the package in the current directory'
        'all'   = 'all packages in the main module and their dependencies'
        'std'   = 'the packages in the Go standard library'
        'cmd'   = 'the Go command and its internal packages'
        'tool'  = 'the tool dependencies of the main module'
    }

    return ,@(
        foreach ($pattern in @($patterns.Keys)) {
            if ([string]::IsNullOrEmpty($WordToComplete) -or $pattern -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                New-GoCompletionResult -CompletionText $pattern -ToolTip $patterns[$pattern]
            }
        }
    )
}

function Get-GoModuleVersionCompletions {
    param([string]$WordToComplete)

    $atIndex = $WordToComplete.LastIndexOf('@')
    if ($atIndex -lt 0) {
        return ,@()
    }

    $prefix = $WordToComplete.Substring(0, $atIndex + 1)
    $typed = $WordToComplete.Substring($atIndex + 1)
    $queries = [ordered]@{
        'latest'  = 'the latest release version'
        'upgrade' = 'the latest version, or the current one if it is newer'
        'patch'   = 'the latest patch release of the current major.minor'
        'none'    = 'remove the dependency'
    }

    return ,@(
        foreach ($query in @($queries.Keys)) {
            if ([string]::IsNullOrEmpty($typed) -or $query -like ([System.Management.Automation.WildcardPattern]::Escape($typed) + '*')) {
                New-GoCompletionResult -CompletionText ($prefix + $query) -ListItemText $query -ToolTip $queries[$query]
            }
        }
    )
}

function Get-GoValueCompletions {
    param(
        [object]$FlagSpec,
        [string]$OptionName,
        [string]$WordToComplete,
        [string]$InlinePrefix = ''
    )

    $enumValues = if ($null -eq $FlagSpec) { @() } else { @($FlagSpec.Values) }
    if ($enumValues.Count -gt 0) {
        return ,@(
            foreach ($value in $enumValues) {
                if ([string]::IsNullOrEmpty($WordToComplete) -or $value -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                    $completionText = if ($InlinePrefix) { $InlinePrefix + $value } else { $value }
                    New-GoCompletionResult -CompletionText $completionText -ListItemText $value -ToolTip "Value for $OptionName"
                }
            }
        )
    }

    switch ($OptionName) {
        '-C'       { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix -DirectoryOnly $true) }
        '-exec'    { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-modfile' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-o'       { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-overlay' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-outputdir' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix -DirectoryOnly $true) }
        '-pkgdir'  { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix -DirectoryOnly $true) }
        '-coverprofile' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-vettool' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-fixtool' { return ,@(Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix) }
        '-pgo' {
            return ,@(
                foreach ($special in @('auto', 'off')) {
                    if ([string]::IsNullOrEmpty($WordToComplete) -or $special -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                        $completionText = if ($InlinePrefix) { $InlinePrefix + $special } else { $special }
                        New-GoCompletionResult -CompletionText $completionText -ListItemText $special -ToolTip 'Value for -pgo'
                    }
                }

                Get-GoPathCompletions -InputText $WordToComplete -InlinePrefix $InlinePrefix
            )
        }
        '-coverpkg' { return ,@(Get-GoPackagePatternCompletions -WordToComplete $WordToComplete) }
    }

    if (-not [string]::IsNullOrEmpty($WordToComplete)) {
        return ,@()
    }

    $placeholder = Get-GoValuePlaceholder -OptionName $OptionName -FlagSpec $FlagSpec
    if ($InlinePrefix) {
        return ,@(New-GoCompletionResult -CompletionText ($InlinePrefix + $placeholder) -ListItemText $placeholder -ToolTip "Value for $OptionName")
    }

    return ,@(New-GoCompletionResult -CompletionText $placeholder -ToolTip "Value for $OptionName")
}

function Get-GoOperandCompletions {
    param(
        [object]$Context,
        [string]$WordToComplete
    )

    $path = @($Context.Command)
    if ($Context.Subcommand -and $Context.Command -ne 'help' -and $Context.Command -ne 'tool') {
        $path = @($Context.Command, $Context.Subcommand)
    }

    $model = Get-GoHelpModel -Path $path
    $operands = @($model.Operands)
    $operandText = $operands -join ' '

    $results = New-Object System.Collections.Generic.List[object]

    foreach ($choice in @($model.Choices)) {
        if ([string]::IsNullOrEmpty($WordToComplete) -or $choice -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            [void]$results.Add((New-GoCompletionResult -CompletionText $choice -ToolTip "go $($path -join ' ') $choice"))
        }
    }

    if (@($model.Choices).Count -gt 0) {
        return $results.ToArray()
    }

    if ($WordToComplete.Contains('@') -and $operandText -match 'package') {
        foreach ($item in @(Get-GoModuleVersionCompletions -WordToComplete $WordToComplete)) {
            [void]$results.Add($item)
        }

        return $results.ToArray()
    }

    if ($operandText -match 'package') {
        foreach ($item in @(Get-GoPackagePatternCompletions -WordToComplete $WordToComplete)) {
            [void]$results.Add($item)
        }
    }

    if ($operandText -match 'var\b') {
        foreach ($item in @(Get-GoEnvNameCompletions -WordToComplete $WordToComplete)) {
            [void]$results.Add($item)
        }

        return $results.ToArray()
    }

    if ($operandText -match 'package|file|dir|moddirs|\.go') {
        foreach ($item in @(Get-GoPathCompletions -InputText $WordToComplete -DirectoryOnly ($operandText -match 'moddirs|dir\b'))) {
            [void]$results.Add($item)
        }
    }

    $results.ToArray()
}

Register-ArgumentCompleter -Native -CommandName @('go', 'go.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Reset-GoCacheForLocation

    # $cursorPosition is an offset into the whole input line; the extent text is
    # command-relative.
    $state = Get-GoCommandState -CommandAst $commandAst -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -FallbackWordToComplete $wordToComplete
    $currentWord = $state.CurrentWord
    $context = Get-GoCommandContext -Tokens $state.PriorTokens

    if ($context.Command -eq 'test' -and $context.TestArgsSeen) {
        New-GoCompletionResult -CompletionText '<test-binary-arg>' -ToolTip 'Argument passed through after go test -args'
        return
    }

    # go parses a command's flags only up to its first operand; later dashed
    # words are operands ('malformed import path "-race"'), and go run hands
    # them to the program. go test reads flags anywhere, and help and tool
    # operands are a topic or a tool name.
    $flagsClosed = $context.OperandCount -gt 0 -and @('test', 'help', 'tool') -notcontains $context.Command
    if ($flagsClosed -and $context.Command -eq 'run') {
        return
    }

    if ($currentWord -like '-*=*') {
        $equalsIndex = $currentWord.IndexOf('=')
        $flagPart = Get-GoOptionBaseName -Token $currentWord.Substring(0, $equalsIndex)
        $valuePart = $currentWord.Substring($equalsIndex + 1)
        $typedFlag = $currentWord.Substring(0, $equalsIndex)

        if (-not $context.Command -and $flagPart -eq '-C') {
            Get-GoPathCompletions -InputText $valuePart -InlinePrefix ($typedFlag + '=') -DirectoryOnly $true | Get-GoUniqueCompletions
            return
        }

        $spec = if ($context.FlagModel.Contains($flagPart)) { $context.FlagModel[$flagPart] } else { $null }
        Get-GoValueCompletions -FlagSpec $spec -OptionName $flagPart -WordToComplete $valuePart -InlinePrefix ($typedFlag + '=') |
            Get-GoUniqueCompletions
        return
    }

    if (-not $context.Command) {
        if ($context.PendingOption -eq '-C') {
            Get-GoPathCompletions -InputText $currentWord -DirectoryOnly $true | Get-GoUniqueCompletions
            return
        }

        $rootResults = New-Object System.Collections.Generic.List[object]
        if ([string]::IsNullOrEmpty($currentWord) -or $currentWord -like '-*') {
            $rootFlags = [ordered]@{}
            $buildFlags = Get-GoBuildFlagModel
            if ($buildFlags.Contains('-C')) {
                $rootFlags['-C'] = $buildFlags['-C']
            } else {
                $rootFlags['-C'] = [pscustomobject]@{ Name = '-C'; TakesValue = $true; ValueName = 'dir'; Values = @() }
            }

            foreach ($item in @(Get-GoOptionCompletions -FlagModel $rootFlags -WordToComplete $currentWord)) {
                [void]$rootResults.Add($item)
            }
        }

        if (-not ($currentWord -like '-*')) {
            foreach ($item in @(Get-GoCommandCompletions -Commands (Get-GoRootCommands) -WordToComplete $currentWord)) {
                [void]$rootResults.Add($item)
            }
        }

        $rootResults | Get-GoUniqueCompletions

        return
    }

    if ($context.PendingOption) {
        if ($context.Command -eq 'env' -and $context.EnvMode -eq 'w') {
            Get-GoEnvWriteCompletions -WordToComplete $currentWord | Get-GoUniqueCompletions
            return
        }

        if ($context.Command -eq 'env' -and $context.EnvMode -eq 'u') {
            Get-GoEnvNameCompletions -WordToComplete $currentWord -ToolTipVerb 'Unset' | Get-GoUniqueCompletions
            return
        }

        $spec = if ($context.FlagModel.Contains($context.PendingOption)) { $context.FlagModel[$context.PendingOption] } else { $null }
        Get-GoValueCompletions -FlagSpec $spec -OptionName $context.PendingOption -WordToComplete $currentWord |
            Get-GoUniqueCompletions
        return
    }

    if ($context.Command -eq 'env' -and $context.EnvMode -eq 'w' -and -not ($currentWord -like '-*')) {
        Get-GoEnvWriteCompletions -WordToComplete $currentWord | Get-GoUniqueCompletions
        return
    }

    if ($context.Command -eq 'env' -and $context.EnvMode -eq 'u' -and -not ($currentWord -like '-*')) {
        Get-GoEnvNameCompletions -WordToComplete $currentWord -ToolTipVerb 'Unset' | Get-GoUniqueCompletions
        return
    }

    if ($currentWord -like '-*' -and -not $flagsClosed) {
        Get-GoOptionCompletions -FlagModel $context.FlagModel -WordToComplete $currentWord | Get-GoUniqueCompletions
        return
    }

    if ($context.Command -eq 'help' -and -not $context.Subcommand) {
        $helpResults = New-Object System.Collections.Generic.List[object]
        foreach ($item in @(Get-GoCommandCompletions -Commands (Get-GoRootCommands) -WordToComplete $currentWord)) {
            [void]$helpResults.Add($item)
        }

        foreach ($item in @(Get-GoTopicCompletions -Topics (Get-GoHelpTopics) -WordToComplete $currentWord)) {
            [void]$helpResults.Add($item)
        }

        $helpResults | Get-GoUniqueCompletions
        return
    }

    if ($context.Command -eq 'tool' -and -not $context.Subcommand) {
        $tools = @(Get-GoToolNames)
        $toolResults = foreach ($toolName in $tools) {
            if ([string]::IsNullOrEmpty($currentWord) -or $toolName -like ([System.Management.Automation.WildcardPattern]::Escape($currentWord) + '*')) {
                New-GoCompletionResult -CompletionText $toolName -ToolTip 'Installed go tool'
            }
        }

        if ($toolResults) {
            $toolResults | Get-GoUniqueCompletions
        } else {
            New-GoCompletionResult -CompletionText '<tool-name>' -ToolTip 'go tool name'
        }

        return
    }

    $nested = @(Get-GoNestedCommands -CommandName $context.Command)
    if ($nested.Count -gt 0 -and -not $context.Subcommand) {
        Get-GoNestedCommandCompletions -CommandName $context.Command -NestedCommands $nested -WordToComplete $currentWord |
            Get-GoUniqueCompletions
        return
    }

    $tailResults = New-Object System.Collections.Generic.List[object]
    foreach ($item in @(Get-GoOperandCompletions -Context $context -WordToComplete $currentWord)) {
        [void]$tailResults.Add($item)
    }

    if ([string]::IsNullOrEmpty($currentWord) -and -not $flagsClosed) {
        foreach ($item in @(Get-GoOptionCompletions -FlagModel $context.FlagModel -WordToComplete $currentWord)) {
            [void]$tailResults.Add($item)
        }
    }

    $tailResults | Get-GoUniqueCompletions
}