v / cmd / tools / vcomplete.v
810 lines · 789 sloc · 17.86 KB · 6dd9033de29819f9ada4011f783b1d8b9d47d7e8
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4//
5// Utility functions helping integrate with various shell auto-completion systems.
6// The install process and communication is inspired from that of [kitty](https://sw.kovidgoyal.net/kitty/#completion-for-kitty)
7// This method avoids writing and maintaining external files on the user's file system.
8// The user will be responsible for adding a small line to their .*rc - that will ensure *live* (i.e. not-static)
9// auto-completion features.
10//
11// # bash
12// To install auto-completion for V in bash, simply add this code to your `~/.bashrc`:
13// `source /dev/stdin <<<"$(v complete setup bash)"`
14// On more recent versions of bash (>3.2) this should suffice:
15// `source <(v complete setup bash)`
16//
17// # fish
18// For versions of fish <3.0.0, add the following to your `~/.config/fish/config.fish`
19// `v complete setup fish | source`
20// Later versions of fish source completions by default.
21//
22// # zsh
23// To install auto-completion for V in zsh - please add the following to your `~/.zshrc`:
24// ```
25// autoload -Uz compinit
26// compinit
27// # Completion for v
28// v complete setup zsh | source /dev/stdin
29// ```
30// Please note that you should let v load the zsh completions after the call to compinit
31//
32// # powershell
33// To install auto-complete for V in PowerShell, simply do this
34// `v complete setup powershell >> $PROFILE`
35// and reload profile
36// `& $PROFILE`
37// If `$PROFILE` didn't exist yet, create it before
38// `New-Item -Type File -Force $PROFILE`
39//
40module main
41
42import os
43
44const auto_complete_shells = ['bash', 'fish', 'zsh', 'powershell'] // list of supported shells
45
46const vexe = os.getenv('VEXE')
47const help_text = "Usage:
48 v complete [options] [SUBCMD] QUERY...
49
50Description:
51 Tool for bridging auto completion between various shells and v
52
53Supported shells:
54 bash, fish, zsh, powershell
55
56Examples:
57 Echo auto-detected shell install script to STDOUT
58 v complete
59 Echo specific shell install script to STDOUT
60 v complete setup bash
61 Auto complete input `v tes`*USER PUSHES TAB* (in Bash compatible format).
62 This is not meant for manual invocation - it's called by the relevant
63 shell via the script installed with `v complete` or `v complete setup SHELL`.
64 v complete bash v tes
65
66Options:
67 -h, --help Show this help text.
68
69SUBCMD:
70 setup : setup [SHELL] - returns the code for completion setup for SHELL
71 bash : [QUERY] - returns Bash compatible completion code with completions computed from QUERY
72 fish : [QUERY] - returns Fish compatible completion code with completions computed from QUERY
73 zsh : [QUERY] - returns ZSH compatible completion code with completions computed from QUERY
74 powershell: [QUERY] - returns PowerShell compatible completion code with completions computed from QUERY"
75
76// Snooped from cmd/v/v.v, vlib/v/pref/pref.c.v
77const auto_complete_commands = [
78 // simple_cmd
79 'ast',
80 'doc',
81 'vet',
82 // tools in one .v file or folder (typically has a "v" prefix)
83 'bin2v',
84 'bug',
85 'build-examples',
86 'build-tools',
87 'build-vbinaries',
88 'bump',
89 'check-md',
90 'complete',
91 'compress',
92 'cover',
93 'create',
94 'doctor',
95 'download',
96 'fmt',
97 'gret',
98 'git-fmt-hook',
99 'ls',
100 'quest',
101 'retry',
102 'reduce',
103 'repl',
104 'repeat',
105 'self',
106 'setup-freetype',
107 'scan',
108 'shader',
109 'symlink',
110 'test-all',
111 'test-cleancode',
112 'test-fmt',
113 'test-parser',
114 'test-self',
115 'test',
116 'tracev',
117 'up',
118 'watch',
119 'where',
120 'wipe-cache',
121 // commands
122 'help',
123 'new',
124 'init',
125 'translate',
126 'self',
127 'search',
128 'install',
129 'link',
130 'update',
131 'upgrade',
132 'outdated',
133 'list',
134 'remove',
135 'unlink',
136 'vlib-docs',
137 'get',
138 'version',
139 'run',
140 'build',
141 'build-module',
142 'missdoc',
143]
144// Entries in the flag arrays below should be entered as is:
145// * Short flags, e.g.: "-v", should be entered: '-v'
146// * Long flags, e.g.: "--version", should be entered: '--version'
147// * Single-dash flags, e.g.: "-version", should be entered: '-version'
148const auto_complete_flags = [
149 '-wasm-validate',
150 '-wasm-stack-top',
151 '-apk',
152 '-arch',
153 '-assert',
154 '-show-timings',
155 '-show-asserts',
156 '-check-syntax',
157 '-check',
158 '-?',
159 '-h',
160 '-help',
161 '--help',
162 '-q',
163 '-v',
164 '-V',
165 '--version',
166 '-version',
167 '-progress',
168 '-Wimpure-v',
169 '-Wfatal-errors',
170 '-silent',
171 '-skip-running',
172 '-cstrict',
173 '-nofloat',
174 '-fast-math',
175 '-e',
176 '-subsystem',
177 '-gc',
178 '-g',
179 '-cg',
180 '-debug-tcc',
181 '-sourcemap',
182 '-warn-about-allocs',
183 '-sourcemap-src-included',
184 '-sourcemap-inline',
185 '-repl',
186 '-live',
187 '-sharedlive',
188 '-shared',
189 '--enable-globals',
190 '-enable-globals',
191 '-autofree',
192 '-print_autofree_vars',
193 '-print_autofree_vars_in_fn',
194 '-trace-calls',
195 '-trace-fns',
196 '-manualfree',
197 '-skip-unused',
198 '-no-skip-unused',
199 '-compress',
200 '-freestanding',
201 '-no-retry-compilation',
202 '-musl',
203 '-glibc',
204 '-no-bounds-checking',
205 '-no-builtin',
206 '-no-preludes',
207 '-no-relaxed-gcc14',
208 '-prof',
209 '-profile',
210 '-cov',
211 '-coverage',
212 '-profile-fns',
213 '-profile-no-inline',
214 '-prod',
215 '-sanitize',
216 '-simulator',
217 '-stats',
218 '-obf',
219 '-obfuscate',
220 '-hide-auto-str',
221 '-translated',
222 '-translated-go',
223 '-m32',
224 '-m64',
225 '-color',
226 '-nocolor',
227 '-showcc',
228 '-show-c-output',
229 '-show-callgraph',
230 '-show-depgraph',
231 '-run-only',
232 '-exclude',
233 '-test-runner',
234 '-dump-c-flags',
235 '-dump-modules',
236 '-dump-files',
237 '-dump-defines',
238 '-experimental',
239 '-usecache',
240 '-use-os-system-to-run',
241 '-macosx-version-min',
242 '-nocache',
243 '-prealloc',
244 '-no-parallel',
245 '-parallel-cc',
246 '-native',
247 '-W',
248 '-w',
249 '-N',
250 '-n',
251 '-no-rsp',
252 '-no-std',
253 '-keepc',
254 '-watch',
255 '-print-v-files',
256 '-print-watched-files',
257 '-http',
258 '-cross',
259 '-os',
260 '-printfn',
261 '-cflags',
262 '-ldflags',
263 '-d',
264 '-define',
265 '-message-limit',
266 '-thread-stack-size',
267 '-cc',
268 '-c++',
269 '-checker-match-exhaustive-cutoff-limit',
270 '-o',
271 '-output',
272 '-is_o',
273 '-b',
274 '-backend',
275 '-es5',
276 '-path',
277 '-bare-builtin-dir',
278 '-custom-prelude',
279 '-raw-vsh-tmp-prefix',
280 '-cmain',
281 '-line-info',
282 '-check-unused-fn-args',
283 '-check-return',
284 '-use-coroutines',
285]
286const auto_complete_flags_cover = [
287 '--help',
288 '-h',
289 '--verbose',
290 '-v',
291 '--hotspots',
292 '-H',
293 '--percentages',
294 '-P',
295 '--lcov',
296 '--show_test_files',
297 '-S',
298 '--absolute',
299 '-A',
300 '--filter',
301 '-f',
302]
303const auto_complete_flags_doc = [
304 '-all',
305 '-f',
306 '-h',
307 '-help',
308 '-m',
309 '-o',
310 '-readme',
311 '-v',
312 '-filename',
313 '-pos',
314 '-no-timestamp',
315 '-inline-assets',
316 '-theme-dir',
317 '-open',
318 '-p',
319 '-s',
320 '-l',
321]
322const auto_complete_flags_download = [
323 '--help',
324 '-h',
325 '--target-folder',
326 '-t',
327 '--sha1',
328 '-1',
329 '--sha256',
330 '-2',
331 '--continue',
332 '-c',
333 '--retries',
334 '-r',
335 '--delay',
336 '-d',
337]
338const auto_complete_flags_fmt = [
339 '-c',
340 '-diff',
341 '-l',
342 '-w',
343 '-debug',
344 '-verify',
345]
346const auto_complete_flags_bin2v = [
347 '-h',
348 '--help',
349 '-m',
350 '--module',
351 '-p',
352 '--prefix',
353 '-w',
354 '--write',
355]
356const auto_complete_flags_retry = [
357 '--help',
358 '-h',
359 '--timeout',
360 '-t',
361 '--delay',
362 '-d',
363 '--retries',
364 '-r',
365]
366const auto_complete_flags_shader = [
367 '--help',
368 '-h',
369 '--force-update',
370 '-u',
371 '--verbose',
372 '-v',
373 '--slang',
374 '-l',
375 '--output',
376 '-o',
377]
378const auto_complete_flags_missdoc = [
379 '--help',
380 '-h',
381 '--tags',
382 '-t',
383 '--deprecated',
384 '-d',
385 '--private',
386 '-p',
387 '--no-line-numbers',
388 '-n',
389 '--exclude',
390 '-e',
391 '--relative-paths',
392 '-r',
393 '--js',
394 '--verify',
395 '--diff',
396]
397const auto_complete_flags_bump = [
398 '--patch',
399 '--minor',
400 '--major',
401]
402const auto_complete_flags_self = [
403 '-prod',
404]
405const auto_complete_flags_where = [
406 '-h',
407 '-f',
408 '-v',
409]
410const auto_complete_flags_reduce = [
411 '-e',
412 '--error_msg',
413 '-c',
414 '--command',
415 '-w',
416 '--fmt',
417 '--version',
418]
419const auto_complete_flags_repeat = [
420 '--help',
421 '-h',
422 '--runs',
423 '-r',
424 '--series',
425 '-s',
426 '--warmup',
427 '-w',
428 '--newline',
429 '-n',
430 '--output',
431 '-O',
432 '--max_time',
433 '-m',
434 '--fail_percent',
435 '-f',
436 '--template',
437 '-t',
438 '--parameter',
439 '-p',
440 '--nmins',
441 '-i',
442 '--nmaxs',
443 '-a',
444 '--ignore',
445 '-e',
446]
447const auto_complete_compilers = [
448 'cc',
449 'gcc',
450 'tcc',
451 'tinyc',
452 'clang',
453 'mingw',
454 'msvc',
455]
456
457// auto_complete prints auto completion results back to the calling shell's completion system.
458// auto_complete acts as communication bridge between the calling shell and V's completions.
459fn auto_complete(args []string) {
460 if args.len <= 1 || args[0] != 'complete' {
461 if args.len == 1 {
462 shell_path := os.getenv('SHELL')
463 if shell_path != '' {
464 shell_name := os.file_name(shell_path).to_lower()
465 if shell_name in auto_complete_shells {
466 println(setup_for_shell(shell_name))
467 exit(0)
468 }
469 eprintln('Unknown shell ${shell_name}. Supported shells are: ${auto_complete_shells}')
470 exit(1)
471 }
472 eprintln('auto completion require arguments to work.')
473 } else {
474 eprintln('auto completion failed for "${args}".')
475 }
476 exit(1)
477 }
478 sub := args[1]
479 sub_args := args[1..]
480 match sub {
481 'setup' {
482 if sub_args.len <= 1 || sub_args[1] !in auto_complete_shells {
483 eprintln('please specify a shell to setup auto completion for (${auto_complete_shells}).')
484 exit(1)
485 }
486 shell := sub_args[1]
487 println(setup_for_shell(shell))
488 }
489 'bash' {
490 if sub_args.len <= 1 {
491 exit(0)
492 }
493 mut lines := []string{}
494 list := auto_complete_request(sub_args[1..])
495 for entry in list {
496 lines << "COMPREPLY+=('${entry}')"
497 }
498 println(lines.join('\n'))
499 }
500 'fish', 'powershell' {
501 if sub_args.len <= 1 {
502 exit(0)
503 }
504 mut lines := []string{}
505 list := auto_complete_request(sub_args[1..])
506 for entry in list {
507 lines << '${entry}'
508 }
509 println(lines.join('\n'))
510 }
511 'zsh' {
512 if sub_args.len <= 1 {
513 exit(0)
514 }
515 mut lines := []string{}
516 mut dirs := []string{}
517 mut files := []string{}
518 list := auto_complete_request(sub_args[1..])
519 for entry in list {
520 match true {
521 os.is_dir(entry) { dirs << entry }
522 os.is_file(entry) { files << entry }
523 else { lines << entry }
524 }
525 }
526 println('compadd -q -- ${lines.join(' ')}')
527 println('compadd -J "dirs" -X "directory" -d -- ${dirs.join(' ')}')
528 println('compadd -J "files" -X "file" -f -- ${files.join(' ')}')
529 }
530 '-h', '--help' {
531 println(help_text)
532 }
533 else {}
534 }
535
536 exit(0)
537}
538
539// append_separator_if_dir returns the input `path` with an appended
540// `/` or `\`, depending on the platform, when `path` is a directory.
541fn append_separator_if_dir(path string) string {
542 if os.is_dir(path) && !path.ends_with(os.path_separator) {
543 return path + os.path_separator
544 }
545 return path
546}
547
548// nearest_path_or_root returns the nearest valid path searching
549// backwards from `path`.
550fn nearest_path_or_root(path string) string {
551 mut fixed_path := path
552 if !os.is_dir(fixed_path) {
553 fixed_path = path.all_before_last(os.path_separator)
554 if fixed_path == '' {
555 fixed_path = '/'
556 }
557 }
558 return fixed_path
559}
560
561// auto_complete_request returns a list of completions resolved from a full argument list.
562fn auto_complete_request(args []string) []string {
563 // Using space will ensure a uniform input in cases where the shell
564 // returns the completion input as a string (['v','run'] vs. ['v run']).
565 split_by := ' '
566 request := args.join(split_by)
567 mut do_home_expand := false
568 mut list := []string{}
569 // new_part := request.ends_with('\n\n')
570 mut parts := request.trim_right(' ').split(split_by)
571 if parts.len <= 1 { // 'v <tab>' -> top level commands.
572 for command in auto_complete_commands {
573 list << command
574 }
575 } else {
576 mut part := parts.last().trim(' ')
577 mut parent_command := ''
578 for i := parts.len - 1; i >= 0; i-- {
579 if parts[i].starts_with('-') {
580 continue
581 }
582 parent_command = parts[i]
583 break
584 }
585 if part.starts_with('-') { // 'v [subcmd] -<tab>' or 'v [subcmd] --<tab>'-> flags.
586 get_flags := fn (base []string, flag string) []string {
587 mut results := []string{}
588 for entry in base {
589 if entry.starts_with(flag) {
590 results << entry
591 }
592 }
593 return results
594 }
595
596 match parent_command {
597 'bin2v' { // 'v bin2v -<tab>'
598 list = get_flags(auto_complete_flags_bin2v, part)
599 }
600 'bump' { // 'v bump -<tab>' -> flags.
601 list = get_flags(auto_complete_flags_bump, part)
602 }
603 'build' { // 'v build -<tab>' -> flags.
604 list = get_flags(auto_complete_flags, part)
605 }
606 'cover' { // 'v cover -<tab>' -> flags.
607 list = get_flags(auto_complete_flags_cover, part)
608 }
609 'doc' { // 'v doc -<tab>' -> flags.
610 list = get_flags(auto_complete_flags_doc, part)
611 }
612 'download' { // 'v download -<tab>' -> flags.
613 list = get_flags(auto_complete_flags_download, part)
614 }
615 'fmt' { // 'v fmt -<tab>' -> flags.
616 list = get_flags(auto_complete_flags_fmt, part)
617 }
618 'missdoc' { // 'v missdoc -<tab>' -> flags.
619 list = get_flags(auto_complete_flags_missdoc, part)
620 }
621 'reduce' { // 'v reduce -<tab>' -> flags.
622 list = get_flags(auto_complete_flags_reduce, part)
623 }
624 'retry' { // 'v retry -<tab>' -> flags.
625 list = get_flags(auto_complete_flags_retry, part)
626 }
627 'repeat' { // 'v repeat -<tab>' -> flags.
628 list = get_flags(auto_complete_flags_repeat, part)
629 }
630 'self' { // 'v self -<tab>' -> flags.
631 list = get_flags(auto_complete_flags_self, part)
632 }
633 'shader' { // 'v shader -<tab>' -> flags.
634 list = get_flags(auto_complete_flags_shader, part)
635 }
636 'where' { // 'v where -<tab>' -> flags.
637 list = get_flags(auto_complete_flags_where, part)
638 }
639 else {
640 for flag in auto_complete_flags {
641 if flag == part {
642 if flag == '-cc' { // 'v -cc <tab>' -> list of available compilers.
643 for compiler in auto_complete_compilers {
644 path := os.find_abs_path_of_executable(compiler) or { '' }
645 if path != '' {
646 list << compiler
647 }
648 }
649 }
650 } else if flag.starts_with(part) { // 'v -<char(s)><tab>' -> flags matching "<char(s)>".
651 list << flag
652 }
653 }
654 }
655 }
656
657 // Clear the list if the result is identical to the part examined
658 // (the flag must have already been completed)
659 if list.len == 1 && part == list[0] {
660 list.clear()
661 }
662 } else {
663 match part {
664 'help' { // 'v help <tab>' -> top level commands except "help".
665 list = auto_complete_commands.filter(it != part && it != 'complete')
666 }
667 else {
668 // 'v <char(s)><tab>' -> commands matching "<char(s)>".
669 // Don't include if part matches a full command - instead go to path completion below.
670 for command in auto_complete_commands {
671 if part != command && command.starts_with(part) {
672 list << command
673 }
674 }
675 }
676 }
677 }
678 // Nothing of value was found.
679 // Mimic shell dir and file completion
680 if list.len == 0 {
681 mut ls_path := '.'
682 mut collect_all := part in auto_complete_commands
683 mut path_complete := false
684 do_home_expand = part.starts_with('~')
685 if do_home_expand {
686 add_sep := if part == '~' { os.path_separator } else { '' }
687 part = part.replace_once('~', os.home_dir().trim_right(os.path_separator)) + add_sep
688 }
689 is_abs_path :=
690 part.starts_with(os.path_separator) // TODO: Windows support for drive prefixes
691 if part.ends_with(os.path_separator) || part == '.' || part == '..' {
692 // 'v <command>(.*/$|.|..)<tab>' -> output full directory list
693 ls_path = '.' + os.path_separator + part
694 if is_abs_path {
695 ls_path = nearest_path_or_root(part)
696 }
697 collect_all = true
698 } else if !collect_all && part.contains(os.path_separator) && os.is_dir(os.dir(part)) {
699 // 'v <command>(.*/.* && os.is_dir)<tab>' -> output completion friendly directory list
700 if is_abs_path {
701 ls_path = nearest_path_or_root(part)
702 } else {
703 ls_path = os.dir(part)
704 }
705 path_complete = true
706 }
707
708 entries := os.ls(ls_path) or { return list }
709 mut last := part.all_after_last(os.path_separator)
710 if is_abs_path && os.is_dir(part) {
711 last = ''
712 }
713 if path_complete {
714 path := part.all_before_last(os.path_separator)
715 for entry in entries {
716 if entry.starts_with(last) {
717 list << append_separator_if_dir(os.join_path(path, entry))
718 }
719 }
720 } else {
721 // Handle special case, where there is only one file in the directory
722 // being completed - if it can be resolved we return that since
723 // handling it in the generalized logic below will result in
724 // more complexity.
725 if entries.len == 1 && os.is_file(os.join_path(ls_path, entries[0])) {
726 mut keep_input_path_format := ls_path
727 if !part.starts_with('./') && ls_path.starts_with('./') {
728 keep_input_path_format = keep_input_path_format.all_after('./')
729 }
730 return [os.join_path(keep_input_path_format, entries[0])]
731 }
732 for entry in entries {
733 if collect_all || entry.starts_with(last) {
734 list << append_separator_if_dir(entry)
735 }
736 }
737 }
738 }
739 }
740 if do_home_expand {
741 return list.map(it.replace_once(os.home_dir().trim_right(os.path_separator), '~'))
742 }
743 return list
744}
745
746fn setup_for_shell(shell string) string {
747 mut setup := ''
748 match shell {
749 'bash' {
750 setup = '
751_v_completions() {
752 local src
753 local limit
754 # Send all words up to the word the cursor is currently on
755 let limit=1+\$COMP_CWORD
756 src=\$(${vexe} complete bash \$(printf "%s\\n" \${COMP_WORDS[@]: 0:\$limit}))
757 if [[ \$? == 0 ]]; then
758 eval \${src}
759 #echo \${src}
760 fi
761}
762
763complete -o nospace -F _v_completions v
764'
765 }
766 'fish' {
767 setup = '
768function __v_completions
769 # Send all words up to the one before the cursor
770 ${vexe} complete fish (commandline -cop)
771end
772complete -f -c v -a "(__v_completions)"
773'
774 }
775 'zsh' {
776 setup = '
777#compdef v
778_v() {
779 local src
780 # Send all words up to the word the cursor is currently on
781 src=\$(${vexe} complete zsh \$(printf "%s\\n" \${(@)words[1,\$CURRENT]}))
782 if [[ \$? == 0 ]]; then
783 eval \${src}
784 #echo \${src}
785 fi
786}
787compdef _v v
788'
789 }
790 'powershell' {
791 setup = '
792Register-ArgumentCompleter -Native -CommandName v -ScriptBlock {
793 param(\$commandName, \$wordToComplete, \$cursorPosition)
794 ${vexe} complete powershell "\$wordToComplete" | ForEach-Object {
795 [System.Management.Automation.CompletionResult]::new(\$_, \$_, \'ParameterValue\', \$_)
796 }
797}
798'
799 }
800 else {}
801 }
802
803 return setup
804}
805
806fn main() {
807 args := os.args[1..]
808 // println('"${args}"')
809 auto_complete(args)
810}
811