nubia_complete/completer.py (138 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
#
import json
import logging
import os
import re
import shlex
import string
logger = logging.getLogger(__name__)
option_regex = re.compile("(?P<key>\-\-?[\w\-]+\=)")
def run_complete(args):
model_file = args.command_model_path
logging.info("Command model: %s", model_file)
comp_line = os.getenv("COMP_LINE")
comp_point = int(os.getenv("COMP_POINT", "0"))
comp_type = os.getenv("COMP_TYPE")
comp_shell = os.getenv("COMP_SHELL", "bash")
if not comp_line:
logger.error("$COMP_LINE is unset, failing!")
return 1
if not comp_point:
logger.error("$COMP_POINT is unset, failing!")
return 1
# Fix the disparity between zsh and bash for COMP_POINT
if comp_shell == "zsh":
comp_point -= 1
# We want to trim the remaining of the line because we don't care about it
comp_line = comp_line[:comp_point]
# We want to tokenize the input using these rules:
# - Separate by space unless there it's we are in " or '
try:
tokens = shlex.split(comp_line)
if len(tokens) < 1:
return 1
# drop the first word (the executable name)
tokens = tokens[1:]
except ValueError:
logger.warning("We are in an open quotations, cannot suggestion completions")
return 0
logger.debug("COMP_LINE: @%s@", comp_line)
logger.debug("COMP_POINT: %s", comp_point)
logger.debug("COMP_TYPE: %s", comp_type)
logger.debug("COMP_SHELL: %s", comp_shell)
# we want to know if the cursor is on a space or a word. If it's on a space,
# then we expect a completion of (command, option, or value).
current_token = None
if comp_line[comp_point - 1] not in string.whitespace:
current_token = tokens[-1]
tokens = tokens[:-1]
logger.debug("Input Tokens: %s", tokens)
logger.debug("Current token: %s", current_token)
# loading the model
with open(model_file, "r") as f:
model = json.load(f)
completions = get_completions(model, tokens, current_token, comp_shell)
for completion in completions:
logger.debug("Completion: @%s@", completion)
print(completion)
def _drop_from_options(options, token, skip_value=False):
# does this token in the format "-[-]x=" ?
tokens = token.split("=")
if skip_value:
tokens = tokens[:1]
for i, option in enumerate(options):
logger.debug("Tokens: %s", tokens)
if tokens[0] == option.get("name") or tokens[0] in option.get("extra_names"):
logger.debug("Dropping option %s", option)
if option.get("expects_argument"):
if len(tokens) > 1:
# we have the argument already
options.pop(i)
return None
return options.pop(i)
else:
return None
else:
logger.debug("mismatch: %s and %s", option.get("name"), tokens[0])
def _get_values_for_option(option, prefix=""):
logger.debug("Should auto-complete for option %s", option.get("name"))
output = option.get("values", [])
if output:
output = [prefix + _space_suffix(k) for k in output]
logger.debug("Values: %s", output)
return output
def get_completions(model, tokens, current, shell):
output = []
options_we_expect = model["options"]
current_command_list = model.get("commands", [])
last_option_found = None
for token in tokens:
if token.startswith("-"):
# it's an option, drop it from expected
current_option = _drop_from_options(options_we_expect, token)
if current_option and current_option.get("expects_argument"):
last_option_found = current_option
else:
# this is:
# - Argument to an option (ignore)
# - Command
# - Some random free argument
if last_option_found:
# does it expect a value?
logger.debug(
"Skipping %s because it's an argument to %s",
token,
last_option_found.get("name"),
)
last_option_found = None
continue
last_option_found = None
for command in current_command_list:
if token == command.get("name"):
logger.debug("We matched command %s", command.get("name"))
options_we_expect.extend(command.get("options", []))
# for sub-commands
current_command_list = command.get("commands", [])
break
else:
logger.debug(
"We didn't find any matching command, ignoring the token %s",
token,
)
# Now that we know where we are, let's complete the current token:
if last_option_found:
# we are expecting a value for this
output = _get_values_for_option(last_option_found)
else:
# If the current token is '--something=' then we should try to
# autocomplete a value for this
if current:
match = option_regex.match(current)
if match:
key = match.groupdict()["key"]
logger.debug("We are in a value-completion inside %s", key)
# it's true
option = _drop_from_options(options_we_expect, current, skip_value=True)
if option:
# YES, we have it, let's get the values
prefix = ""
if shell == "zsh":
# in zsh, we need to prepend the completions with the
# key
prefix = key
return _get_values_for_option(option, prefix)
output.extend(_completions_for_options(options_we_expect))
output.extend(_completions_for_commands(current_command_list))
return output
def _space_suffix(word):
return word + " "
def _completions_for_options(options):
output = []
should_suffix = int(os.getenv("NUBIA_SUFFIX_ENABLED", "1"))
def __suffix(key, expects_argument=True):
if should_suffix and expects_argument:
return key + "="
else:
return _space_suffix(key)
for option in options:
expects_argument = False
if option.get("expects_argument"):
expects_argument = True
output.append(__suffix(option.get("name"), expects_argument))
return output
def _completions_for_commands(commands):
return [_space_suffix(x["name"]) for x in commands]