-
Notifications
You must be signed in to change notification settings - Fork 66
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
base: master
Are you sure you want to change the base?
Conversation
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:
Really, impressive work as always! |
@@ -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) => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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], |
There was a problem hiding this comment.
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 &", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
tests/completion.test.ts
Outdated
})).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 () => { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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(` `), |
There was a problem hiding this comment.
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)?
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 |
That's already what happens, we only complete options if a |
I see, that's surprising at first but it makes sense 👍 |
Totally forgot about validators, I'll need a bit of time to think about how validators should behave with
I'll leave all documentation for future PRs.
I'll continue the discussion about this in the review conversation about |
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:
-<tab>
PartialCommand
type correctlyDocumentation(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)
Option.stringify
CompletionType