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

feat: completion (experimental) #89

Open
wants to merge 125 commits into
base: master
Choose a base branch
from

Conversation

paul-soporan
Copy link
Contributor

@paul-soporan paul-soporan commented May 30, 2021

This PR implements support for shell completions of all argument types. It's still a WIP, but I'm getting really really really close to having the first iteration ready for review.

Note: This PR also fixes some small bugs and improves various errors.

TODO list

Required before merging this PR:

  • Investigate the behavior of -<tab>
  • Implement PartialCommand type correctly
  • Make sure that completions are sorted the same way across all shells
  • Rework and finalize API of completion commands
  • Code cleanup
  • Documentation (will document before we mark it as stable)
  • Demo (will add a demo before we mark it as stable)

Nice to have: (can be implemented after this PR is merged)

  • Escape shell completions according to each shell's rules
  • Complete all paths
  • Complete all long option names
  • Investigate possible bug where rest consumes trailing positionals (not introduced by this PR so can be left for another time)
  • Look into how each shell aggregates partially overlapping results and see whether we need to do anything to improve the way the completions are displayed
  • Option.stringify
  • Make clipanion display how much time is spent executing each completion function when in debug mode
  • Support PowerShell's CompletionType
  • Sort the completions
  • Cache the completions

@paul-soporan paul-soporan marked this pull request as ready for review January 17, 2022 23:25
@arcanis
Copy link
Owner

arcanis commented Jan 19, 2022

Really impressive work; I didn't look at clcs in details yet, but the changes in Clipanion look mostly fine (just the part about the callbacks being non-serializables; since serializing the state machine didn't happen so far I wouldn't be too sad to factor it away if it improves performances, but I'd prefer if it was in a separate PR, if ever) and it works well on my zsh.

Some extra remarks:

  • It seems there's a bug if a command has options that have validators attached. The demo didn't work until I removed the maxRetries option. I suspect it's because it causes the code to throw.

  • There's some documentation to be made around the fact that the binary must have a name; in particular, the yarn <bin name> story will have to be figured out (although I suspect it'll be much easier than yarn <script name>, which is perhaps too much to ask).

Really, impressive work as always!

