-
Notifications
You must be signed in to change notification settings - Fork 995
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 support for completions in fish
shell + a few dev scripts to ease workflow
#2281
base: main
Are you sure you want to change the base?
Changes from 4 commits
e846cf6
c0ee6e8
d84dc66
4513d9b
6cb69fb
2c144c4
1fd530a
f37ee6f
99b8281
6ae7a5e
e0e6f7f
c04439a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ package-lock.json | |
example.* | ||
.DS_Store | ||
.npmrc | ||
*.tgz |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,3 +47,23 @@ _{{app_name}}_yargs_completions() | |
compdef _{{app_name}}_yargs_completions {{app_name}} | ||
###-end-{{app_name}}-completions-### | ||
`; | ||
|
||
export const completionFishTemplate = `### {{app_name}} completion - begin. generated by omelette.js ### | ||
# | ||
# yargs command completion script | ||
# | ||
# Installation: {{app_name}} {{completion_command}} >> ~/.config/fish/config.fish. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed a completions folder when I installed fish to try this out. Would it be more appropriate to suggest writing the completion into https://fishshell.com/docs/current/completions.html#where-to-put-completions |
||
# | ||
function _{{app_name}}_yargs_completion | ||
set cmd (commandline -b) | ||
for arg in (string split " " $cmd) | ||
set -a args "'$arg'" | ||
end | ||
set completions (eval {{app_name}} --get-yargs-completions $args) | ||
for completion in $completions | ||
echo -e $completion | ||
end | ||
end | ||
complete -f -c {{app_name}} -a '(_{{app_name}}_yargs_completion)' | ||
### {{app_name}} completion - end ### | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,12 +18,16 @@ type CompletionCallback = ( | |
/** Instance of the completion module. */ | ||
export interface CompletionInstance { | ||
completionKey: string; | ||
|
||
generateCompletionScript($0: string, cmd: string): string; | ||
|
||
getCompletion( | ||
args: string[], | ||
done: (err: Error | null, completions: string[] | undefined) => void | ||
): any; | ||
|
||
registerFunction(fn: CompletionFunction): void; | ||
|
||
setParsed(parsed: DetailedArguments): void; | ||
} | ||
|
||
|
@@ -34,6 +38,8 @@ export class Completion implements CompletionInstance { | |
private customCompletionFunction: CompletionFunction | null = null; | ||
private indexAfterLastReset = 0; | ||
private readonly zshShell: boolean; | ||
private readonly fishShell: boolean; | ||
private readonly bashShell: boolean; | ||
|
||
constructor( | ||
private readonly yargs: YargsInstance, | ||
|
@@ -45,6 +51,10 @@ export class Completion implements CompletionInstance { | |
(this.shim.getEnv('SHELL')?.includes('zsh') || | ||
this.shim.getEnv('ZSH_NAME')?.includes('zsh')) ?? | ||
false; | ||
this.fishShell = this.shim.getEnv('SHELL')?.includes('fish') ?? false; | ||
this.bashShell = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get the intent, but the combination of the name |
||
this.shim.getEnv('SHELL')?.includes('bash') ?? | ||
(!this.zshShell && !this.fishShell); | ||
} | ||
|
||
private defaultCompletion( | ||
|
@@ -92,8 +102,11 @@ export class Completion implements CompletionInstance { | |
this.usage.getCommands().forEach(usageCommand => { | ||
const commandName = parseCommand(usageCommand[0]).cmd; | ||
if (args.indexOf(commandName) === -1) { | ||
if (!this.zshShell) { | ||
if (this.bashShell) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is hard to reason about what shell gets handled where here. The original code had basically zsh and not zsh. I suggest keep the same sort of logic, so it is a more obvious diff, and easier to understand what gets handled where.
|
||
completions.push(commandName); | ||
} else if (this.fishShell) { | ||
const desc = usageCommand[1] || ''; | ||
completions.push(commandName.replace(/:/g, '\\:') + '\t' + desc); | ||
} else { | ||
const desc = usageCommand[1] || ''; | ||
completions.push(commandName.replace(/:/g, '\\:') + ':' + desc); | ||
|
@@ -107,7 +120,7 @@ export class Completion implements CompletionInstance { | |
private optionCompletions( | ||
completions: string[], | ||
args: string[], | ||
argv: Arguments, | ||
_argv: Arguments, | ||
current: string | ||
) { | ||
if ( | ||
|
@@ -119,7 +132,7 @@ export class Completion implements CompletionInstance { | |
this.yargs.getGroups()[this.usage.getPositionalGroupName()] || []; | ||
|
||
Object.keys(options.key).forEach(key => { | ||
const negable = | ||
const negatable = | ||
!!options.configuration['boolean-negation'] && | ||
options.boolean.includes(key); | ||
const isPositionalKey = positionalKeys.includes(key); | ||
|
@@ -128,11 +141,12 @@ export class Completion implements CompletionInstance { | |
if ( | ||
!isPositionalKey && | ||
!options.hiddenOptions.includes(key) && | ||
!this.argsContainKey(args, key, negable) | ||
!this.argsContainKey(args, key, negatable) | ||
) { | ||
this.completeOptionKey(key, completions, current); | ||
if (negable && !!options.default[key]) | ||
if (negatable && !!options.default[key]) { | ||
this.completeOptionKey(`no-${key}`, completions, current); | ||
} | ||
} | ||
}); | ||
} | ||
|
@@ -188,15 +202,19 @@ export class Completion implements CompletionInstance { | |
} | ||
|
||
private getPreviousArgChoices(args: string[]): string[] | void { | ||
if (args.length < 1) return; // no args | ||
if (args.length < 1) { | ||
return; | ||
} // no args | ||
let previousArg = args[args.length - 1]; | ||
let filter = ''; | ||
// use second to last argument if the last one is not an option starting with -- | ||
if (!previousArg.startsWith('-') && args.length > 1) { | ||
filter = previousArg; // use last arg as filter for choices | ||
previousArg = args[args.length - 2]; | ||
} | ||
if (!previousArg.startsWith('-')) return; // still no valid arg, abort | ||
if (!previousArg.startsWith('-')) { | ||
return; | ||
} // still no valid arg, abort | ||
const previousArgKey = previousArg.replace(/^-+/, ''); | ||
|
||
const options = this.yargs.getOptions(); | ||
|
@@ -230,15 +248,21 @@ export class Completion implements CompletionInstance { | |
private argsContainKey( | ||
args: string[], | ||
key: string, | ||
negable: boolean | ||
negatable: boolean | ||
): boolean { | ||
const argsContains = (s: string) => | ||
args.indexOf((/^[^0-9]$/.test(s) ? '-' : '--') + s) !== -1; | ||
if (argsContains(key)) return true; | ||
if (negable && argsContains(`no-${key}`)) return true; | ||
if (argsContains(key)) { | ||
return true; | ||
} | ||
if (negatable && argsContains(`no-${key}`)) { | ||
return true; | ||
} | ||
if (this.aliases) { | ||
for (const alias of this.aliases[key]) { | ||
if (argsContains(alias)) return true; | ||
if (argsContains(alias)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
|
@@ -255,8 +279,14 @@ export class Completion implements CompletionInstance { | |
const isShortOption = (s: string) => /^[^0-9]$/.test(s); | ||
const dashes = | ||
!startsByTwoDashes(current) && isShortOption(key) ? '-' : '--'; | ||
if (!this.zshShell) { | ||
if (this.bashShell) { | ||
completions.push(dashes + key); | ||
} else if (this.fishShell) { | ||
const desc = descs[key] || ''; | ||
completions.push( | ||
dashes + | ||
`${key.replace(/:/g, '\\:')}\t${desc.replace('__yargsString__:', '')}` | ||
); | ||
} else { | ||
const desc = descs[key] || ''; | ||
completions.push( | ||
|
@@ -333,17 +363,22 @@ export class Completion implements CompletionInstance { | |
|
||
// generate the completion script to add to your .bashrc. | ||
generateCompletionScript($0: string, cmd: string): string { | ||
let script = this.zshShell | ||
const script = this.zshShell | ||
? templates.completionZshTemplate | ||
: this.fishShell | ||
? templates.completionFishTemplate | ||
: templates.completionShTemplate; | ||
const name = this.shim.path.basename($0); | ||
|
||
// add ./ to applications not yet installed as bin. | ||
if ($0.match(/\.js$/)) $0 = `./${$0}`; | ||
if ($0.match(/\.js$/)) { | ||
$0 = `./${$0}`; | ||
} | ||
|
||
script = script.replace(/{{app_name}}/g, name); | ||
script = script.replace(/{{completion_command}}/g, cmd); | ||
return script.replace(/{{app_path}}/g, $0); | ||
return script | ||
.replace(/{{app_name}}/g, name) | ||
.replace(/{{completion_command}}/g, cmd) | ||
.replace(/{{app_path}}/g, $0); | ||
} | ||
|
||
// register a function to perform your own custom | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,6 @@ | ||||||
{ | ||||||
"name": "yargs", | ||||||
"version": "17.6.2", | ||||||
"version": "17.6.3-0", | ||||||
jglanz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
"description": "yargs the modern, pirate-themed, successor to optimist.", | ||||||
"main": "./index.cjs", | ||||||
"exports": { | ||||||
|
@@ -76,6 +76,7 @@ | |||||
"gts": "^3.0.0", | ||||||
"hashish": "0.0.4", | ||||||
"mocha": "^9.0.0", | ||||||
"npm-run-all": "^4.1.5", | ||||||
"rimraf": "^3.0.2", | ||||||
"rollup": "^2.23.0", | ||||||
"rollup-plugin-cleanup": "^3.1.1", | ||||||
|
@@ -94,10 +95,13 @@ | |||||
"coverage": "c8 report --check-coverage", | ||||||
"prepare": "npm run compile", | ||||||
"pretest": "npm run compile -- -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", | ||||||
"compile": "rimraf build && tsc", | ||||||
"compile": "rimraf build && tsc -b tsconfig.json", | ||||||
"compile:watch": "npm run compile -- --watch", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So if the combination of comments makes sense, I suggest
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realised I am comfortable passing command-line flags into |
||||||
"postcompile": "npm run build:cjs", | ||||||
"build:cjs": "rollup -c rollup.config.cjs", | ||||||
"build:cjs:watch": "rollup -w -c rollup.config.cjs", | ||||||
"postbuild:cjs": "rimraf ./build/index.cjs.d.ts", | ||||||
"dev": "npm run compile && run-p compile:watch build:cjs:watch", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the current scripts mean this will run compile twice, and invokes rimraf twice. Maybe do the rimraf up front and remove it from the watch scripts. (Disclaimer: making these suggestions online without trying it for real!)
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is |
||||||
"check": "gts lint && npm run check:js", | ||||||
"check:js": "eslint . --ext cjs --ext mjs --ext js", | ||||||
"clean": "gts clean" | ||||||
|
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.
Although we may well wish to acknowledge Omelette somewhere, it seems misleading to say "generated by omelette.js" when it was generated by Yargs?