Skip to content

Commit 23a6ef9

Browse files
authored
fix: update fish script based on cobra (#100)
* fix: update fish script based on cobra * fix: lint * chore: add changeset
1 parent efaa060 commit 23a6ef9

File tree

4 files changed

+710
-321
lines changed

4 files changed

+710
-321
lines changed

.changeset/witty-shoes-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bomb.sh/tab': patch
3+
---
4+
5+
Update fish shell completion script to match latest Cobra output. (#99)

src/fish.ts

Lines changed: 192 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@ export function generate(name: string, exec: string): string {
1919

2020
return `# fish completion for ${name} -*- shell-script -*-
2121
22-
# Define shell completion directives
23-
set -l ShellCompDirectiveError ${ShellCompDirectiveError}
24-
set -l ShellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace}
25-
set -l ShellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp}
26-
set -l ShellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt}
27-
set -l ShellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs}
28-
set -l ShellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder}
29-
3022
function __${nameForVar}_debug
3123
set -l file "$BASH_COMP_DEBUG_FILE"
3224
if test -n "$file"
@@ -37,134 +29,227 @@ end
3729
function __${nameForVar}_perform_completion
3830
__${nameForVar}_debug "Starting __${nameForVar}_perform_completion"
3931
40-
# Extract all args except the completion flag
41-
set -l args (string match -v -- "--completion=" (commandline -opc))
42-
43-
# Extract the current token being completed
44-
set -l current_token (commandline -ct)
45-
46-
# Check if current token starts with a dash
47-
set -l flag_prefix ""
48-
if string match -q -- "-*" $current_token
49-
set flag_prefix "--flag="
50-
end
51-
52-
__${nameForVar}_debug "Current token: $current_token"
53-
__${nameForVar}_debug "All args: $args"
32+
# Extract all args except the last one
33+
set -l args (commandline -opc)
34+
# Extract the last arg and escape it in case it is a space or wildcard
35+
set -l lastArg (string escape -- (commandline -ct))
36+
37+
__${nameForVar}_debug "args: $args"
38+
__${nameForVar}_debug "last arg: $lastArg"
39+
40+
# Build the completion request command
41+
set -l requestComp "${exec} complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg"
5442
55-
# Call the completion program and get the results
56-
set -l requestComp "${exec} complete -- $args"
5743
__${nameForVar}_debug "Calling $requestComp"
5844
set -l results (eval $requestComp 2> /dev/null)
59-
45+
6046
# Some programs may output extra empty lines after the directive.
6147
# Let's ignore them or else it will break completion.
6248
# Ref: https://github.com/spf13/cobra/issues/1279
6349
for line in $results[-1..1]
64-
if test (string sub -s 1 -l 1 -- $line) = ":"
65-
# The directive
66-
set -l directive (string sub -s 2 -- $line)
67-
set -l directive_num (math $directive)
50+
if test (string trim -- $line) = ""
51+
# Found an empty line, remove it
52+
set results $results[1..-2]
53+
else
54+
# Found non-empty line, we have our proper output
6855
break
6956
end
7057
end
71-
72-
# No directive specified, use default
73-
if not set -q directive_num
74-
set directive_num 0
58+
59+
set -l comps $results[1..-2]
60+
set -l directiveLine $results[-1]
61+
62+
# For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
63+
# completions must be prefixed with the flag
64+
set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
65+
66+
__${nameForVar}_debug "Comps: $comps"
67+
__${nameForVar}_debug "DirectiveLine: $directiveLine"
68+
__${nameForVar}_debug "flagPrefix: $flagPrefix"
69+
70+
for comp in $comps
71+
printf "%s%s\\n" "$flagPrefix" "$comp"
7572
end
76-
77-
__${nameForVar}_debug "Directive: $directive_num"
7873
79-
# Process completions based on directive
80-
if test $directive_num -eq $ShellCompDirectiveError
81-
# Error code. No completion.
82-
__${nameForVar}_debug "Received error directive: aborting."
74+
printf "%s\\n" "$directiveLine"
75+
end
76+
77+
# This function limits calls to __${nameForVar}_perform_completion, by caching the result
78+
function __${nameForVar}_perform_completion_once
79+
__${nameForVar}_debug "Starting __${nameForVar}_perform_completion_once"
80+
81+
if test -n "$__${nameForVar}_perform_completion_once_result"
82+
__${nameForVar}_debug "Seems like a valid result already exists, skipping __${nameForVar}_perform_completion"
83+
return 0
84+
end
85+
86+
set --global __${nameForVar}_perform_completion_once_result (__${nameForVar}_perform_completion)
87+
if test -z "$__${nameForVar}_perform_completion_once_result"
88+
__${nameForVar}_debug "No completions, probably due to a failure"
8389
return 1
8490
end
8591
86-
# Filter out the directive (last line)
87-
if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":"
88-
set results $results[1..-2]
92+
__${nameForVar}_debug "Performed completions and set __${nameForVar}_perform_completion_once_result"
93+
return 0
94+
end
95+
96+
# This function is used to clear the cached result after completions are run
97+
function __${nameForVar}_clear_perform_completion_once_result
98+
__${nameForVar}_debug ""
99+
__${nameForVar}_debug "========= clearing previously set __${nameForVar}_perform_completion_once_result variable =========="
100+
set --erase __${nameForVar}_perform_completion_once_result
101+
__${nameForVar}_debug "Successfully erased the variable __${nameForVar}_perform_completion_once_result"
102+
end
103+
104+
function __${nameForVar}_requires_order_preservation
105+
__${nameForVar}_debug ""
106+
__${nameForVar}_debug "========= checking if order preservation is required =========="
107+
108+
__${nameForVar}_perform_completion_once
109+
if test -z "$__${nameForVar}_perform_completion_once_result"
110+
__${nameForVar}_debug "Error determining if order preservation is required"
111+
return 1
89112
end
90113
91-
# No completions, let fish handle file completions unless forbidden
92-
if test (count $results) -eq 0
93-
if test $directive_num -ne $ShellCompDirectiveNoFileComp
94-
__${nameForVar}_debug "No completions, performing file completion"
95-
return 1
96-
end
97-
__${nameForVar}_debug "No completions, but file completion forbidden"
114+
set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1])
115+
__${nameForVar}_debug "Directive is: $directive"
116+
117+
set -l shellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder}
118+
set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
119+
__${nameForVar}_debug "Keeporder is: $keeporder"
120+
121+
if test $keeporder -ne 0
122+
__${nameForVar}_debug "This does require order preservation"
98123
return 0
99124
end
100125
101-
# Filter file extensions
102-
if test $directive_num -eq $ShellCompDirectiveFilterFileExt
103-
__${nameForVar}_debug "File extension filtering"
104-
set -l file_extensions
105-
for item in $results
106-
if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-"
107-
set -a file_extensions "*$item"
108-
end
109-
end
110-
__${nameForVar}_debug "File extensions: $file_extensions"
111-
112-
# Use the file extensions as completions
113-
set -l completions
114-
for ext in $file_extensions
115-
# Get all files matching the extension
116-
set -a completions (string replace -r '^.*/' '' -- $ext)
117-
end
118-
119-
for item in $completions
120-
echo -e "$item\t"
121-
end
122-
return 0
126+
__${nameForVar}_debug "This doesn't require order preservation"
127+
return 1
128+
end
129+
130+
# This function does two things:
131+
# - Obtain the completions and store them in the global __${nameForVar}_comp_results
132+
# - Return false if file completion should be performed
133+
function __${nameForVar}_prepare_completions
134+
__${nameForVar}_debug ""
135+
__${nameForVar}_debug "========= starting completion logic =========="
136+
137+
# Start fresh
138+
set --erase __${nameForVar}_comp_results
139+
140+
__${nameForVar}_perform_completion_once
141+
__${nameForVar}_debug "Completion results: $__${nameForVar}_perform_completion_once_result"
142+
143+
if test -z "$__${nameForVar}_perform_completion_once_result"
144+
__${nameForVar}_debug "No completion, probably due to a failure"
145+
# Might as well do file completion, in case it helps
146+
return 1
123147
end
124148
125-
# Filter directories
126-
if test $directive_num -eq $ShellCompDirectiveFilterDirs
127-
__${nameForVar}_debug "Directory filtering"
128-
set -l dirs
129-
for item in $results
130-
if test -d "$item"
131-
set -a dirs "$item/"
132-
end
133-
end
134-
135-
for item in $dirs
136-
echo -e "$item\t"
137-
end
138-
return 0
149+
set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1])
150+
set --global __${nameForVar}_comp_results $__${nameForVar}_perform_completion_once_result[1..-2]
151+
152+
__${nameForVar}_debug "Completions are: $__${nameForVar}_comp_results"
153+
__${nameForVar}_debug "Directive is: $directive"
154+
155+
set -l shellCompDirectiveError ${ShellCompDirectiveError}
156+
set -l shellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace}
157+
set -l shellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp}
158+
set -l shellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt}
159+
set -l shellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs}
160+
161+
if test -z "$directive"
162+
set directive 0
163+
end
164+
165+
set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
166+
if test $compErr -eq 1
167+
__${nameForVar}_debug "Received error directive: aborting."
168+
# Might as well do file completion, in case it helps
169+
return 1
139170
end
140171
141-
# Process remaining completions
142-
for item in $results
143-
if test -n "$item"
144-
# Check if the item has a description
145-
if string match -q "*\t*" -- "$item"
146-
set -l completion_parts (string split \t -- "$item")
147-
set -l comp $completion_parts[1]
148-
set -l desc $completion_parts[2]
149-
150-
# Add the completion and description
151-
echo -e "$comp\t$desc"
152-
else
153-
# Add just the completion
154-
echo -e "$item\t"
172+
set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
173+
set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
174+
if test $filefilter -eq 1; or test $dirfilter -eq 1
175+
__${nameForVar}_debug "File extension filtering or directory filtering not supported"
176+
# Do full file completion instead
177+
return 1
178+
end
179+
180+
set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
181+
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
182+
183+
__${nameForVar}_debug "nospace: $nospace, nofiles: $nofiles"
184+
185+
# If we want to prevent a space, or if file completion is NOT disabled,
186+
# we need to count the number of valid completions.
187+
# To do so, we will filter on prefix as the completions we have received
188+
# may not already be filtered so as to allow fish to match on different
189+
# criteria than the prefix.
190+
if test $nospace -ne 0; or test $nofiles -eq 0
191+
set -l prefix (commandline -t | string escape --style=regex)
192+
__${nameForVar}_debug "prefix: $prefix"
193+
194+
set -l completions (string match -r -- "^$prefix.*" $__${nameForVar}_comp_results)
195+
set --global __${nameForVar}_comp_results $completions
196+
__${nameForVar}_debug "Filtered completions are: $__${nameForVar}_comp_results"
197+
198+
# Important not to quote the variable for count to work
199+
set -l numComps (count $__${nameForVar}_comp_results)
200+
__${nameForVar}_debug "numComps: $numComps"
201+
202+
if test $numComps -eq 1; and test $nospace -ne 0
203+
# We must first split on \\t to get rid of the descriptions to be
204+
# able to check what the actual completion will be.
205+
# We don't need descriptions anyway since there is only a single
206+
# real completion which the shell will expand immediately.
207+
set -l split (string split --max 1 "\\t" $__${nameForVar}_comp_results[1])
208+
209+
# Fish won't add a space if the completion ends with any
210+
# of the following characters: @=/:.,
211+
set -l lastChar (string sub -s -1 -- $split)
212+
if not string match -r -q "[@=/:.,]" -- "$lastChar"
213+
# In other cases, to support the "nospace" directive we trick the shell
214+
# by outputting an extra, longer completion.
215+
__${nameForVar}_debug "Adding second completion to perform nospace directive"
216+
set --global __${nameForVar}_comp_results $split[1] $split[1].
217+
__${nameForVar}_debug "Completions are now: $__${nameForVar}_comp_results"
155218
end
156219
end
220+
221+
if test $numComps -eq 0; and test $nofiles -eq 0
222+
# To be consistent with bash and zsh, we only trigger file
223+
# completion when there are no other completions
224+
__${nameForVar}_debug "Requesting file completion"
225+
return 1
226+
end
157227
end
158-
159-
# If directive contains NoSpace, tell fish not to add a space after completion
160-
if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0
161-
return 2
162-
end
163-
228+
164229
return 0
165230
end
166231
167-
# Set up the completion for the ${name} command
168-
complete -c ${name} -f -a "(eval __${nameForVar}_perform_completion)"
232+
# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
233+
# so we can properly delete any completions provided by another script.
234+
# Only do this if the program can be found, or else fish may print some errors; besides,
235+
# the existing completions will only be loaded if the program can be found.
236+
if type -q "${name}"
237+
# The space after the program name is essential to trigger completion for the program
238+
# and not completion of the program name itself.
239+
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
240+
complete --do-complete "${name} " > /dev/null 2>&1
241+
end
242+
243+
# Remove any pre-existing completions for the program since we will be handling all of them.
244+
complete -c ${name} -e
245+
246+
# This will get called after the two calls below and clear the cached result
247+
complete -c ${name} -n '__${nameForVar}_clear_perform_completion_once_result'
248+
# The call to __${nameForVar}_prepare_completions will setup __${nameForVar}_comp_results
249+
# which provides the program's completion choices.
250+
# If this doesn't require order preservation, we don't use the -k flag
251+
complete -c ${name} -n 'not __${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results'
252+
# Otherwise we use the -k flag
253+
complete -k -c ${name} -n '__${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results'
169254
`;
170255
}

0 commit comments

Comments
 (0)