2023-05-23 08:47:11 +08:00
|
|
|
using namespace System.Management.Automation
|
|
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
# Thin wrapper around `task __complete`. All suggestion logic lives in the
|
|
|
|
|
# Go engine — do not add completion logic here.
|
2026-05-31 18:06:39 +09:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
|
2023-05-23 08:47:11 +08:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
|
|
|
|
|
param($wordToComplete, $commandAst, $cursorPosition)
|
2020-01-20 11:26:15 +00:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
$TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' }
|
2025-11-29 12:16:58 +01:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
# Words after the program name, truncated to the cursor.
|
|
|
|
|
$argsToPass = @()
|
|
|
|
|
$elements = $commandAst.CommandElements
|
|
|
|
|
if ($elements.Count -gt 1) {
|
|
|
|
|
for ($i = 1; $i -lt $elements.Count; $i++) {
|
|
|
|
|
$el = $elements[$i]
|
|
|
|
|
if ($el.Extent.StartOffset -ge $cursorPosition) { break }
|
|
|
|
|
$argsToPass += $el.ToString()
|
2025-11-29 12:16:58 +01:00
|
|
|
}
|
2026-06-29 17:38:24 +02:00
|
|
|
}
|
|
|
|
|
# The trailing word (possibly empty) must reach the engine so it knows
|
2026-07-03 16:03:33 +02:00
|
|
|
# the cursor sits on a fresh word. It is already present when it coincides
|
|
|
|
|
# with the last command element captured above.
|
|
|
|
|
if ($argsToPass.Count -eq 0 -or $argsToPass[-1] -ne $wordToComplete) {
|
2026-06-29 17:38:24 +02:00
|
|
|
$argsToPass += $wordToComplete
|
|
|
|
|
}
|
2025-11-29 12:16:58 +01:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
$output = & $TaskExe __complete @argsToPass 2>$null
|
|
|
|
|
if (-not $output) { return }
|
2025-11-29 12:16:58 +01:00
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
$lines = @($output)
|
|
|
|
|
if ($lines.Count -eq 0) { return }
|
|
|
|
|
$last = $lines[-1]
|
|
|
|
|
if (-not $last.StartsWith(':')) { return }
|
|
|
|
|
|
|
|
|
|
$directive = [int]($last.Substring(1))
|
|
|
|
|
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
|
|
|
|
|
|
2026-07-03 22:29:42 +02:00
|
|
|
# Completion directives, mirroring internal/complete/complete.go.
|
|
|
|
|
$NoFileComp = 4
|
|
|
|
|
$FilterFileExt = 8
|
|
|
|
|
$FilterDirs = 16
|
|
|
|
|
|
2026-07-03 16:03:33 +02:00
|
|
|
# Note: DirectiveNoSpace (bit 2) cannot be honored here — PowerShell's
|
|
|
|
|
# CompletionResult API has no per-item "no trailing space" option, so a
|
|
|
|
|
# suggestion like `VAR=` gets a trailing space. This is a PowerShell limit.
|
|
|
|
|
|
2026-07-03 22:29:42 +02:00
|
|
|
# FilterFileExt: keep files whose extension matches, plus directories so the
|
|
|
|
|
# user can still descend into them. `-Include` is unreliable without
|
|
|
|
|
# `-Recurse`, so filter with Where-Object instead.
|
|
|
|
|
if ($directive -band $FilterFileExt) {
|
|
|
|
|
$exts = $data | ForEach-Object { ".$_" }
|
|
|
|
|
return Get-ChildItem -Path "$wordToComplete*" -ErrorAction SilentlyContinue |
|
|
|
|
|
Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } |
|
|
|
|
|
ForEach-Object {
|
|
|
|
|
$type = if ($_.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem }
|
|
|
|
|
[CompletionResult]::new($_.Name, $_.Name, $type, $_.Name)
|
|
|
|
|
}
|
2023-05-23 08:47:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 17:38:24 +02:00
|
|
|
# FilterDirs
|
2026-07-03 22:29:42 +02:00
|
|
|
if ($directive -band $FilterDirs) {
|
|
|
|
|
return Get-ChildItem -Path "$wordToComplete*" -Directory -ErrorAction SilentlyContinue |
|
2026-06-29 17:38:24 +02:00
|
|
|
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 16:03:33 +02:00
|
|
|
# Build candidates, filtering by the current word. PowerShell does not filter
|
|
|
|
|
# native argument-completer results itself, so without this every suggestion
|
|
|
|
|
# would be offered regardless of what the user typed.
|
|
|
|
|
$results = @($data | ForEach-Object {
|
2026-06-29 17:38:24 +02:00
|
|
|
$parts = $_ -split "`t", 2
|
|
|
|
|
$value = $parts[0]
|
2026-07-03 16:03:33 +02:00
|
|
|
if ($wordToComplete -and -not $value.StartsWith($wordToComplete)) { return }
|
2026-06-29 17:38:24 +02:00
|
|
|
$desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value }
|
|
|
|
|
[CompletionResult]::new($value, $value, [CompletionResultType]::ParameterValue, $desc)
|
2026-07-03 16:03:33 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# NoFileComp (bit 4) unset and nothing matched → fall back to file completion,
|
|
|
|
|
# since the engine returned DirectiveDefault (e.g. --cacert, after `--`).
|
2026-07-03 22:29:42 +02:00
|
|
|
if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) {
|
2026-07-03 16:03:33 +02:00
|
|
|
return Get-ChildItem -Path . -ErrorAction SilentlyContinue |
|
|
|
|
|
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
|
2026-06-29 17:38:24 +02:00
|
|
|
}
|
2026-07-03 16:03:33 +02:00
|
|
|
|
|
|
|
|
return $results
|
2023-05-23 08:47:11 +08:00
|
|
|
}
|