v: add shell auto-completion with `source <(v complete setup bash)` (#6886)
							parent
							
								
									93d460f8fc
								
							
						
					
					
						commit
						82b16cbf45
					
				|  | @ -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 <tab>' -> top level commands.
 | ||||||
|  | 		for command in auto_complete_commands { | ||||||
|  | 			list << command | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		part := parts.last().trim(' ') | ||||||
|  | 		if part.starts_with('-') { // 'v -<tab>' -> flags.
 | ||||||
|  | 			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 | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if part == 'help' { // 'v help <tab>' -> top level commands except "help".
 | ||||||
|  | 				list = auto_complete_commands.filter(it != part).filter(it != 'complete') | ||||||
|  | 			} else if part == 'build' { // 'v build <tab>' -> all flags.
 | ||||||
|  | 				list = auto_complete_flags | ||||||
|  | 			} 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 | ||||||
|  | 			if part.ends_with(os.path_separator) || part == '.' || part == '..' { // 'v <command>(.*/$|.|..)<tab>' -> 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 <command>(.*/.* && os.is_dir)<tab>'  -> 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) | ||||||
|  | } | ||||||
|  | @ -15,6 +15,7 @@ const ( | ||||||
| 		'self', 'tracev', 'symlink', 'bin2v', | 		'self', 'tracev', 'symlink', 'bin2v', | ||||||
| 		'test', 'test-fmt', 'test-compiler', 'test-fixed', 'test-cleancode', | 		'test', 'test-fmt', 'test-compiler', 'test-fixed', 'test-cleancode', | ||||||
| 		'repl', | 		'repl', | ||||||
|  | 		'complete', | ||||||
| 		'build-tools', 'build-examples', | 		'build-tools', 'build-examples', | ||||||
| 		'build-vbinaries', | 		'build-vbinaries', | ||||||
| 		'setup-freetype', 'doc', 'doctor' | 		'setup-freetype', 'doc', 'doctor' | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue