// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. // // Utility functions helping integrate with various shell auto-completion systems. // The install process and communication is inspired from that of [kitty](https://sw.kovidgoyal.net/kitty/#completion-for-kitty) // This method avoids writing and maintaining external files on the user's file system. // The user will be responsible for adding a small line to their .*rc - that will ensure *live* (i.e. not-static) // auto-completion features. // // # bash // To install auto-completion for V in bash, simply add this code to your `~/.bashrc`: // `source /dev/stdin <<<"$(v complete setup bash)"` // On more recent versions of bash (>3.2) this should suffice: // `source <(v complete setup bash)` // // # fish // For versions of fish <3.0.0, add the following to your `~/.config/fish/config.fish` // `v complete setup fish | source` // Later versions of fish source completions by default. // // # zsh // To install auto-completion for V in zsh - please add the following to your `~/.zshrc`: // ``` // autoload -Uz compinit // compinit // # Completion for v // v complete setup zsh | source /dev/stdin // ``` // Please note that you should let v load the zsh completions after the call to compinit // // # powershell // To install auto-complete for V in PowerShell, simply do this // `v complete setup powershell >> $PROFILE` // and reload profile // `& $PROFILE` // If `$PROFILE` didn't exist yet, create it before // `New-Item -Type File -Force $PROFILE` // module main import os const ( auto_complete_shells = ['bash', 'fish', 'zsh', 'powershell'] // list of supported shells vexe = os.getenv('VEXE') help_text = "Usage: v complete [options] [SUBCMD] QUERY... Description: Tool for bridging auto completion between various shells and v Supported shells: bash, fish, zsh, powershell Examples: Echo auto-detected shell install script to STDOUT v complete Echo specific shell install script to STDOUT v complete setup bash Auto complete input `v tes`*USER PUSHES TAB* (in Bash compatible format). This is not meant for manual invocation - it's called by the relevant shell via the script installed with `v complete` or `v complete setup SHELL`. v complete bash v tes Options: -h, --help Show this help text. SUBCMD: setup : setup [SHELL] - returns the code for completion setup for SHELL bash : [QUERY] - returns Bash compatible completion code with completions computed from QUERY fish : [QUERY] - returns Fish compatible completion code with completions computed from QUERY zsh : [QUERY] - returns ZSH compatible completion code with completions computed from QUERY powershell: [QUERY] - returns PowerShell compatible completion code with completions computed from QUERY" ) // Snooped from cmd/v/v.v, vlib/v/pref/pref.v const ( auto_complete_commands = [ // simple_cmd 'ast', 'doc', 'vet', // tools in one .v file 'bin2v', 'bug', 'build-examples', 'build-tools', 'build-vbinaries', 'bump', 'check-md', 'complete', 'compress', 'create', 'doctor', 'fmt', 'gret', 'ls', 'repl', 'self', 'setup-freetype', 'shader', 'symlink', 'test-all', 'test-cleancode', 'test-fmt', 'test-parser', 'test-self', 'test', 'tracev', 'up', 'watch', 'wipe-cache', // commands 'help', 'new', 'init', 'translate', 'self', 'search', 'install', 'update', 'upgrade', 'outdated', 'list', 'remove', 'vlib-docs', 'get', 'version', 'run', 'build', 'build-module', 'missdoc', ] // Entries in the flag arrays below should be entered as is: // * Short flags, e.g.: "-v", should be entered: '-v' // * Long flags, e.g.: "--version", should be entered: '--version' // * Single-dash flags, e.g.: "-version", should be entered: '-version' auto_complete_flags = [ '-apk', '-show-timings', '-check-syntax', '-check', '-v', '-progress', '-silent', '-g', '-cg', '-repl', '-live', '-sharedlive', '-shared', '--enable-globals', '-enable-globals', '-autofree', '-compress', '-freestanding', '-no-parallel', '-no-preludes', '-prof', '-profile', '-profile-no-inline', '-prod', '-simulator', '-stats', '-obfuscate', '-translated', '-color', '-nocolor', '-showcc', '-show-c-output', '-experimental', '-usecache', '-prealloc', '-parallel', '-native', '-W', '-keepc', '-w', '-print-v-files', '-error-limit', '-message-limit', '-os', '-printfn', '-cflags', '-define', '-d', '-cc', '-o', '-b', '-path', '-custom-prelude', '-name', '-bundle', '-V', '-version', '--version', ] auto_complete_flags_doc = [ '-all', '-f', '-h', '-help', '-m', '-o', '-readme', '-v', '-filename', '-pos', '-no-timestamp', '-inline-assets', '-theme-dir', '-open', '-p', '-s', '-l', ] auto_complete_flags_fmt = [ '-c', '-diff', '-l', '-w', '-debug', '-verify', ] auto_complete_flags_bin2v = [ '-h', '--help', '-m', '--module', '-p', '--prefix', '-w', '--write', ] auto_complete_flags_shader = [ '--help', '-h', '--force-update', '-u', '--verbose', '-v', '--slang', '-l', '--output', '-o', ] auto_complete_flags_missdoc = [ '--help', '-h', '--tags', '-t', '--deprecated', '-d', '--private', '-p', '--no-line-numbers', '-n', '--exclude', '-e', '--relative-paths', '-r', '--js', '--verify', '--diff', ] auto_complete_flags_bump = [ '--patch', '--minor', '--major', ] auto_complete_flags_self = [ '-prod', ] auto_complete_compilers = [ 'cc', 'gcc', 'tcc', 'tinyc', 'clang', 'mingw', 'msvc', ] ) // auto_complete prints auto completion results back to the calling shell's completion system. // auto_complete acts as communication bridge between the calling shell and V's completions. fn auto_complete(args []string) { if args.len <= 1 || args[0] != 'complete' { if args.len == 1 { shell_path := os.getenv('SHELL') if shell_path.len > 0 { shell_name := os.file_name(shell_path).to_lower() if shell_name in auto_complete_shells { println(setup_for_shell(shell_name)) exit(0) } eprintln('Unknown shell ${shell_name}. Supported shells are: ${auto_complete_shells}') exit(1) } eprintln('auto completion require arguments to work.') } else { eprintln('auto completion failed for "${args}".') } exit(1) } sub := args[1] sub_args := args[1..] match sub { 'setup' { if sub_args.len <= 1 || sub_args[1] !in auto_complete_shells { eprintln('please specify a shell to setup auto completion for (${auto_complete_shells}).') exit(1) } shell := sub_args[1] println(setup_for_shell(shell)) } 'bash' { if sub_args.len <= 1 { exit(0) } mut lines := []string{} list := auto_complete_request(sub_args[1..]) for entry in list { lines << "COMPREPLY+=('${entry}')" } println(lines.join('\n')) } 'fish', 'powershell' { if sub_args.len <= 1 { exit(0) } mut lines := []string{} list := auto_complete_request(sub_args[1..]) for entry in list { lines << '${entry}' } println(lines.join('\n')) } 'zsh' { if sub_args.len <= 1 { exit(0) } mut lines := []string{} mut dirs := []string{} mut files := []string{} list := auto_complete_request(sub_args[1..]) for entry in list { match true { os.is_dir(entry) { dirs << entry } os.is_file(entry) { files << entry } else { lines << entry } } } println('compadd -q -- ${lines.join(' ')}') println('compadd -J "dirs" -X "directory" -d -- ${dirs.join(' ')}') println('compadd -J "files" -X "file" -f -- ${files.join(' ')}') } '-h', '--help' { println(help_text) } else {} } exit(0) } // append_separator_if_dir returns the input `path` with an appended // `/` or `\`, depending on the platform, when `path` is a directory. fn append_separator_if_dir(path string) string { if os.is_dir(path) && !path.ends_with(os.path_separator) { return path + os.path_separator } return path } // nearest_path_or_root returns the nearest valid path searching // backwards from `path`. fn nearest_path_or_root(path string) string { mut fixed_path := path if !os.is_dir(fixed_path) { fixed_path = path.all_before_last(os.path_separator) if fixed_path == '' { fixed_path = '/' } } return fixed_path } // auto_complete_request returns a list of completions resolved from a full argument list. fn auto_complete_request(args []string) []string { // Using space will ensure a uniform input in cases where the shell // returns the completion input as a string (['v','run'] vs. ['v run']). split_by := ' ' request := args.join(split_by) mut do_home_expand := false mut list := []string{} // new_part := request.ends_with('\n\n') mut parts := request.trim_right(' ').split(split_by) if parts.len <= 1 { // 'v ' -> top level commands. for command in auto_complete_commands { list << command } } else { mut part := parts.last().trim(' ') mut parent_command := '' for i := parts.len - 1; i >= 0; i-- { if parts[i].starts_with('-') { continue } parent_command = parts[i] break } if part.starts_with('-') { // 'v [subcmd] -' or 'v [subcmd] --'-> flags. get_flags := fn (base []string, flag string) []string { mut results := []string{} for entry in base { if entry.starts_with(flag) { results << entry } } return results } match parent_command { 'bin2v' { // 'v bin2v -' list = get_flags(auto_complete_flags_bin2v, part) } 'build' { // 'v build -' -> flags. list = get_flags(auto_complete_flags, part) } 'doc' { // 'v doc -' -> flags. list = get_flags(auto_complete_flags_doc, part) } 'fmt' { // 'v fmt -' -> flags. list = get_flags(auto_complete_flags_fmt, part) } 'self' { // 'v self -' -> flags. list = get_flags(auto_complete_flags_self, part) } 'shader' { // 'v shader -' -> flags. list = get_flags(auto_complete_flags_shader, part) } 'missdoc' { // 'v missdoc -' -> flags. list = get_flags(auto_complete_flags_missdoc, part) } 'bump' { // 'v bump -' -> flags. list = get_flags(auto_complete_flags_bump, part) } else { for flag in auto_complete_flags { if flag == part { if flag == '-cc' { // 'v -cc ' -> list of available compilers. for compiler in auto_complete_compilers { path := os.find_abs_path_of_executable(compiler) or { '' } if path != '' { list << compiler } } } } else if flag.starts_with(part) { // 'v -' -> flags matching "". list << flag } } } } // Clear the list if the result is identical to the part examined // (the flag must have already been completed) if list.len == 1 && part == list[0] { list.clear() } } else { match part { 'help' { // 'v help ' -> top level commands except "help". list = auto_complete_commands.filter(it != part && it != 'complete') } else { // 'v ' -> commands matching "". // Don't include if part matches a full command - instead go to path completion below. for command in auto_complete_commands { if part != command && command.starts_with(part) { list << command } } } } } // Nothing of value was found. // Mimic shell dir and file completion if list.len == 0 { mut ls_path := '.' mut collect_all := part in auto_complete_commands mut path_complete := false do_home_expand = part.starts_with('~') if do_home_expand { add_sep := if part == '~' { os.path_separator } else { '' } part = part.replace_once('~', os.home_dir().trim_right(os.path_separator)) + add_sep } is_abs_path := part.starts_with(os.path_separator) // TODO Windows support for drive prefixes if part.ends_with(os.path_separator) || part == '.' || part == '..' { // 'v (.*/$|.|..)' -> output full directory list ls_path = '.' + os.path_separator + part if is_abs_path { ls_path = nearest_path_or_root(part) } collect_all = true } else if !collect_all && part.contains(os.path_separator) && os.is_dir(os.dir(part)) { // 'v (.*/.* && os.is_dir)' -> output completion friendly directory list if is_abs_path { ls_path = nearest_path_or_root(part) } else { ls_path = os.dir(part) } path_complete = true } entries := os.ls(ls_path) or { return list } mut last := part.all_after_last(os.path_separator) if is_abs_path && os.is_dir(part) { last = '' } if path_complete { path := part.all_before_last(os.path_separator) for entry in entries { if entry.starts_with(last) { list << append_separator_if_dir(os.join_path(path, entry)) } } } else { // Handle special case, where there is only one file in the directory // being completed - if it can be resolved we return that since // handling it in the generalized logic below will result in // more complexity. if entries.len == 1 && os.is_file(os.join_path(ls_path, entries[0])) { mut keep_input_path_format := ls_path if !part.starts_with('./') && ls_path.starts_with('./') { keep_input_path_format = keep_input_path_format.all_after('./') } return [os.join_path(keep_input_path_format, entries[0])] } for entry in entries { if collect_all || entry.starts_with(last) { list << append_separator_if_dir(entry) } } } } } if do_home_expand { return list.map(it.replace_once(os.home_dir().trim_right(os.path_separator), '~')) } return list } fn setup_for_shell(shell string) string { mut setup := '' match shell { 'bash' { setup = ' _v_completions() { local src local limit # Send all words up to the word the cursor is currently on let limit=1+\$COMP_CWORD src=\$(${vexe} complete bash \$(printf "%s\\n" \${COMP_WORDS[@]: 0:\$limit})) if [[ \$? == 0 ]]; then eval \${src} #echo \${src} fi } complete -o nospace -F _v_completions v ' } 'fish' { setup = ' function __v_completions # Send all words up to the one before the cursor ${vexe} complete fish (commandline -cop) end complete -f -c v -a "(__v_completions)" ' } 'zsh' { setup = ' #compdef v _v() { local src # Send all words up to the word the cursor is currently on src=\$(${vexe} complete zsh \$(printf "%s\\n" \${(@)words[1,\$CURRENT]})) if [[ \$? == 0 ]]; then eval \${src} #echo \${src} fi } compdef _v v ' } 'powershell' { setup = ' Register-ArgumentCompleter -Native -CommandName v -ScriptBlock { param(\$commandName, \$wordToComplete, \$cursorPosition) ${vexe} complete powershell "\$wordToComplete" | ForEach-Object { [System.Management.Automation.CompletionResult]::new(\$_, \$_, \'ParameterValue\', \$_) } } ' } else {} } return setup } fn main() { args := os.args[1..] // println('"$args"') auto_complete(args) }