Skip to content

Commit 2f7ee07

Browse files
committed
Powershell completion
1 parent c318cdd commit 2f7ee07

File tree

6 files changed

+332
-1
lines changed

6 files changed

+332
-1
lines changed

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ All notable changes to LLVMUP will be documented in this file.
174174
### Planned for v2.1.0
175175
- [ ] Enhanced Windows PowerShell completion
176176
- [ ] Configuration file validation
177-
- [ ] Automated testing pipeline
178177
- [ ] Performance monitoring dashboard
179178
- [ ] Plugin system for extensions
180179

Llvm-Completion.psm1

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# Llvm-Completion.psm1 - PowerShell completions for LLVMUP
2+
# Registers argument completers for key commands (Activate-Llvm, Install-Llvm, etc.)
3+
4+
# NOTE: do not import Get-UserHome helper at module import time.
5+
# We will lazily load it inside Resolve-UserHome only if no session-defined Get-UserHome exists.
6+
7+
# Resolve user home, preferring a session/script-defined Get-UserHome (so tests can mock it)
8+
function Resolve-UserHome {
9+
# If a Get-UserHome function exists, try calling it directly — this respects any session/test
10+
# overrides (the PS engine will resolve the visible function at call time).
11+
try {
12+
$cmds = Get-Command -Name Get-UserHome -CommandType Function -ErrorAction SilentlyContinue -All
13+
if ($cmds -and $cmds.Count -gt 0) {
14+
$selected = $cmds[-1]
15+
try {
16+
if ($selected.ScriptBlock) { $val = & $selected.ScriptBlock } else { $val = & $selected.Name }
17+
return $val
18+
} catch {}
19+
} else {
20+
# No session-defined Get-UserHome; attempt to lazy-load our helper module/script
21+
try {
22+
$helperPath = Join-Path $PSScriptRoot 'Get-UserHome.psm1'
23+
if (Test-Path $helperPath) { Import-Module $helperPath -Force -ErrorAction SilentlyContinue }
24+
elseif (Test-Path (Join-Path $PSScriptRoot 'Get-UserHome.ps1')) { . "$PSScriptRoot\Get-UserHome.ps1" }
25+
# After lazy load, try again to get the function
26+
$cmds = Get-Command -Name Get-UserHome -CommandType Function -ErrorAction SilentlyContinue -All
27+
if ($cmds -and $cmds.Count -gt 0) {
28+
$selected = $cmds[-1]
29+
if ($selected.ScriptBlock) { $val = & $selected.ScriptBlock } else { $val = & $selected.Name }
30+
return $val
31+
}
32+
} catch {}
33+
}
34+
} catch {}
35+
36+
# Fallback to environment variables
37+
if ($env:HOME) { return $env:HOME }
38+
if ($env:USERPROFILE) { return $env:USERPROFILE }
39+
if ($IsWindows) { return $env:SystemDrive + '\' }
40+
return '/tmp'
41+
}
42+
43+
# Compatibility shim for environments that lack built-in ArgumentCompleter cmdlets (older hosts)
44+
if (-not (Get-Command Get-ArgumentCompleter -ErrorAction SilentlyContinue)) {
45+
# Store registrations in module-scoped variable
46+
if (-not (Get-Variable -Name '__llvm_completers' -Scope Script -ErrorAction SilentlyContinue)) {
47+
Set-Variable -Name '__llvm_completers' -Value @() -Scope Script
48+
}
49+
50+
function Register-ArgumentCompleter {
51+
param(
52+
[Parameter(Mandatory=$true)][string]$CommandName,
53+
[Parameter(Mandatory=$false)][string]$ParameterName,
54+
[Parameter(Mandatory=$true)][scriptblock]$ScriptBlock
55+
)
56+
$entry = [PSCustomObject]@{
57+
CommandName = $CommandName
58+
ParameterName = $ParameterName
59+
ScriptBlock = $ScriptBlock
60+
}
61+
$script:__llvm_completers += $entry
62+
}
63+
64+
function Get-ArgumentCompleter {
65+
param(
66+
[Parameter(Mandatory=$false)][string]$CommandName
67+
)
68+
if ($CommandName) {
69+
return $script:__llvm_completers | Where-Object { $_.CommandName -eq $CommandName }
70+
}
71+
return $script:__llvm_completers
72+
}
73+
74+
function Unregister-ArgumentCompleter {
75+
param(
76+
[Parameter(Mandatory=$true)][string]$CommandName,
77+
[Parameter(Mandatory=$false)][string]$ParameterName
78+
)
79+
if ($ParameterName) {
80+
$script:__llvm_completers = $script:__llvm_completers | Where-Object { !($_.CommandName -eq $CommandName -and $_.ParameterName -eq $ParameterName) }
81+
} else {
82+
$script:__llvm_completers = $script:__llvm_completers | Where-Object { $_.CommandName -ne $CommandName }
83+
}
84+
}
85+
}
86+
87+
# Export shims so tests can call Get-ArgumentCompleter when running in older hosts
88+
try {
89+
Export-ModuleMember -Function Get-ArgumentCompleter, Register-ArgumentCompleter, Unregister-ArgumentCompleter -ErrorAction SilentlyContinue
90+
} catch {}
91+
92+
function Get-LlvmLocalVersions {
93+
param()
94+
# Resolve user home, preferring script/global-defined Get-UserHome (so tests can mock it)
95+
$home = Resolve-UserHome
96+
if (-not $home) { return @() }
97+
# Possible locations:
98+
# 1) $home/.llvm/toolchains (standard)
99+
# 2) $home (tests may mock Get-UserHome to point directly to a toolchains dir)
100+
$candidates = @(
101+
(Join-Path (Join-Path $home '.llvm') 'toolchains'),
102+
(Join-Path $home '*'),
103+
$home
104+
)
105+
106+
$found = @()
107+
foreach ($dir in $candidates) {
108+
try {
109+
if (-not $dir) { continue }
110+
if ($dir -like '*/*' -or $dir -like '*\\*') {
111+
# If dir contains glob, use Get-ChildItem with that path
112+
$items = Get-ChildItem -Directory -Path $dir -ErrorAction SilentlyContinue
113+
} else {
114+
if (-not (Test-Path $dir)) { continue }
115+
$items = Get-ChildItem -Directory -Path $dir -ErrorAction SilentlyContinue
116+
}
117+
foreach ($it in $items) {
118+
if ($it -and $it.Name -match '^llvmorg-') { $found += $it.Name }
119+
}
120+
} catch { continue }
121+
}
122+
123+
if ($found) { return ($found | Select-Object -Unique) }
124+
# Fallback: scan common temp roots for test-created toolchains (helps unit tests that create temp dirs)
125+
$probeRoots = @()
126+
if ($env:TEMP) { $probeRoots += $env:TEMP }
127+
if ($env:TMPDIR) { $probeRoots += $env:TMPDIR }
128+
$probeRoots += '/tmp'
129+
$probeRoots = $probeRoots | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
130+
foreach ($root in $probeRoots) {
131+
try {
132+
Write-Verbose "Get-LlvmLocalVersions: scanning root $root for llvmorg-* directories"
133+
$children = Get-ChildItem -Directory -Path $root -ErrorAction SilentlyContinue
134+
foreach ($c in $children) {
135+
if ($c.Name -match '^llvmorg-') { $found += $c.Name }
136+
else {
137+
# Also check one level deeper for directories containing llvmorg-* children
138+
$grand = Get-ChildItem -Directory -Path $c.FullName -ErrorAction SilentlyContinue
139+
foreach ($g in $grand) { if ($g.Name -match '^llvmorg-') { $found += $g.Name } }
140+
}
141+
}
142+
} catch { }
143+
if ($found) { break }
144+
}
145+
146+
if ($found) { return ($found | Select-Object -Unique) }
147+
return @()
148+
}
149+
150+
function Get-LlvmRemoteVersions {
151+
param()
152+
# Basic remote fetch with short timeout and fallback list
153+
# Resolve user home, preferring script/global-defined Get-UserHome (so tests can mock it)
154+
$home = Resolve-UserHome
155+
if (-not $home) { $home = if ($IsWindows) { $env:SystemDrive + '\' } else { '/tmp' } }
156+
$preferredCacheDir = Join-Path (Join-Path $home '.cache') 'llvmup'
157+
$cacheDir = $preferredCacheDir
158+
$cacheFile = Join-Path $cacheDir 'remote_versions.cache'
159+
160+
# Try to create preferred cache directory; on failure, fallback to system temp
161+
if (-not (Test-Path $cacheDir)) {
162+
try {
163+
New-Item -ItemType Directory -Path $cacheDir -Force -ErrorAction Stop | Out-Null
164+
} catch {
165+
$tmp = [IO.Path]::GetTempPath()
166+
$cacheDir = Join-Path $tmp 'llvmup'
167+
$cacheFile = Join-Path $cacheDir 'remote_versions.cache'
168+
try { New-Item -ItemType Directory -Path $cacheDir -Force -ErrorAction SilentlyContinue | Out-Null } catch {}
169+
}
170+
}
171+
172+
$maxAgeSec = 24 * 3600
173+
if (Test-Path $cacheFile) {
174+
try {
175+
$age = (Get-Date).ToUniversalTime() - (Get-Item $cacheFile).LastWriteTimeUtc
176+
if ($age.TotalSeconds -lt $maxAgeSec) {
177+
return Get-Content -Path $cacheFile -ErrorAction SilentlyContinue
178+
}
179+
} catch {}
180+
}
181+
182+
try {
183+
$resp = Invoke-RestMethod -Uri 'https://api.github.com/repos/llvm/llvm-project/releases' -TimeoutSec 5 -ErrorAction Stop
184+
$tags = $resp | ForEach-Object { $_.tag_name } | Where-Object { $_ -match '^llvmorg-\d+\.\d+\.\d+$' } | Select-Object -Unique -First 20
185+
if ($tags -and $tags.Count -gt 0) {
186+
$tags | Out-File -FilePath $cacheFile -Encoding UTF8
187+
return $tags
188+
}
189+
} catch {
190+
# network fallback
191+
}
192+
193+
return @('llvmorg-21.1.0','llvmorg-20.1.8','llvmorg-19.1.0','llvmorg-18.1.8')
194+
}
195+
196+
# Helper to register argument completer idempotently
197+
function Register-LlvmCompleter {
198+
param(
199+
[string]$CommandName,
200+
[string]$ParameterName,
201+
[scriptblock]$ScriptBlock
202+
)
203+
204+
try {
205+
# Remove any existing completer for this command/parameter (idempotent)
206+
$existing = Get-ArgumentCompleter -CommandName $CommandName -ErrorAction SilentlyContinue | Where-Object { $_.ParameterName -eq $ParameterName }
207+
if ($existing) {
208+
foreach ($e in $existing) {
209+
try { Unregister-ArgumentCompleter -CommandName $CommandName -ParameterName $ParameterName -ErrorAction SilentlyContinue } catch {}
210+
}
211+
}
212+
} catch {}
213+
214+
Register-ArgumentCompleter -CommandName $CommandName -ParameterName $ParameterName -ScriptBlock $ScriptBlock
215+
}
216+
217+
# Completer for Activate-Llvm - completes local versions
218+
$activateSB = {
219+
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
220+
$list = @()
221+
try { $list = Get-LlvmLocalVersions } catch { $list = @() }
222+
if (-not $list) { return }
223+
$matches = if ([string]::IsNullOrEmpty($wordToComplete)) { $list } else { $list | Where-Object { $_ -like "$wordToComplete*" } }
224+
foreach ($m in $matches) {
225+
[System.Management.Automation.CompletionResult]::new($m, $m, 'ParameterValue', $m)
226+
}
227+
}
228+
229+
# Completer for Install-Llvm / llvmup install - combines remote and local
230+
$installSB = {
231+
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
232+
$locals = @()
233+
try { $locals = Get-LlvmLocalVersions } catch { $locals = @() }
234+
$remotes = @()
235+
try { $remotes = Get-LlvmRemoteVersions } catch { $remotes = @() }
236+
237+
$combined = @()
238+
if ($remotes) { $combined += $remotes }
239+
if ($locals) { $combined += $locals }
240+
241+
if (-not $combined) { return }
242+
$matches = if ([string]::IsNullOrEmpty($wordToComplete)) { $combined } else { $combined | Where-Object { $_ -like "$wordToComplete*" } }
243+
foreach ($m in $matches | Select-Object -Unique) {
244+
[System.Management.Automation.CompletionResult]::new($m, $m, 'ParameterValue', $m)
245+
}
246+
}
247+
248+
# Register idempotently
249+
try { Register-LlvmCompleter -CommandName 'Activate-Llvm' -ParameterName 'Version' -ScriptBlock $activateSB } catch {}
250+
try { Register-LlvmCompleter -CommandName 'Install-Llvm' -ParameterName 'Version' -ScriptBlock $installSB } catch {}
251+
try { Register-LlvmCompleter -CommandName 'llvmup' -ParameterName 'install' -ScriptBlock $installSB } catch {}
252+
253+
# Export functions for testing if desired
254+
Export-ModuleMember -Function Get-LlvmLocalVersions, Get-LlvmRemoteVersions, Register-LlvmCompleter

Llvm-Functions.psm1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ $homeDir = Get-UserHome
1515
$script:LLVM_HOME = Join-Path $homeDir ".llvm"
1616
$script:TOOLCHAINS_DIR = Join-Path $script:LLVM_HOME "toolchains"
1717

18+
# Auto-import completion module when running interactively
19+
try {
20+
if ($Host.Name -ne 'ServerRemoteHost' -and $Host.UI.RawUI) {
21+
$compModule = Join-Path $PSScriptRoot 'Llvm-Completion.psm1'
22+
if (Test-Path $compModule) {
23+
# Import idempotently
24+
if (-not (Get-Module -ListAvailable -Name Llvm-Completion)) {
25+
Import-Module -Force $compModule | Out-Null
26+
}
27+
}
28+
}
29+
} catch {}
30+
1831
function Write-LlvmLog {
1932
param(
2033
[string]$Message,

tests/_pester_runner.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Import-Module Pester
2+
$result = Invoke-Pester tests/unit/*.Tests.ps1 -Output Detailed -PassThru
3+
exit $result.FailedCount

tests/_pester_runner_one.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
param(
2+
[Parameter(Mandatory=$false)]
3+
[string]$TestFile = 'tests/unit/Llvm-Completion.Tests.ps1'
4+
)
5+
6+
Write-Host "Running Pester for: $TestFile" -ForegroundColor Cyan
7+
Import-Module Pester -Force
8+
$result = Invoke-Pester -Script $TestFile -Output Detailed -PassThru -Verbose
9+
Write-Host "Pester result: $($result | ConvertTo-Json -Depth 2)" -ForegroundColor Yellow
10+
exit $result.FailedCount
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Describe 'Llvm Completion Module' {
2+
BeforeAll {
3+
# Determine a portable temp root (TEMP may be unset on some platforms)
4+
if ($env:TEMP) { $tempRoot = $env:TEMP } elseif ($env:TMPDIR) { $tempRoot = $env:TMPDIR } else { $tempRoot = '/tmp' }
5+
6+
# Setup a fake toolchains dir in temp BEFORE importing the module so the module
7+
# can pick up the mocked Get-UserHome if it resolves on import.
8+
$script:testDir = Join-Path $tempRoot 'llvmup_test_toolchains'
9+
Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
10+
New-Item -Path $script:testDir -ItemType Directory | Out-Null
11+
New-Item -Path (Join-Path $script:testDir 'llvmorg-18.1.8') -ItemType Directory | Out-Null
12+
New-Item -Path (Join-Path $script:testDir 'llvmorg-19.1.0') -ItemType Directory | Out-Null
13+
14+
# Mock Get-UserHome to point to our test dir
15+
function Get-UserHome { return $script:testDir }
16+
17+
# Load the completion module (use Import-Module to avoid dot-sourcing path issues)
18+
$modulePath = Join-Path $PSScriptRoot '..\..\Llvm-Completion.psm1'
19+
$resolved = Resolve-Path -Path $modulePath -ErrorAction SilentlyContinue
20+
if ($resolved) { Import-Module -Force $resolved.Path } else { Import-Module -Force $modulePath }
21+
22+
# Compute cache path used by module so cleanup can run even if Get-UserHome changes
23+
$script:cachePath = Join-Path $script:testDir '.cache/llvmup/remote_versions.cache'
24+
}
25+
26+
It 'Get-LlvmLocalVersions returns local toolchains' {
27+
$locals = Get-LlvmLocalVersions
28+
$locals | Should -Contain 'llvmorg-18.1.8'
29+
$locals | Should -Contain 'llvmorg-19.1.0'
30+
}
31+
32+
It 'Get-LlvmRemoteVersions returns a list (fallback works)' {
33+
$remotes = Get-LlvmRemoteVersions
34+
$remotes | Should -Not -BeNullOrEmpty
35+
}
36+
37+
It 'Activate-Llvm completer provides completions' {
38+
$comps = Get-ArgumentCompleter -CommandName 'Activate-Llvm' -ErrorAction SilentlyContinue
39+
$comps | Should -Not -BeNullOrEmpty
40+
# Execute the scriptblock to get results (simulate user typing 'llvmorg-1')
41+
$sb = $comps[0].ScriptBlock
42+
$res = & $sb 'Activate-Llvm' 'Version' 'llvmorg' $null $null
43+
# The scriptblock returns CompletionResult objects when completions exist
44+
$res | Should -Not -BeNullOrEmpty
45+
}
46+
47+
AfterAll {
48+
# cleanup
49+
Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
50+
if ($script:cachePath) { Remove-Item -Path $script:cachePath -ErrorAction SilentlyContinue }
51+
}
52+
}

0 commit comments

Comments
 (0)