@@ -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-
3022function __${ nameForVar } _debug
3123 set -l file "$BASH_COMP_DEBUG_FILE"
3224 if test -n "$file"
@@ -37,134 +29,227 @@ end
3729function __${ 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
165230end
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