From 82b16cbf45e7f5dbd1a4907902558351dab54b8f Mon Sep 17 00:00:00 2001 From: Larpon Date: Fri, 20 Nov 2020 15:17:31 +0100 Subject: [PATCH] v: add shell auto-completion with `source <(v complete setup bash)` (#6886) --- cmd/tools/vcomplete.v | 302 ++++++++++++++++++++++++++++++++++++++++++ cmd/v/v.v | 1 + 2 files changed, 303 insertions(+) create mode 100644 cmd/tools/vcomplete.v diff --git a/cmd/tools/vcomplete.v b/cmd/tools/vcomplete.v new file mode 100644 index 0000000000..2cfcce7349 --- /dev/null +++ b/cmd/tools/vcomplete.v @@ -0,0 +1,302 @@ +// Copyright (c) 2019-2020 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 +// TODO +// +// # zsh +// TODO +// +// # powershell +// TODO +module main + +import os + +const ( + auto_complete_shells = ['bash', 'fish', 'zsh', 'powershell'] +) + +// Snooped from cmd/v/v.v, vlib/v/pref/pref.v +const ( + auto_complete_commands = [ + /* simple_cmd */ + 'fmt', + 'up', + 'vet', + 'self', + 'tracev', + 'symlink', + 'bin2v', + 'test', + 'test-fmt', + 'test-compiler', + 'test-fixed', + 'test-cleancode', + 'repl', + 'complete', + 'build-tools', + 'build-examples', + 'build-vbinaries', + 'setup-freetype', + 'doc', + 'doctor', + /* commands */ + 'help', + 'new', + 'init', + 'complete', + 'translate', + 'self', + 'search', + 'install', + 'update', + 'upgrade', + 'outdated', + 'list', + 'remove', + 'vlib-docs', + 'get', + 'version', + 'run', + 'build', + 'build-module', + ] + auto_complete_flags = [ + '-apk', + '-show-timings', + '-check-syntax', + '-v', + '-progress', + '-silent', + '-g', + '-cg', + '-repl', + '-live', + '-sharedlive', + '-shared', + '--enable-globals', + '-enable-globals', + '-autofree', + '-compress', + '-freestanding', + '-no-preludes', + '-prof', + '-profile', + '-profile-no-inline', + '-prod', + '-simulator', + '-stats', + '-obfuscate', + '-translated', + '-color', + '-nocolor', + '-showcc', + '-show-c-output', + '-experimental', + '-usecache', + '-prealloc', + '-parallel', + '-x64', + '-W', + '-reuse-tmpc', + '-w', + '-print_v_files', + '-error-limit', + '-os', + '-printfn', + '-cflags', + '-define', + '-d', + '-cc', + '-o', + '-b', + '-path', + '-custom-prelude', + '-name', + '-bundle', + '-V', + '-version', + '--version', + ] + auto_complete_compilers = [ + 'cc', + 'gcc', + 'tcc', + 'tinyc', + 'clang', + 'mingw', + 'msvc', + ] +) + +fn auto_complete(args []string) { + if args.len <= 1 || args[0] != 'complete' { + if args.len == 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] + mut setup := '' + match shell { + 'bash' { setup = r' +_v_completions() { + local src + local limit + # Send all words up to the word the cursor is currently on + let limit=1+$COMP_CWORD + src=$(v 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' {} //TODO see https://github.com/kovidgoyal/kitty/blob/75a94bcd96f74be024eb0a28de87ca9e15c4c995/kitty/complete.py#L113 + // 'zsh' {} //TODO see https://github.com/kovidgoyal/kitty/blob/75a94bcd96f74be024eb0a28de87ca9e15c4c995/kitty/complete.py#L87 + // 'powershell' {} //TODO + else {} + } + println(setup) + } + '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' {} //TODO + // 'zsh' {} //TODO + // 'powershell' {} //TODO + else {} + } + exit(0) +} + +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 + split_by := ' ' + request := args.join(split_by) + 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 { + part := parts.last().trim(' ') + if part.starts_with('-') { // 'v -' -> flags. + 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 + } + } + } else { + if part == 'help' { // 'v help ' -> top level commands except "help". + list = auto_complete_commands.filter(it != part).filter(it != 'complete') + } else if part == 'build' { // 'v build ' -> all flags. + list = auto_complete_flags + } 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 + if part.ends_with(os.path_separator) || part == '.' || part == '..' { // 'v (.*/$|.|..)' -> output full directory list + ls_path = '.' + os.path_separator + 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 + ls_path = os.dir(part) + path_complete = true + } + entries := os.ls(ls_path) or { + return list + } + last := part.all_after_last(os.path_separator) + if path_complete { + path := part.all_before_last(os.path_separator) + for entry in entries { + if entry.starts_with(last) { + list << entry + } + } + // If only one possible file - send full path to completion system. + // Please note that this might be bash specific - needs more testing. + if list.len == 1 { + list = [os.join_path(path, list[0])] + } + } else { + for entry in entries { + if collect_all { + list << entry + } else { + if entry.starts_with(last) { + list << entry + } + } + } + } + } + } + return list +} + +fn main() { + args := os.args[1..] + //println('"$args"') + auto_complete(args) +} diff --git a/cmd/v/v.v b/cmd/v/v.v index 1cce4d93a5..67e63a25df 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -15,6 +15,7 @@ const ( 'self', 'tracev', 'symlink', 'bin2v', 'test', 'test-fmt', 'test-compiler', 'test-fixed', 'test-cleancode', 'repl', + 'complete', 'build-tools', 'build-examples', 'build-vbinaries', 'setup-freetype', 'doc', 'doctor'