v/cmd/tools/vcomplete.v

598 lines
14 KiB
V

// Copyright (c) 2019-2022 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',
'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{}
list := auto_complete_request(sub_args[1..])
for entry in list {
lines << 'compadd -U -S' + '""' + ' -- ' + "'$entry';"
}
println(lines.join('\n'))
}
'-h', '--help' {
println(help_text)
}
else {}
}
exit(0)
}
// append_separator_if_dir is a utility function.that returns the input `path` appended an
// OS dependant path separator if the `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 retuns 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 <tab>' -> 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] -<tab>' or 'v [subcmd] --<tab>'-> 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 -<tab>'
list = get_flags(auto_complete_flags_bin2v, part)
}
'build' { // 'v build -<tab>' -> flags.
list = get_flags(auto_complete_flags, part)
}
'doc' { // 'v doc -<tab>' -> flags.
list = get_flags(auto_complete_flags_doc, part)
}
'fmt' { // 'v fmt -<tab>' -> flags.
list = get_flags(auto_complete_flags_fmt, part)
}
'self' { // 'v self -<tab>' -> flags.
list = get_flags(auto_complete_flags_self, part)
}
'shader' { // 'v shader -<tab>' -> flags.
list = get_flags(auto_complete_flags_shader, part)
}
'missdoc' { // 'v missdoc -<tab>' -> flags.
list = get_flags(auto_complete_flags_missdoc, part)
}
'bump' { // 'v bump -<tab>' -> flags.
list = get_flags(auto_complete_flags_bump, part)
}
else {
for flag in auto_complete_flags {
if flag == part {
if flag == '-cc' { // 'v -cc <tab>' -> 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 -<char(s)><tab>' -> flags matching "<char(s)>".
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 <tab>' -> top level commands except "help".
list = auto_complete_commands.filter(it != part && it != 'complete')
}
else {
// 'v <char(s)><tab>' -> commands matching "<char(s)>".
// 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 <command>(.*/$|.|..)<tab>' -> 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 <command>(.*/.* && os.is_dir)<tab>' -> 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 {
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)
}