Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-completion #7

Merged
merged 15 commits into from
Aug 1, 2024
Merged

Add auto-completion #7

merged 15 commits into from
Aug 1, 2024

Conversation

ammario
Copy link
Member

@ammario ammario commented Mar 15, 2024

TODO:

  • Add completion handler to Command
  • Thoroughly test interface on coder/coder
  • Find a clean way to skip flag parsing when invoked for auto-completion
  • Find a clean way for commands to give their own auto-complete suggestions
  • The completion command should have an interactive install by default

Closes #3

@ethanndickson
Copy link
Member

ethanndickson commented Jun 26, 2024

Few things to note after spending some time on this:

  • Are we sure we want to supply a default completion command?
    I'm in favour of just exposing an API for retrieving all the info one might need to create the command.
    Library users will likely want to apply their own styling if it's interactive. Even in coder/coder we want to just reuse the cliui boilerplate we have, and I'm not sure we want to recreate that here, or even import it?

  • To my surprise, if you disable path auto-completion in fish's complete using -f, fish just straight up ignores arguments if it thinks you're trying to autocomplete a path:
    e.g, I have some uncommitted fish+go code responsible for figuring out the current word:
    COMPLETION_MODE=1 CURSOR_POS=21 completetest file ../ will print files in the parent directory, using the Go file handler.
    Hitting tab on completetest file ../ will not give any suggestions.
    This means the protocol we use needs a way to tell the shell whether it should use it's file auto-completion. I haven't looked at the bash implementation all that much, but this is currently what Cobra does for all the shells it supports, and I do think it's better than implementing our own path completer in Go.

  • I'd like to get auto-completions in the middle of a line working - I'm currently passing the cursor position to serpent, but I think it'd be better to just pass the args until the currently selected character.

  • I wanted to keep the Go API for completions similar to the middleware API, allowing users to compose completion handler functions, but I'm not sure there's that great of a need? The only use case I can envision for composing is when you have a command that has both arguments, and a subcommand with different arguments. Then you would compose a default handler (flags + subcommands) with a custom handler (for your arguments). This doesn't sound like a super popular pattern, afaik there's nothing in coder/coder that does something like this, so I think a single handler for a Command and a single handler for an Option (that has a flag) will do.
    We can then supply convenience functions for these handlers like func EnumHandler(choices ...string) CompHandler.

Let me know if you have any thoughts. I'll make a commit for feedback once I have a bare minimum prototype that I'm happy with.

@ethanndickson ethanndickson self-assigned this Jun 26, 2024
@ammario
Copy link
Member Author

ammario commented Jun 26, 2024

Are we sure we want to supply a default completion command?
I'm in favour of just exposing an API for retrieving all the info one might need to create the command.
Library users will likely want to apply their own styling if it's interactive. Even in coder/coder we want to just reuse the cliui boilerplate we have, and I'm not sure we want to recreate that here, or even import it?

Nope, I'm game to provide the tools for people to craft their own command, but then we should put a complete example in the README.

To my surprise, if you disable path auto-completion in fish's complete using -f, fish just straight up ignores arguments if it thinks you're trying to autocomplete a path:
e.g, I have some uncommitted fish+go code responsible for figuring out the current word:
COMPLETION_MODE=1 CURSOR_POS=21 completetest file ../ will print files in the parent directory, using the Go file handler.
Hitting tab on completetest file ../ will not give any suggestions.
This means the protocol we use needs a way to tell the shell whether it should use it's file auto-completion. I haven't looked at the bash implementation all that much, but this is currently what Cobra does for all the shells it supports, and I do think it's better than implementing our own path completer in Go.

It's been a while since I was looking at this, but I came to the conclusion that it's better to disable the path completion and do it all within Go since that's pretty easy to implement and then users can easily add filters like "only accept accept directory".

I'd like to get auto-completions in the middle of a line working - I'm currently passing the cursor position to serpent, but I think it'd be better to just pass the args until the currently selected character.

I struggled with this too. I think doing middle-line completion correctly will involve a larger refactor to argument parsing. I think it makes sense, for MVP, to just prune args to cursor.