sources/core.ts Outdated Show resolved Hide resolved
@@ -867,6 +968,19 @@ export class CommandBuilder<Context> {

for (let t = 0; t < path.length; ++t) {
const nextPathNode = injectNode(machine, makeNode());

// TODO: find a way to not leak command into the core
registerDynamic(machine, lastPathNode, `isCompletion`, NODE_SUCCESS, [`setCompletion`, CompletionType.PathSegment, (request, command) => {
Copy link
Owner

Choose a reason for hiding this comment

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

The main reason why tuples are passed to registerDynamic was to make the state machine serializable. We can't pass a callback as parameter for this to stay true

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel like it would make more sense to make the state machine serializable again in a different PR when we actually start using the serializability property - the implementation would vary depending on the specific use case.

For example, I could have a workaround to not passing callbacks but it would only make the code more complicated for no reason for now, and if in the future we'd want to pre-generate the state machine for example and hydrate it at runtime the implementation would be incompatible and would have to be changed anyways.

registerDynamic(machine, node, [`isBatchOption`, this.allOptionNames], node, `pushBatch`);
registerDynamic(machine, node, [`isBoundOption`, this.allOptionNames, this.options], node, `pushBound`);
registerDynamic(machine, node, [`isBatchOption`, this.allOptionNames], node, [`chain`, [
[`setBatchCompletion`, this],
Copy link
Owner

Choose a reason for hiding this comment

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

Same thing, passing this will prevent serialization

},
"scripts": {
"prepack": "rm -rf lib && rollup -c",
"postpack": "rm -rf lib",
"test": "FORCE_COLOR=1 mocha --require ts-node/register --extension ts tests",
"test:clipanion": "FORCE_COLOR=1 mocha --require ts-node/register tests/**/*.test.ts",
"test": "yarn test:clipanion & yarn test:clcs &",
Copy link
Owner

Choose a reason for hiding this comment

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

I think mocha supports --parallel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like I get TypeError: The "options" argument must be of type object. Received an instance of Array and Error: write EPIPE when using --parallel, no idea why

})).to.deep.equal([`sources`]);
});

it(`shouldn't spend time filtering out completions that don't match as that's the job of the shell scripts`, async () => {
Copy link
Owner

@arcanis arcanis Jan 19, 2022

Choose a reason for hiding this comment

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

Wouldn't it be done faster by JS rather than shells? (Plus, it could remove many entries that wouldn't have to go through the stdout stream, which isn't the fastest operation either)

completionCleanupCommandPaths,
completionProviderCommandPaths,
completionRequestCommandPaths,
completionDebugCommandPaths,
Copy link
Owner

Choose a reason for hiding this comment

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

Those options are a bit long; can't we just have a single one that would operate as a configurable prefix? I use a similar pattern in Griselbrand:

https://github.com/arcanis/griselbrand/blob/main/sources/index.ts#L71

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I need the granularity for Yarn (you might remember the discussion about @yarnpkg/completion-engine & friends). I think this is enough for the Yarn use case, and the common case that will satisfy the needs of 99% of CLIs is already implemented (the current path defaults).

completion is already the best namespace possible, so if a project deviates from it, it most likely will have to customize more than just the mount point (e.g. the magic required for Yarn completion).

If in the future a common use case for a common mount point arises, we can implement it, but for now I don't really see the need.

// We make sure that we don't register a completion provider for an invalid binary before writing to the shell configuration file.
validateBinaryName(this.cli.binaryName);

return await setupShellConfigurationFile({
Copy link
Owner

Choose a reason for hiding this comment

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

Nothing is currently printed; I think it'd be a good idea to print some basic information: which shell we detected, where we're going to write things, which command to run to remove it and/or update it.

validateBinaryName(this.cli.binaryName);

return await setupShellConfigurationFile({
completionProviderCommand: [this.cli.binaryName, ...completionProviderCommandPaths[0] ?? []].join(` `),
Copy link
Owner

Choose a reason for hiding this comment

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

Question for a followup: I sometimes have CLI defined in my package.json; something like this:

"scripts": {
  "cli": "node ./scripts/cli.js"
}

In those cases, I define binaryName as yarn cli. How would that work here? Is it something that should be implemented in Yarn or Clipanion (perhaps both: Yarn could somehow signal the "real" binary name to Clipanion, and use its proxy completion to get the real one)?

@arcanis
Copy link
Owner

arcanis commented Jan 19, 2022

Oh, something else I forgot to add: I noticed that completions can merge options & non-options. I wonder if we could improve that to only show non-options if there's any (with the assumption that if someone wants to complete an option, it's not unreasonable they'd solve the ambiguity by first typing the leading -). I suspect the results would more closely match the expectations:

image

@paul-soporan
Copy link
Contributor Author

it's not unreasonable they'd solve the ambiguity by first typing the leading -

That's already what happens, we only complete options if a - is typed, otherwise we only complete path segments and positionals. The thing is that the clipanion builtins that look like options (--help, --version, and --clipanion=definitions) are actually path segments, not options. Having special treatment for path segments that look like options is on my TODO list, but I decided to leave it for a future iteration.

@arcanis
Copy link
Owner

arcanis commented Jan 19, 2022

I see, that's surprising at first but it makes sense 👍

@paul-soporan
Copy link
Contributor Author

It seems there's a bug if a command has options that have validators attached.

Totally forgot about validators, I'll need a bit of time to think about how validators should behave with PartialCommand. I'm thinking about disabling the validators while completing things but there's also the problem of coercions 🤔

There's some documentation to be made around the fact that the binary must have a name;

I'll leave all documentation for future PRs.

in particular, the yarn story will have to be figured out (although I suspect it'll be much easier than yarn <script name>, which is perhaps too much to ask).

I'll continue the discussion about this in the review conversation about binaryName.

@arcanis arcanis mentioned this pull request Jul 22, 2023
@arcanis arcanis mentioned this pull request Oct 6, 2023
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.

3 participants