spectre.console
spectre.console copied to clipboard
AutoCompletion Support #2
Base stolen from #727 . fixed & extended
I know there is alot going on there and i wasn't always sure if i was doing it in the right way. Comments&Changes welcome!
Status:
- [x] Static Command-Tree-based Completion
- [x] Static Property-Attribute-based Completion
- [x] Dynamic Handler-based Completion
- [x] Powershell Integration
Non-Goal:
- Other integrations than Powershell for now.
Edit: The powershell integration seems to have turned out a bit wonky. Maybe we want to extract it into a pre-release library until its stable enough
Please upvote :+1: this pull request if you are interested in it.
@microsoft-github-policy-service agree
I am going to mention the people from the other pr and https://github.com/spectreconsole/spectre.console/issues/267, they might be interested, otherwise: Sorry 😅
@Jawz84 @phil-scott-78 @FrankRay78 @OskarKlintrot
Commit https://github.com/JKamsker/spectre.console/commit/a39517fe91d1c8f5fce4de20ec5644dcc4a03e59 implements static completions, attribute based
Objections? anyone? @patriksvensson @phil-scott-78
@patriksvensson Finished from my side, can i get a review?
@JKamsker Currently on vacation but will get back to the computer in a week or so.
@JKamsker Currently on vacation but will get back to the computer in a week or so.
Awesome! Looking forward to it!
Implemented an Autocompletion "Server" which just listens to stdin and writes the completions to stdout Added Json format
Complimentary script (poc): https://gist.github.com/JKamsker/f31428a9ad8a181fd961314254c834a5 Will be included and allowed to be enabled with switches
Hi @patriksvensson ,
I hope you had a great vacation! Just circling back to this PR on AutoCompletion Support. Whenever you get a chance to review it, any feedback or questions are much appreciated. If there's any specific area you'd like me to focus on or if there's anything I can assist with to facilitate the review, please don’t hesitate to mention. Eager to make any necessary adjustments to align with the project's standards.
Thanks for your time!
Best, JKamsker
@patriksvensson Status?
Honest question, @JKamsker: Do you feel a single word, thrown out like that motivates anyone to take a closer look at your PR?
@nils-a I appreciate your comment and I realize now that my previous message may have come across as impatient. For that, I sincerely apologize. I understand that reviewing a pull request, especially one as complex as this one, requires a significant amount of time and effort.
@patriksvensson , I want to express my gratitude for the time you're investing to review this PR. I'm fully aware of the complexity involved and I'm more than willing to assist in any way that I can. Whether it's explaining certain parts of the code, providing additional documentation, or anything else that might make the review process easier for you, please feel free to ask.
I'm eager to receive any feedback or questions you might have. The main goal here is to improve the project and I'm committed to making any necessary adjustments to align with the project's standards.
Thank you for your understanding and for your time.
I got to admit I have only a very vague idea on how completion in PowerShell works. How would this PR enable completion? I'm guessing the script you mentioned above comes into play for that?
@nils-a
for the sake of readability, my tool is called apget
apget completion powershell --install : This command is build into the library. It basically gives you a script which calls Register-ArgumentCompleter. It is a method which registers a hook into the powershell completion system. It gets the command as a whole and the script calls the tool for completion
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$completions = @()
if(Test-Path -PathType Leaf -Path ".\ApGet.exe"){
$completions = & ".\ApGet.exe" cli complete --position $cursorPosition "$wordToComplete"
}
else{
$completions = & "C:\Users\JKamsker\Documents\PowerShell\Tools\Apget\ApGet.exe" cli complete --position $cursorPosition "$wordToComplete"
}
if ($completions) {
foreach ($completion in $completions) {
[System.Management.Automation.CompletionResult]::new($completion, $completion, 'ParameterValue', $completion)
}
}
else {
$null
}
}
if ($completions) {
foreach ($completion in $completions) {
[System.Management.Automation.CompletionResult]::new($completion, $completion, 'ParameterValue', $completion)
}
}
else {
$null
}
}
(In addition to that, it also sets some aliases)
When the completion is invoked, for e.g "apget ", the hook passes it to the tool like so apget cli complete --position 11 "apget " (position optional, but pwsh requires it)
and that is the universal reusable part of it. Most shells have similar methods like Register-ArgumentCompleter but all of them can use apget cli complete --position 11 "apget " (with or without poisition)
apget cli complete --position 11 "apget ": Returns a list of commands/branches directly available like
config
version
product
install
tools
I see, and the way I am reading it the completer is local to a session, which means if we wanted to have it global and always available, we'd need to add the call to [myapp] completion powershell --install to the $Profile, right?
@nils-a --install adds it to the $profile and its always available, even after the terminal has reopened. Leaving the switch out will keep the autocompletion until the session ends.
But the command only gives you the script. I forgot to add, that we need to call apget completion powershell --install | iex to invoke the script 😅
You could also go further and extend the script yourself and webinstall your tool with irm https://my.website.at/installer.ps1 | iex
Ok, so you have my interest @JKamsker . I'm certainly no expert on this, so can you tell me if this is PowerShell specific? (ie. does the compiled command line application that uses spectre.console need to run in PowerShell) Or does it / can it with some kind of extension also support other shells?
I'm trying to understand the scope of the implementation ie. Win/Linux/Mac and also breadth of shell ie. cmd / PS / bash etc
I ask, because whilst I can't find the link, I kind of remember seeing MS Terminal provide a number of different shims (?) for autocomplete across different shells (?). I wondered if this was similar.
UPDATE: here is the link: How to enable tab completion for the .NET CLI
@FrankRay78, this functionality should theoretically be applicable to any shell, assuming someone has created an equivalent to Register-ArgumentCompleter for the shell in question. The command apget cli complete --position 11 "apget " should yield the same result as "dotnet complete", although I've only verified this on pwsh shell.
The command apget completion powershell --install is not the crux of this feature in my opinion, but rather an additional perk. It's entirely possible for someone to devise similar commands for their respective shell, even without specific knowledge or access to spectreconsole's underlying structure.
On the other hand, offering other shell alternatives out of the box could be a good "selling point"
Whilst I'm not the maintainer of this sub-system @JKamsker so can't approve the PR, I am interested and plan to make time over the weekend to review the commits and test it locally.
@JKamsker Any hint what might be wrong?
➜ .\JavaVersionSwitcher.exe completion powershell --install | iex Invoke-Expression: Cannot bind argument to parameter 'Command' because it is an empty string. Invoke-Expression: Missing closing '}' in statement block or type definition.
The generated script is:
function Invoke-JavaVersionSwitcher {
if (Test-Path -PathType Leaf -Path ".\JavaVersionSwitcher.dll") {
& "dotnet" ".\JavaVersionSwitcher.dll" $args
return
}
return & "dotnet" "C:\_dev\nils-org\JavaVersionSwitcher\src\JavaVersionSwitcher\bin\Debug\net7.0\JavaVersionSwitcher.dll" $args
}
# Register-CompleterFor -CommandName JavaVersionSwitcher
function Register-CompleterFor{
#appname parameter
param(
[Parameter(Mandatory=$true)]
[string]$name
)
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
# $completions = & "dotnet" "C:\_dev\nils-org\JavaVersionSwitcher\src\JavaVersionSwitcher\bin\Debug\net7.0\JavaVersionSwitcher.dll" cli complete --position $cursorPosition "$wordToComplete"
$completions = @()
if(Test-Path -PathType Leaf -Path ".\JavaVersionSwitcher.dll"){
$completions = & "dotnet" ".\JavaVersionSwitcher.dll" cli complete --position $cursorPosition "$wordToComplete"
}
else{
$completions = & "dotnet" "C:\_dev\nils-org\JavaVersionSwitcher\src\JavaVersionSwitcher\bin\Debug\net7.0\JavaVersionSwitcher.dll" cli complete --position $cursorPosition "$wordToComplete"
}
if ($completions) {
foreach ($completion in $completions) {
[System.Management.Automation.CompletionResult]::new($completion, $completion, 'ParameterValue', $completion)
}
}
else {
$null
}
}
}
Set-alias -name JavaVersionSwitcher -Value Invoke-JavaVersionSwitcher
Set-alias -name javaversionswitcher -Value Invoke-JavaVersionSwitcher
Register-CompleterFor -name "JavaVersionSwitcher"
Register-CompleterFor -name "javaversionswitcher"
Register-CompleterFor -name "Invoke-JavaVersionSwitcher"
# adds for eg. Write-Host "Hello World! [Randomnumber]" to profile only once, otherwise does it overwrites it
function Add-ProfileLine {
param(
[string]$identifier,
[string]$Line
)
$ProfilePath = $PROFILE.CurrentUserAllHosts
# If not exists: Empty string array
$ProfileContent = @()
if (Test-Path -Path $ProfilePath) {
$ProfileContent = [System.IO.File]::ReadAllLines($ProfilePath)
}
# we need to remove Id: [identifier] and the line after it
$IdLine = "#Id: $identifier"
# $IdLineIndex = $ProfileContent.IndexOf($IdLine)
if ($IdLineIndex -ne -1 -and $IdLineIndex -lt ($ProfileContent.Count - 1)) {
$newContent = @()
$switch = $false
for ($i = 0; $i -lt $ProfileContent.Count; $i++) {
$content = $ProfileContent[$i]
if ($content -eq $IdLine) {
$switch = $true
continue
}
if ($switch) {
$switch = $false
continue
}
$newContent += $content
}
$ProfileContent = $newContent
}
# ProfileContent suddendly is a string instead of a string array
# make it an array if it is a string
if ($ProfileContent -is [string]) {
$ProfileContent = @($ProfileContent)
}
# adding the line
$ProfileContent += $IdLine
$ProfileContent += $Line
# writing the file
$ProfileContent | Set-Content $ProfilePath
}
function Add-Profile-Script {
param(
[string]$identifier,
[string]$scriptContent
)
$ProfilePath = $profile.CurrentUserAllHosts
$ScriptsPath = Join-Path (Split-Path $ProfilePath) "Scripts"
$ScriptPath = Join-Path $ScriptsPath "$identifier.ps1"
# Create the Scripts directory if it doesn't exist
if (-not (Test-Path -Path $ScriptsPath -PathType Container)) {
New-Item -ItemType Directory -Path $ScriptsPath -Force
}
# Write the content to the file
# overwrite if it exists
$scriptContent | Set-Content $ScriptPath
Add-ProfileLine -identifier $identifier -Line "if (Test-Path '$ScriptPath') { . '$ScriptPath' }"
}
$scriptContent = & "dotnet" "C:\_dev\nils-org\JavaVersionSwitcher\src\JavaVersionSwitcher\bin\Debug\net7.0\JavaVersionSwitcher.dll" completion powershell | Out-String
Add-Profile-Script -identifier javaversionswitcher -scriptContent $scriptContent
@nils-a no idea :/ in my tool it works....does just .\JavaVersionSwitcher.exe completion powershell | iex work?
Anyway, i will add an example that works for me, maybe you wanna test it out then
@nils-a no idea :/ in my tool it works....does just
.\JavaVersionSwitcher.exe completion powershell | iexwork?
no, same error:
➜ .\JavaVersionSwitcher.exe completion powershell | iex Invoke-Expression: Cannot bind argument to parameter 'Command' because it is an empty string. Invoke-Expression: Missing closing '}' in statement block or type definition.
@nils-a it was the wrong command all along.... I was able to reproduce the error. The solution was the | out-string | no idea what it does, but it works with that addition. Install should work aswell
.\AutoCompletion.exe completion powershell | Out-String | iex
I like it. Will do more tests tomorrow.
@nils-a I apologize for the delay. I fell ill over the weekend and am still recovering. I'll address this pull request once I'm in better shape.
@FrankRay78
Ok, so you have my interest @JKamsker . I'm certainly no expert on this, so can you tell me if this is PowerShell specific? (ie. does the compiled command line application that uses spectre.console need to run in PowerShell) Or does it / can it with some kind of extension also support other shells?
The primary functionality of this PR isn't exclusive to PowerShell. It's a universal tab completion engine, with the PowerShell module serving as an example of how it can be utilized. In principle, any shell could leverage it with the appropriate wrapper code.
The command line interface for this is .\AutoCompletion.exe cli complete "WORDTOCOMPLETE" (--position NUMBER). You can find more details here.
I'm trying to understand the scope of the implementation ie. Win/Linux/Mac and also breadth of shell ie. cmd / PS / bash etc
The goal here is to introduce a universal tab completion engine that any shell can utilize. The PowerShell module is merely the first implementation, with more to follow in the future. Implementing such a completion feature isn't overly complex and it doesn't rely on any spectre.console internals, unlike the core command cli complete.
I ask, because whilst I can't find the link, I kind of remember seeing MS Terminal provide a number of different shims (?) for autocomplete across different shells (?). I wondered if this was similar.
UPDATE: here is the link: How to enable tab completion for the .NET CLI
Theoretically, the completion code provided should work with the shims you've mentioned, although I haven't personally tested these.
Whilst I'm not the maintainer of this sub-system @JKamsker so can't approve the PR, I am interested and plan to make time over the weekend to review the commits and test it locally.
Cool, i really appreciate some extra eyes on this. I'm happy to answer any questions you might have.
Did you already have time to look at the code? I'm curious to hear your thoughts.
Quick Reminder:
It would be so cool to have a pre-populated command settings in IAsyncCommandCompletable.GetSuggestionsAsync(https://github.com/spectreconsole/spectre.console/pull/1260/files#diff-62f314eab97b968fe2cd2edf30cbb3e1fb7f4f24ee5a26d306436f662e928063R14). Or, at least, the command line arguments themselves. Sometimes dynamic suggestions are depended from already entered parameters.
@alex-fomin Thanks for the suggestion! I'm a bit tied up at the moment and won't be able to get to it right away, but it's definitely on my radar to tackle as soon as I can.
In the meantime, if you're up for it, you're totally welcome to take a stab at it by forking my PR and adding the changes yourself. Cheers!
@alex-fomin I have added the functionality to https://www.nuget.org/packages/JKToolKit.Spectre.AutoCompletion.Integrations and https://www.nuget.org/packages/JKToolKit.Spectre.AutoCompletion (Version 0.0.4) It works almost the same, except u have to add it explicitly.
public static void Main(string[] args)
{
var app = new CommandApp();
app.Configure
(
config =>
{
config
.AddAutoCompletion(x => x.AddPowershell())
.AddCommand<LionCommand>("lion");
}
) ;
}
Will port it to this pr when i got time.
Heyo @JKamsker, great job so far. Subscribed to the discussion and eager to try it out as soon as I get the chance. Just wanted to let you know the packages’ repos are private. If it‘s by choice don‘t mind me.
Now published as separate repo. https://github.com/JKamsker/JKToolKit.Spectre.AutoCompletion
@krisrok