#!/usr/bin/env bash
# Shebang merely for syntax highlighting.
# This file should be pasted directly into ~/.bashrc or
# put into the bashrc drop-in directory ~/.bashrc.d/
# and then sourced from ~/.bashrc.

# WARNING: This plug-in overwrites the readling bindings for ' ', '|', '&', ';', '(', ')', '<', '>'
#          It also creates new bindings '\e\C-a<x>' where `<x>` is one of the characters above.
#          Additionally, it overwrites the binding for '\C-m' and creates a corresponding binding '\e\C-an'.
#          Therefore, it might interfere with other plug-ins or custom bindings.
#          You may change the '\e\C-a' binding-"prefix" to whatever you want as it's only used
#          as a hook inside other readline macros. The bindings '|', '&', ';', '(', ')', '<', '>' (and their
#          corresponding '\e\C-a' bindings) may also be dropped if you don't need them.

# Regexes for (heuristically) determining whether we're abbreviating the first word.
# Construction put into functions to prevent global namespace pollution
# and increase readability.
_control_op () {
    declare -r _newline=$'\n' \
               _bg_proc_or_and_and='[^&;]&&?' \
               _pipe_or_or_or='[^&|;]\|\|?' \
               _semicolon_or_case_operators='[^;|];;?&?' \
                _opening_paren='(^|[^[:alnum:]])\('
    declare _control_op="^"
    _control_op+="|$_newline"
    _control_op+="|$_bg_proc_or_and_and"
    _control_op+="|$_pipe_or_or_or"
    _control_op+="|$_semicolon_or_case_operators"
    _control_op+="|$_opening_paren"
    printf %s "$_control_op"
}
_control_op="$(_control_op)"
unset -f _control_op

