ergonomica/ergonomica

View on GitHub
ergonomica/lib/interface/completer.py

Summary

Maintainability
D
2 days
Test Coverage
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-

"""
[lib/interface/completer.py]

The autocomplete engine for ergonomica.
"""

# for completing directory/filenames
import os
import subprocess
import re

from prompt_toolkit.completion import Completer, Completion
from ergonomica.lib.lang.tokenizer import tokenize
from ergonomica.lib.lang.parser import parse, Symbol

def get_all_args_from_man(command):
    """
    Returns a dictionary mapping option->their descriptions
    """

    try:
        devnull = open(os.devnull, 'w')
        try:
            options = [x for x in subprocess.check_output(["man", command], stderr=devnull).decode().replace("\x08", "").replace("\n\n", "{TEMP}").replace("\n", " ").replace("{TEMP}", "\n").split("\n") if x.startswith("     -")]
        except OSError:
            return []
        except subprocess.CalledProcessError:
            try:
                options = [x for x in subprocess.check_output([command, "--help"], stderr=devnull).decode().replace("\x08", "").replace("\n\n", "{TEMP}").replace("\n", " ").replace("{TEMP}", "\n").split("\n") if x.startswith("     -")]
            except subprocess.CalledProcessError:
                return []
            except OSError:
                return []
            return []

        options = [re.sub("[ ]+", " ", x) for x in options]

        out = []
        for i in options:
            out.append((i.strip().split(" ")[0][::2], " ".join(i.strip().split(" ")[1:])))

        return out
    except:
        # TODO: clean this up. potentially take inspiration from Fish shell?
        # currently this is really buggy, and given the wide variety of manpage styles, having this break somebody's Ergonomica wouldn't be great.
        return {}


def get_arg_type(verbs, text):
    """
    Get the type of the current argument to complete,
    given the buffer text and the verbs dictionary.
    """

    if text[-1] == " ":
        text += "a"

    tokens = parse(tokenize(text))

    if text.endswith("(") or (len(tokens) == 0) or isinstance(tokens[-1], Symbol):
        return [("<function>", "")]

    for token in tokens[::-1]:
        if isinstance(token, Symbol):
            current_command = token

    argcount = 0
    for i in range(len(tokens))[::-1]:
        token = tokens[i]
        if (i == 0) or (isinstance(tokens[i - 1], list)):
            argcount = len(tokens)  - i

    # lookup and get docstring
    try:
        # regexp match
        docstrings = re.search(r'(Usage|usage):\n\s.*', verbs[current_command].__doc__).group()

        # preprocess
        docstrings = [x.strip().split() for x in docstrings.split("\n")[1:]]

    except AttributeError:
        return [("<file/directory>", "")]
    except TypeError: # empty buffer
        return [("<file/directory>", "")]
    except KeyError: # no such command
        if os.name != "nt":
            return [("<file/directory>", "")] + get_all_args_from_man(current_command)
        else:
            return [("<file/directory>", "")]

    # we .split() the docstring which splits it by spaces--but this needs to be corrected
    # for individual elements that contain spaces, e.g. (-a | --address)
    # parsed_docstring contains the corrected list of arguments.
    parsed_docstrings = []
    for docstring in docstrings:
        parsed_docstrings.append([])
        for item in docstring:
            if (parsed_docstrings[-1] == []) or \
                ((parsed_docstrings[-1][-1].count('(') == parsed_docstrings[-1][-1].count(')')) and \
                (parsed_docstrings[-1][-1].count('[') == parsed_docstrings[-1][-1].count(']'))):
                parsed_docstrings[-1].append(item)

            else:
                parsed_docstrings[-1][-1] += item

    out = []
    for parsed_docstring in parsed_docstrings:
        try:
            preset_arg = re.match(r'[a-z]+', parsed_docstring[argcount - 1])
            if preset_arg and (preset_arg.group() == parsed_docstring[argcount - 1]):
                out.append((parsed_docstring[argcount - 1], ""))
            else:
                try:
                    out.append((re.match(r'<[a-z]+?>', parsed_docstring[argcount - 1]).group(), ""))
                except AttributeError:
                    # current argument doesn't have a declared type
                    out.append(("<file/directory>", ""))
                except IndexError:
                    # no argument
                    pass
        except IndexError:
            pass

    return out


def complete(verbs, text):
    """
    Return a completion for a command or directory.
    """

    verbs.update({"if": None,
                  "set": None,
                  "global": None,
                  "lambda": None,})

    fixed_text = text
    if text.endswith(" "):
        fixed_text += "a"
        last_word = ""

    last_word = tokenize(text)[-1]

    options = []
    meta = {}

    if True:#(["("] + tokenize(text))[-2] != "(":
        for argtype in get_arg_type(verbs, fixed_text):

            if argtype[1] != "":
                # aka there's a meta definition
                meta[argtype[0]] = argtype[1]

            if not (argtype[0].startswith("<") or argtype[0].endswith(">")):
                # then add it directory
                options.append(argtype[0])

            if argtype[0] == "<none>":
                # aka no more arguments to supply to function
                pass

            elif argtype[0] == "<variable>":
                options += [x for x in verbs.keys() if not hasattr(verbs[x], "__call__")] + ['def']

            elif argtype[0] in ["<file>", "<directory>", "<file/directory>"]:
                if os.path.basename(text) == text:
                    try:
                        options += os.listdir(".")
                    except OSError:
                        pass
                else:
                    dirname = os.path.dirname(text.split(" ")[1])
                    original_dirname = dirname

                    # process dirname
                    if not dirname.startswith("/"):
                        if dirname.startswith("~"):
                            dirname = os.path.expanduser(dirname)
                        else:
                            dirname = "./" + dirname
                    try:
                        options += [os.path.join(original_dirname, x) for x in os.listdir(dirname)]
                    except OSError:
                        pass

                if argtype[0] == "<file>":
                    options = [x for x in options if os.path.isfile(x)]
                elif argtype[0] == "<directory>":
                    options = [x for x in options if os.path.isdir(x)]

            elif argtype[0] == "<string>":
                options += [text.split(" ")[-1] + '"']

            elif argtype[0] == "<function>":
                commands = [os.listdir(y) for y in os.environ['PATH'].split(os.pathsep)]
                flattened_commands = [x for l in commands for x in l]
                options = [x for x in verbs.keys() if hasattr(verbs[x], "__call__")] + flattened_commands

    if text.endswith(" "):
        last_word = ""

    if last_word == "(":
        last_word = ""

    if not text.endswith(" "):
        options = [i for i in options if i.startswith(last_word)]

    if options == []:
        if text.endswith("/"):
            try:
                options = os.listdir(last_word)
            except OSError:
                options = []
            return ([(len(last_word), option) for option in options], meta)
    if options != []:
        return ([(len(last_word), i) for i in options], meta)
    return ([], {})


class ErgonomicaCompleter(Completer):
    """
    Ergonomica subset of the Completer class.
    This handles all Ergonomica completion (using its syntax).
    """

    verbs = {}

    def __init__(self, verbs):
        self.verbs = verbs

    def get_completions(self, document, complete_event):
        try:
            completions, meta = complete(self.verbs, document.text)
            for completion in completions:
                yield Completion(completion[1], start_position=-completion[0], display_meta=meta.get(completion[1], ''))
        except Exception:
            pass