spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

AutoCompletion Support #2

Open JKamsker opened this issue 2 years ago • 32 comments

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.

JKamsker avatar Jul 19 '23 14:07 JKamsker

@microsoft-github-policy-service agree

JKamsker avatar Jul 19 '23 14:07 JKamsker

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

JKamsker avatar Jul 19 '23 15:07 JKamsker

Commit https://github.com/JKamsker/spectre.console/commit/a39517fe91d1c8f5fce4de20ec5644dcc4a03e59 implements static completions, attribute based grafik

Objections? anyone? @patriksvensson @phil-scott-78

JKamsker avatar Jul 20 '23 20:07 JKamsker

@patriksvensson Finished from my side, can i get a review?

JKamsker avatar Jul 24 '23 16:07 JKamsker

@JKamsker Currently on vacation but will get back to the computer in a week or so.

patriksvensson avatar Jul 25 '23 15:07 patriksvensson

@JKamsker Currently on vacation but will get back to the computer in a week or so.

Awesome! Looking forward to it!

JKamsker avatar Jul 25 '23 15:07 JKamsker

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

JKamsker avatar Jul 30 '23 23:07 JKamsker

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

JKamsker avatar Aug 07 '23 17:08 JKamsker

@patriksvensson Status?

JKamsker avatar Sep 14 '23 21:09 JKamsker

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 avatar Sep 15 '23 06:09 nils-a

@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.

JKamsker avatar Sep 15 '23 06:09 JKamsker

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 avatar Sep 15 '23 07:09 nils-a

@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

JKamsker avatar Sep 15 '23 07:09 JKamsker

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 avatar Sep 15 '23 07:09 nils-a

@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

JKamsker avatar Sep 15 '23 07:09 JKamsker

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 avatar Sep 15 '23 08:09 FrankRay78

@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"

JKamsker avatar Sep 15 '23 09:09 JKamsker

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.

FrankRay78 avatar Sep 15 '23 12:09 FrankRay78

@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 avatar Sep 15 '23 18:09 nils-a

@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

JKamsker avatar Sep 15 '23 19:09 JKamsker

@nils-a no idea :/ in my tool it works....does just .\JavaVersionSwitcher.exe completion powershell | iex work?

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 avatar Sep 15 '23 20:09 nils-a

scropio

@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

JKamsker avatar Sep 15 '23 20:09 JKamsker

image

I like it. Will do more tests tomorrow.

nils-a avatar Sep 15 '23 20:09 nils-a

@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.

JKamsker avatar Sep 19 '23 17:09 JKamsker

@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:

JKamsker avatar Sep 24 '23 18:09 JKamsker

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 avatar Nov 08 '23 08:11 alex-fomin

@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!

JKamsker avatar Nov 08 '23 09:11 JKamsker

@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.

JKamsker avatar Nov 19 '23 21:11 JKamsker

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.

krisrok avatar Nov 20 '23 21:11 krisrok

Now published as separate repo. https://github.com/JKamsker/JKToolKit.Spectre.AutoCompletion

@krisrok

JKamsker avatar Mar 12 '24 21:03 JKamsker