_redir_or_assign_word () {
    declare -r _varredir_or_number='\{[[:alpha:]_][[:alnum:]_]*\}|[[:digit:]]*' \
               _redir_in_or_out='>|<' \
               _close_or_move_fd='&[[:digit:]]*-?' \
               _redir_or_append_out_and_err='(&)(>>?)' \
               _heredoc_herestring_in_out_etc='>>?|<(>|<(-?|<))?' \
               _identifier_equals='[[:alpha:]_][[:alnum:]]*=' \
               _simple_unquoted_word='[^[:space:]<>&;()|'\''"`$]+' \
               _double_quoted_string='\$?"([^"]|\\")*"' \
               _single_quoted_string="\\\$?'[^']*'" \
               _backtick_string='`([^`]|\\`)*`' \
               _simple_variable='\$[[:alpha:]_][[:alnum:]]*' \
               _advanced_variable='\$\{([[:alnum:]]+|[#-_$!*#])[^{}]*\}' \
               _deprecated_arithmetic_eval='\$\[[^][]*\]' \
               _cmd_subst_or_arithmetic_eval='\$\([^()]*\)' \
               _process_substition='<\([^()]*\)' \
               _escaped_char='\\.'
    declare _part="$_simple_unquoted_word"
    _part+="|$_double_quoted_string"
    _part+="|$_single_quoted_string"
    _part+="|$_backtick_string"
    _part+="|$_simple_variable"
    _part+="|$_advanced_variable"
    _part+="|$_deprecated_arithmetic_eval"
    _part+="|$_cmd_subst_or_arithmetic_eval"
    _part+="|$_process_substition"
    _part+="|$_escaped_char"
    declare -r _word="($_part)+" \
               _close_redirection="($_varredir_or_number)($_redir_in_or_out)($_close_or_move_fd)" \
               _generic_redirection="($_redir_or_append_out_and_err|($_varredir_or_number)($_heredoc_herestring_in_out_etc))"
    declare -r _word_redirection_or_assignment="($_generic_redirection[[:space:]]*|$_identifier_equals)($_word)"
    printf %s "$_close_redirection|$_word_redirection_or_assignment"
}
_redir_or_assign_word="$(_redir_or_assign_word)"
unset -f _redir_or_assign_word

_try_abbrev () {
    # For use within bind -x.
    # Perform abbreviation lookup and interactive expansion on the shell-word left to the cursor (no quote parsing).
    # Takes a single argument which is the character to self-insert (usually space or another shell metacharacter).

    declare -r line_until_point=${READLINE_LINE::READLINE_POINT}

    if
        # Check if the character at point is a shell metacharacter or EOL
        [[ $READLINE_POINT -eq ${#READLINE_LINE} || "${READLINE_LINE:READLINE_POINT:1}" = [\<\>\&\;\(\)\|[:space:]] ]] &&

        # Determine word to complete.
        # Uses the same word separators as bash (| & ; ( ) space tab newline) (aka "shell metacharacters")
        # But < and > are ignored, since they always expect a file/delimiter/string afterwards and not a command
        declare -r word_at_point=${line_until_point##*[&;()|[:space:]]} &&

        # Select replacement (if any).
        declare replacement=${BASH_ALIASES["${word_at_point:-__nonexistent}"]-} &&

        # Determine if it's the first word of a simple command, unless we're expanding a global alias.
        # Uses the same command separators as bash (|| & && ; ;; ;& ;;& ( ) | |& newline) (aka "control operators").
        # Due to complexity and performance reasons, we cannot reliably know
        # whether the cursor position (aka "point" in readline terms) is in a quoted/escaped
        # shell context or not (we would have to implement a full-blown bash parser) or
        # whether there are any redirections/assignment words before the first word.
        # The only heuristic we try is to check whether we're at the beginning of the line or if
        # there's a control operator immediately to the left of the word (ignoring whitespace)
        # and that there's no character/metacharacter prefixing the control operator
        # s.t. there's a syntax error. We also do some rudimentary redirection and assignment word
        # checking (including quoting) after the control operator. If the syntax is invalid (as far as
        # the regex can tell), then we bail and no alias expansion will happen.
        [[ ${replacement::1} = "!" || ${line_until_point%"$word_at_point"} =~ ($_control_op)[[:space:]]*((($_redir_or_assign_word)[[:space:]])*)$ ]] &&
        # If the regex leads to problems/performance issues, you may try this simpler regex
        # that does no redirection/assignment word checking:
        # [[ ${replacement::1} = "!" || ${line_until_point%"$word_at_point"} =~ ($_control_op)[[:space:]]*$ ]] &&
        [[ $replacement ]]; then

        # Account for global aliases (strip "!" prefix and disable self-insertion)
        [[ ${replacement::1} = "!" ]] && replacement=${replacement#!} && set -- ''

        # Calculate point and mark offset.
        declare -ir offset=${#replacement}-${#word_at_point}+${#1}

        # Update line.
        READLINE_LINE=${line_until_point%"$word_at_point"}$replacement${1-}${READLINE_LINE:READLINE_POINT}
    else
        declare -ir offset=${#1}

        # Self-insert by default
        READLINE_LINE=${line_until_point}${1-}${READLINE_LINE:READLINE_POINT}
    fi

    # Advance mark if it's right of (or on) the point.
    [[ $READLINE_MARK && $READLINE_POINT -le "$READLINE_MARK" ]] && READLINE_MARK=$((READLINE_MARK+offset))

    # Advance point.
    READLINE_POINT=$((READLINE_POINT+offset))

    # NB: READLINE_ARGUMENT is deliberately ignored.
}

# From the bash man page:
#   metacharacter
#     A character that, when unquoted, separates words. One of the following:
#     |  & ; ( ) < > space tab newline
# Note: double quote should be escaped, if added to the list of metacharacters
for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do
    bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"'
    bind    '"'"$ch"'": "\e\C-a'"$ch"'"'
    # bind    '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # Moves point weirdly
done

# Newline must be treated specially, because it doesn't self-insert
# and has the extra functionality of accepting a line.
bind -x '"\e\C-an": _try_abbrev ""'
bind    '"\C-m": "\e\C-an\e^\C-j"'