I wanted to keep the Go API for completions similar to the middleware API, allowing users to compose completion handler functions, but I'm not sure there's that great of a need? The only use case I can envision for composing is when you have a command that has both arguments, and a subcommand with different arguments. Then you would compose a default handler (flags + subcommands) with a custom handler (for your arguments). This doesn't sound like a super popular pattern, afaik there's nothing in coder/coder that does something like this, so I think a single handler for a Command and a single handler for an Option (that has a flag) will do.
We can then supply convenience functions for these handlers like func EnumHandler(choices ...string) CompHandler.

Yeah, we don't need to optimize for composition in the MVP.

Bear in mind that coder/coder is the only major user of the library, and it's OK if we make breaking changes down the road.

@ethanndickson
Copy link
Member

ethanndickson commented Jul 12, 2024

@ammario
This should be all the core functionality, can merge once you're happy with it :)
Nice-to-haves like descriptions can come in a future PR.

command.go Outdated
// When invoked `WithOS`, this includes argv[0], otherwise it is the same as Args.
AllArgs []string
// CurWord is the word the terminal cursor is currently in
CurWord string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can CurWord be an index into AllArgs? This provides future compatibility with mid-line completions.

Copy link
Member

@ethanndickson ethanndickson Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get auto-complete for equals flags (--flag=) I'm currently just setting this to empty string before we call any handlers. If we don't do this then anyone writing a flag completion handler would sometimes see --flag=<arg> as the current word, and other times just <arg>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what use-case for mid-line completions isn't handled by just truncating the line at the cursor? Everything I tried just works.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok. If we're truncating the line at the cursor then CurWord's only purpose is to distinguish the true flag value? That makes sense, I think it warrants a comment.

}

func ListFiles(word string, filter func(info os.FileInfo) bool) []string {
out := make([]string, 0, 32)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32 initial cap seems arbitrary?

if there's a good reason it should become a comment, otherwise make this var out []string to reduce cognitive overhead.

Copy link
Member

@ethanndickson ethanndickson Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's arbitrary, I just wanted to avoid allocing the array 5 times for 16 files. Assuming var out []string gives us a zero capacity: 0->1->2->4->8->16.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too minor to hold up the PR, but what leads you to optimize around that number of files?

Copy link
Member

@ethanndickson ethanndickson Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just naively based off the fact that a 0 capacity slice grows 'slowly' for the first 32 elements. Though I also could have just set the capacity to the length of the slice returned by Readdir. I'll go with just a var out []string since it's not hugely important.

@ammario ammario requested review from ethanndickson and removed request for ethanndickson July 12, 2024 20:55
@ammario
Copy link
Member Author

ammario commented Jul 15, 2024

unfortunately GitHub doesn't let me be a "reviewer" on my own PR. On future hand-offs we should probably recreate the PR. In this one, just tag me via comment when ready for additional review.

@ethanndickson
Copy link
Member

ethanndickson commented Jul 17, 2024

Yeah I'm happy with these changes, pls review 🙏 @ammario

command.go Outdated
// When invoked `WithOS`, this includes argv[0], otherwise it is the same as Args.
AllArgs []string
// CurWord is the word the terminal cursor is currently in
CurWord string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok. If we're truncating the line at the cursor then CurWord's only purpose is to distinguish the true flag value? That makes sense, I think it warrants a comment.

}

func ListFiles(word string, filter func(info os.FileInfo) bool) []string {
out := make([]string, 0, 32)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too minor to hold up the PR, but what leads you to optimize around that number of files?

@ethanndickson
Copy link
Member

@ammario

@ammario
Copy link
Member Author

ammario commented Aug 1, 2024

Hey Ethan, sorry for the late review. I just skimmed the changes so we can get this in. I assume you're going to be integrating this into coder/coder next?

@ammario ammario merged commit 91966a2 into main Aug 1, 2024
1 check passed
@ethanndickson
Copy link
Member

Yep, when my backlog clears up that'll be next, serpent version bump + interactive install.

@ammario ammario deleted the completion branch August 2, 2024 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug: coder completion sub-command no longer available
2 participants