#!/usr/bin/bash
# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
set -Eeuo pipefail #-x
ABSPATHSCRIPT=`readlink -f "$0"` && function die  { [ -n "$*" ] && echo "${ABSPATHSCRIPT##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; }

# == Execution Trampoline ==
# Hook to serve as a single-executable trampoline for `jj show --tool $SELF`
test -z "${JJFZF_EXEC_WITH0_AS_:-}" || { $JJFZF_EXEC_WITH0_AS_ "$@" || : ; exec true ; } # force exit_status=0
SELF="$0" # Allow to re-execute this script

# == Early options, outside a jj repo ==
COLORALWAYS=
while test $# -ne 0 ; do
  case "$1" in \
    --version)		echo jj-fzf 0.32.0 2025-08-21T06:41:55Z; exit ;;
    -h|--help)		exec man ${ABSPATHSCRIPT%/*}/doc/jj-fzf.1 ;;
    --color=always)	COLORALWAYS=t ;;
    *)         		break ;;
  esac
  shift
done
command -v gsed 2>/dev/null 1>&2 || { gsed() { \sed "$@"; } && export -f gsed; }

# == Subdirs may vanish during checkouts ==
JJPWD="$PWD"
JJROOT=$(jj --ignore-working-copy root) &&
  cd "$JJROOT" ||	# always ensure root relative paths
    die "$PWD: not a JJ repository"

# == PREVIEW fast path ==
JJFZF_PRIVATE="$(jj config get --ignore-working-copy --no-pager git.private-commits 2>/dev/null)" &&
  [[ "$JJFZF_PRIVATE" =~ ^[.a-z_()-]+$ ]] || JJFZF_PRIVATE=''	# only supported unquoted revset names
JJ_FZF_SHOWDETAILS='
concat(
  builtin_log_oneline,
  "Change ID: " ++ self.change_id() ++ "\n",
  "Commit ID: " ++ commit_id ++ "\n",
  "Flags:     ", separate(" ",
    if(immutable, label("node immutable", "immutable")),
    if(hidden, label("hidden", "hidden")),
    if(divergent, label("divergent", "divergent")),
    if(conflict, label("conflict", "conflict")),
    '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', 'private')), }"'
  ) ++ "\n",
  surround("Refs:      ", "\n", separate(" ", local_bookmarks, remote_bookmarks, tags)),
  "Parents:  " ++ self.parents().map(|c| " " ++ c.change_id()) ++ "\n",
  "Author:    " ++ format_detailed_signature(author) ++ "\n",
  "Committer: " ++ format_detailed_signature(committer)  ++ "\n\n",
  indent("    ",
    coalesce(description, label(if(empty, "empty"), description_placeholder) ++ "\n")),
  "\n",
)' # extended version of builtin_log_detailed; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
# Show commit diff according to jj-fzf.diff-mode
jj_show_diff()
{
  local COLOR && [[ " $* " =~ --color=always ]] && COLOR=--color=always || COLOR=--color=never
  # Use git-diff (for --git and --word-diff) which has better heuristics for informative hunk headers than jj (0.31.0)
  case "$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true)" in
    diff-b)	export JJFZF_EXEC_WITH0_AS_='git -P diff --no-index --diff-algorithm=histogram -b '"$COLOR"
		jj show --no-pager --ignore-working-copy "$@" --tool "$SELF" ;;
    word-b)	export JJFZF_EXEC_WITH0_AS_='git -P diff --no-index --diff-algorithm=histogram -b --word-diff '"$COLOR"
		jj show --no-pager --ignore-working-copy "$@" --tool "$SELF" ;;
    *)		jj show --no-pager --ignore-working-copy "$@" ;;
  esac
}
# handle 'preview' early on to avoid slow previews due to parsing the entire script
export REVPAT='^[^a-z()0-9]*([k-xyz]{7,})([?]*)\ '		# line start, ignore --graph, parse revision letters, catch '??'-postfix
if test "${1:-}" == preview					# preview command, nested invocation
then
  if [[ "${2:-} " =~ $REVPAT ]]					# match beginning of jj log line
  then
    REVISION="${BASH_REMATCH[1]}"
    if [[ "${BASH_REMATCH[2]}" == '??' ]]			# divergent change_id
    then
      # https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
      jj --no-pager --ignore-working-copy show -T builtin_log_oneline -r "${BASH_REMATCH[1]}" 2>&1 || :
      echo && echo
    fi
    REVISION=$(echo " $2 " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || exit 0	# find likely commit id
    { jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$REVISION"
      jj_show_diff --color=always -T '"\n"' -r "$REVISION"
    } 2>&1 | head -n 4000
  else								# no valid revision
    true
  fi
  exit 0
fi
export OPRPAT='^[^a-z0-9]*([0-9a-f]{9,})[?]*\ '	# line start, ignore --graph, parse hex letters, space separator
export HEX7PAT='\ ([0-9a-f]{7,})\ ' # space enclosed hexadecimal pattern
case "${1:-}" in
  preview_oplog)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always op log --no-graph -n 1 -T builtin_op_log_comfortable
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log -s -r .. # -T builtin_log_oneline
      } ; exit ;;
  preview_opshow)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always op log --no-graph -n 1 -T builtin_op_log_comfortable
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log --no-graph -s -r "@"
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always show -T ' "\n" ' -r "@"
      } ; exit ;;
  preview_oppatch)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --color=always op show -p "${BASH_REMATCH[1]}"
      } | head -n 4000 ; exit ;;
  preview_opdiff)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --color=always op diff -f "${BASH_REMATCH[1]}" -t @
      } ; exit ;;
  preview_evolog)
    [[ " ${2:-} " =~ $HEX7PAT ]] && {
      jj --no-pager --ignore-working-copy evolog --color=always -n1 -p -T "$JJ_FZF_SHOWDETAILS" -r "${BASH_REMATCH[1]}" |
	head -n 4000
    } ; exit ;;
esac

# == General Options ==
MULTISELECT= HELPKEYBINDINGS= SHOWKEYBINDINGS= COLORALWAYS= ONESHOT=false POSTCMDEXIT=
while test $# -ne 0 ; do
  case "$1" in \
    --help-bindings)	HELPKEYBINDINGS=t ;;
    -m)			MULTISELECT=-m ;;
    +m)			MULTISELECT= ;;
    --postcmd-exit)	POSTCMDEXIT=1 ;;
    --key-bindings)	SHOWKEYBINDINGS=t ;;
    --oneshot)		ONESHOT=true ;; # auto-exit after first command
    --color=always)	COLORALWAYS=t ;;
    *)         		break ;;
  esac
  shift
done

# == Config ==
export FZF_DEFAULT_OPTS=	# prevent user defaults from messing up the layout
declare -A DOC
# JJ repository
JJFZFSHOW="jj --no-pager --ignore-working-copy show --tool true"
JJFZFONELINE="jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline"
JJFZFPAGER="less -Rc"
JJSUBSHELL='T=$(tty 2>/dev/null||tty <&1 2>/dev/null||tty <&2 2>/dev/null) && test -n "$T" && echo -e "\n#\n# Type \"exit\" to leave subshell\n#" &&
            unset FZF_DEFAULT_COMMAND && { test ! -d "'"$JJPWD"'" || cd "'"$JJPWD"'"; } && exec /usr/bin/env '"$SHELL"' <$T 1>$T 2>$T'
INFO_BINDING=" fzf </dev/null >/dev/tty 2>&1 --prompt '        '  --disabled --layout=reverse --height 1 --margin 4 --padding 4 --border=block --no-info --no-scrollbar --no-clear --bind=enter:print-query "
FUNCTIONS=()
FZFSETTINGS=(
  --ansi --no-mouse -x -e --track
  --info default
  --layout reverse-list
  --scroll-off 3
  --bind "alt-up:offset-up"
  --bind "alt-down:offset-down"
  --bind "ctrl-x:jump"
  --bind "ctrl-z:execute( $JJSUBSHELL )"
  --bind='f11:change-preview-window(bottom,75%,border-horizontal|)'
  --preview-window 'wrap,right,border-left'
  --bind=ctrl-alt-x:"execute-silent($INFO_BINDING)+clear-screen"
)
FZFPOPUP=(fzf "${FZFSETTINGS[@]}" --margin "0,3%,5%,3%" --border)
TEMPD=
# for function exports to work sub-shell must be bash too
export SHELL=bash

# == JJ CONFIG ==
# parsable version of builtin_log_oneline; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_ONELINE='
if(root,
  format_root_commit(self),
  label(if(current_working_copy, "working_copy"),
    concat(
      separate(" ",
        format_short_change_id_with_hidden_and_divergent_info(self),
        if(author.email(), author.email().local(), email_placeholder),
        author.timestamp().local().format("%Y-%m-%d"),
        format_short_commit_id(commit_id),
        bookmarks,
        tags,
        working_copies,
        if(git_head, label("git_head", "git_head()")),
        if(conflict, label("conflict", "conflict")),
        if(empty, label("empty", "(empty)")),
        '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', '🌟')), }"'
        if(description,
          description.first_line(),
          label(if(empty, "empty"), description_placeholder),
        ),
      ) ++ "\n",
    ),
  )
)'
# builtin_log_oneline with commit_id *before* other tags/bookmarks/etc and force committer().timestamp
EVOLOG_ONELINE='
if(root,
  format_root_commit(self),
  label(if(current_working_copy, "working_copy"),
    concat(
      separate(" ",
        format_short_change_id_with_hidden_and_divergent_info(self),
        if(author.email(), author.email().local(), email_placeholder),
        format_timestamp(self.committer().timestamp()),
        format_short_commit_id(commit_id),
        bookmarks,
        tags,
        working_copies,
        if(git_head, label("git_head", "git_head()")),
        if(conflict, label("conflict", "conflict")),
        if(empty, label("empty", "(empty)")),
        if(description,
          description.first_line(),
          label(if(empty, "empty"), description_placeholder),
        ),
      ) ++ "\n",
    ),
  )
)'

# == Utils ==
# Create temporary dir, assigns $TEMPD
temp_dir()
{
  test -n "$TEMPD" || {
    TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/jj-fzf.pid
  }
}
# Match JJ revision as first ASCII word (e.g. as in builtin_log_oneline)
export OPPAT='^[^a-z()0-9]*([0-9a-f]{9,})\ '
# Try to extract non-divergent revision or parse expression
xrev_maybe()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RV='@' || RV=
  # or match abbreviated change_id pattern
  if test -z "$RV" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECHANGE='if(self.divergent(), "", change_id)'
    # only allow non-divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECHANGE" 2>/dev/null) || :
  fi
  # or match syntactically valid expressions
  test -z "$RV" && # divergent matches produce concatenated change_ids
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T change_id 2>/dev/null) || :
  # final validation that $RV is indeed a unique identifier for a non-divergent change_id
  test -n "$RV" &&
    jj --no-pager --ignore-working-copy log --no-graph -T change_id -r "$RV" 2>/dev/null # pass on exit status
)
# Extract non-divergent revision or show error
xrev()
(
  xrev_maybe "$@" ||
    ERROR "failed to parse revision: ${1:-}"
)
FUNCTIONS+=( 'xrev' )
# Extract commit_id or show error
xrev_as_commit()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RC='@' || RC=
  # or match abbreviated change_id pattern
  if test -z "$RC" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECOMMIT='if(self.divergent(), "", commit_id)'
    # check for divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECOMMIT" 2>/dev/null) || :
    test -n "$RC" ||	# non-divergent, else fallback to commit hash parsing
      RC=$(echo " $* " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || :
  fi
  # or match syntactically valid expressions
  test -z "$RC" && # divergent matches produce concatenated commit_ids
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T commit_id 2>/dev/null) || :
  # final validation that $RC is indeed a unique identifier for a single commit
  test -n "$RC" &&
    jj --no-pager --ignore-working-copy log --no-graph -T commit_id -r "$RC" 2>/dev/null ||
      ERROR "failed to parse commit id: ${1:-}"
)
FUNCTIONS+=( 'xrev_as_commit' )
# Yield the revision change_id or a commit_id if it is divergent
xrev_or_commit()
(
  xrev_maybe "$@" ||
    xrev_as_commit "$@"
)
FUNCTIONS+=( 'xrev_or_commit' )
# Fill <ARRAY> with <PREFIX> and xrev_or_commit from $@
parse_xrevs_or_commits()
{
  local -n _parsed_REVS=$1	# nameref to the target array
  local _prefix_REV=$2
  shift 2
  test $# -ne 0 && _parsed_REVS=() || _parsed_REVS=($_prefix_REV @)
  while test $# -ne 0; do
    local _rev
    _parsed_rev=$(xrev_or_commit "$1") || die "no such revision: $1"
    _parsed_REVS+=($_prefix_REV "$_parsed_rev")
    shift
  done
}

# Look up full commit hash via JJ commit_id
rev_commitid()	( xrev_as_commit "$@" )
# Print first bookmark or the revision itself
rev_bookmark1()	( $JJFZFSHOW -T 'concat(separate(" ",bookmarks), " ", change_id)' -r "$1" | awk '{print $1;}' )
# Get revision description
rev_description() ( $JJFZFSHOW -T 'concat(description)' -r "$1" )

# Condense commit empty/description/parent state into a key word
rev_edpstate()
(
  export EDPSTATE='separate("-", if(empty, "empty", "diff"), if(description, "description", "silent"), "p" ++ self.parents().len()) ++ "\n"'
  $JJFZFSHOW -r "$1" -T "$EDPSTATE" # empty-description-p2 diff-silent-p1 etc
)

# List parents of a revision
rev_parents()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "all: $1-" -T 'change_id++"\n"'
)

# List commits of a revset
list_revset()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "all: $* " -T 'commit_id++"\n"'
)

# join_args <joiner> [args...]
join_args()
{
  local j="${1:-}" first="${2:-}"
  if shift 2; then
    printf "%s" "$first" "${@/#/$j}"
  fi
}

# reverse_array ORIG REVERSED - copy the elements from ORIG in REVERSED in reverse order
reverse_array()
{
  local -n array_O_=$1
  local -n array_R_=$2
  # Loop in reverse order
  for ((i=${#array_O_[@]}-1; i>=0; i--)); do
    array_R_+=("${array_O_[i]}")
  done
}

# diff_arrays BEFORE AFTER RESULT - store the elements from AFTER without elements from BEFORE in RESULT
diff_arrays()
{
  local -n array_O_=$1
  local -n array_N_=$2
  local -n array_R_=$3
  declare -A counts_
  # Mark elements in A
  for elem in "${array_O_[@]}" ; do
    counts_["$elem"]=1
  done
  # Add all of B to C if not in A
  for elem in "${array_N_[@]}"; do
    test -z "${counts_[$elem]:-}" &&
      array_R_+=("$elem") # || echo "SKIP: $elem : ${counts_[$elem]:-}"
  done
  true
}

# backward_chronologic [REVISIONS] - produce revisions in backwards chronological order
backward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r all:"$ORREVS" -T 'change_id ++ "\n"'
)

# forward_chronologic [REVISIONS] - produce revisions in chronological order
forward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r all:"$ORREVS" -T 'change_id ++ "\n"' --reversed
)

# Require .git directory and set GIT_DIR
require_git_dir()
{
  test -e "$JJROOT/.git" &&
    export GIT_DIR="$JJROOT/.git" || {
      test -e "$JJROOT/.jj/repo/store/git" &&
	export GIT_DIR="$JJROOT/.jj/repo/store/git" ||
	  die "$PWD: failed to find .git store"
    }
}

# Write revision from `jj new -m $3 --no-edit -B $2` to $1
jj_new_before_no_edit()
{
  local -n result_=$1 # nameref
  local R="$(xrev "${2:-}")" # must use revision to find new parents
  local M="${3:-}"
  # record base commit parents before/after
  local A=( $(rev_parents "$R") )
  ( set -x
    jj new --no-edit --message="$M" --insert-before "$R" # --no-pager
  ) || die
  local B=( $(rev_parents "$R") )
  local C=() && diff_arrays A B C
  [ ${#C[@]} -eq 1 ] ||
    die "failed to find newly created revision"
  result_="${C[0]}"
}

# Exit the current shell with an error message and delay
ERROR()
{
  FUNC="${FUNC:-$0}"
  echo "ERROR: ${FUNC:+$FUNC:}" "$*" >&2
  # Wait a few seconds unless the user presses Enter
  read -t "${JJ_FZF_ERROR_DELAY:-2}"
  exit
}

# == Helpers ==
# Echo signoff
echo_signoff()
(
  JJFZF_SIGNOFF=true	# config get jjfzf.signoff
  if test "${JJFZF_SIGNOFF:-true}" == true ; then
    echo # separating newline before signoff section
    $JJFZFSHOW -T 'format_detailed_signature(author) ++ "\n"' -r @ |
      gsed -e 's/>.*/>/ ; s/^/Signed-off-by: /'
  fi
)
# Echo current or default message
echo_commit_msg()
(
  R="$1"
  if test "$R" != --merge ; then
    S=$(rev_edpstate "$R")
    # keep any existing message
    [[ $S =~ -silent- ]] || {
      rev_description "$R"
      return
    }
    # list parents
    PARENTS=( $(jj --no-pager --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r all:"$R-" --reversed) )
  else # --merge
    shift
    PARENTS=( $(forward_chronologic "$@") )
  fi
  # Create merge message
  if test "$R" == --merge -o "${#PARENTS[@]}" -ge 2 ; then
    SEP="^^^^^^^^^"
    NEWCOMMITS=()
    for p in "${PARENTS[@]}"; do
      NEWCOMMITS+=( $(rev_commitid $p) )
    done
    MERGE_BASE=$(git merge-base --octopus "${NEWCOMMITS[@]}")
    echo -e "# $SEP DRAFT:  merge" "${PARENTS[@]}" "$SEP # DELETE THIS"
    test "${#PARENTS[@]}" -le 2 &&
      echo "Merge branch '`rev_bookmark1 ${PARENTS[1]}`' into '`rev_bookmark1 ${PARENTS[0]}`'" ||
	echo "Merge branches:" "${PARENTS[@]}"
    for c in "${NEWCOMMITS[@]}"; do
      test "$c" == "$MERGE_BASE" && continue
      test "${#PARENTS[@]}" -le 2 &&
	echo -e "\n* Branch commit log:" || # "$c ^$MERGE_BASE"
	  echo -e "\n* Branch '`rev_bookmark1 $c`' commit log:"
      git log --pretty=$'\f%s%+b' $c ^$MERGE_BASE |
	gsed '/^\([A-Z][a-z0-9-]*-by\|Cc\):/d' | # strip Signed-off-by:
	gsed '/^$/d ; s/^/\t/ ; s/^\t\f$/  (no description)/ ; s/^\t\f/  /'
    done
    echo_signoff
  else # Commit message based on files
    # start with file name prefixes
    FILES=()
    readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | gsed 's/^\w //')
    test ${#FILES[@]} -gt 0 &&
      printf "%s: \n" "${FILES[@]}" ||
	echo ""
    { jj config --no-pager get 'ui.default-description' 2>/dev/null || : ; } | gsed '1{/^$/d}'
    test ${#FILES[@]} -le 0 ||
      echo_signoff
  fi
)
# Run user editor: user_editor_on_var <FILE> <VARIABLE> [COMMIT]
user_editor_on_var()
{
  local FILE="$1" COMMIT="${3:-}" N=
  declare -n _ueovMSG="$2"			# <VARIABLE> alias
  # create msg file
  temp_dir
  local TEMPFILE="$TEMPD/$FILE"
  cat >"$TEMPFILE" <<<"$_ueovMSG"
  test -z "$COMMIT" || {
    jj_show_diff --color=never -T '"\n"' -r "$COMMIT" |
      head -n 4000 > "$TEMPFILE.diff"
    test -s "$TEMPFILE.diff" && {
      echo
      echo '# -------- >8 -------- >8 -------- 8< -------- 8< --------'
      echo '# Everything below the snippet mark will be ignored'
      echo '#'
      echo '# Content diff of this revision:'
      cat "$TEMPFILE.diff"
    }
    rm -f "$TEMPFILE.diff"
  } >> "$TEMPFILE"
  # edit commit msg
  test -n "${JJ_EDITOR:-}" || # https://jj-vcs.github.io/jj/latest/config/#editor
    JJ_EDITOR="$(jj config get ui.editor 2>/dev/null || echo "${VISUAL:-${EDITOR:-pico}}")"
  $JJ_EDITOR "$TEMPFILE" &&
    N="$(cat "$TEMPFILE")" && {
      test "$_ueovMSG" != "$N" &&
	_ueovMSG="$(gsed -r '/^# -+ >8 -+ >8 -+ 8< -+ 8< -+/Q' < "$TEMPFILE")"
      rm -f "$TEMPFILE"
      return 0
    }
  rm -f "$TEMPFILE"
  return 1
}
# Read input with completion: RESULT="$(PROMPT=… INIT=… read_completing [words…])"
read_completing()
(
  WORDS=( "$@" )
  _read_completion() {
    local line="$READLINE_LINE" point="$READLINE_POINT"
    local cur="${line:0:$point}" # Cut word at point
    # Extract current completion word
    cur="${cur##* }"
    # Generate completions
    local compreply=( $(compgen -W "${WORDS[*]}" -- "${cur}" || :) )
    if test ${#compreply[@]} -ne 1 ; then
      printf "%s\n" "${compreply[@]}" | column >&2
    else # Use unique completion
      local oldlen=${#cur}
      # Replace current word with the completion
      READLINE_LINE="${line:0:$((point - oldlen))}${compreply[0]}${line:$point}"
      READLINE_POINT=$(( point + ${#READLINE_LINE} - ${#line} ))
    fi
    true # Return false aborts readline
  }
  set -o emacs # Use emacs readline mode
  bind -x '"\t": _read_completion'
  READOPTS=()
  test -z "${PROMPT:-}" || READOPTS+=(-p "$PROMPT")
  test -z "${INIT:-}" || READOPTS+=(-i "$INIT")
  read -e "${READOPTS[@]}" INPUT
  test -z "$INPUT" ||
    printf "%s\n" "$INPUT"
)

# == Functions ==
declare -A KEYBINDINGS
declare -A MULTIKEYBINDINGS
FIRSTS=""
NEXTS=""
SILENTREFRESH=""

# fzflog revset aliases
revsets_toml()
(
  FZFLOG_DEPTH="$(jj --ignore-working-copy config get jj-fzf.fzflog-depth 2>/dev/null || echo 0)"
  echo "revset-aliases.fzflog = ''' jjlog | ancestors(bookmarks() | remote_bookmarks(), $FZFLOG_DEPTH) ''' "
  echo "revset-aliases.jjlog = ''' $(jj --ignore-working-copy config get revsets.log 2>/dev/null || echo ..) '''"
  echo "template-aliases.'format_short_change_id(id)' = 'id.shortest(8)'"
  echo "template-aliases.'format_short_commit_id(id)' = 'id.shortest(8)'"
)

# fzflog [--revsetname] [rev] - revision log for fzf
fzflog()
(
  # SEE ALSO: jj config get revsets.log
  [[ "${1:-}" == --revsetname ]] && { REVSETNAME=true; shift; } || REVSETNAME=false
  [[ $# -ge 1 ]] &&
    REVSETS_LOG="$1" ||
      REVSETS_LOG=$(jj --ignore-working-copy config get 'jj-fzf.revsets.log' 2>/dev/null || :)
  test -n "$REVSETS_LOG" || REVSETS_LOG="fzflog"
  if $REVSETNAME ; then
    echo "$REVSETS_LOG"
  else
    jj --no-pager --ignore-working-copy \
       --config-toml "$(revsets_toml)" \
       log --color=always -T "$JJ_FZF_ONELINE" -r "$REVSETS_LOG" 2>/dev/null
  fi
)
FUNCTIONS+=( 'fzflog' )

# revset-filter <revset> - assign to jj-fzf.revsets.log
DOC['revset-filter']='Restart `jj-fzf` using the current query string as new revset for this repository.'
revset-filter()
(
  REVSET="$1"
  jj --config-toml "$(revsets_toml)" --no-pager --ignore-working-copy log --no-graph -T '' -r "$REVSET" >/dev/null 2>&1 ||
    REVSET=fzflog
  ( set -x
    jj --ignore-working-copy config set --repo 'jj-fzf.revsets.log' "$REVSET"
  ) || ERROR
)
KEYBINDINGS["Ctrl-R"]="revset-filter"	# overridden below

# Abandon Revision
DOC['abandon']='Use `jj abandon` to remove the currently selected revisions (or divergent commit) from the history.'
abandon()
(
  local -a REVS && parse_xrevs_or_commits REVS '-r' "$@"
  ( set -x
    jj abandon "${REVS[@]}" ) ||
    sleep 1
)
MULTIKEYBINDINGS["Alt-A"]="abandon"

# Bookmark Creation
DOC['bookmark']='Use `jj bookmark {create|set -B}` to (re-)assign a bookmark name to the currently selected revision (or divergent commit).'
bookmark()
(
  R="$(xrev_or_commit "${1:-@}")"
  #echo "# Existing Bookmarks:" && jj --no-pager --ignore-working-copy bookmark list
  readarray -t BOOKMARKS < <(jj --no-pager --ignore-working-copy bookmark list -T 'self.name()++"\n"' | sort | uniq)
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "::$R|$R+" -T 'bookmarks++"\n"' | gsed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && INIT="${NEAREST[0]}" || INIT=""
  PROMPT='Bookmark Name: '
  echo "# Assign Bookmark to:"
  jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T builtin_log_oneline
  # Read bookmark with completion
  B="$(read_completing "${BOOKMARKS[@]}")"
  B="${B%% *}" && B="${B##* }" && test -z "$B" && return
  # See https://git-scm.com/docs/git-check-ref-format
  INVALIDPAT='(//|\.\.|/\.|[ :^~?*]|\[|^/|/$|\.$|^@$|@\{|\\|'$'[\x01-\x1f])'
  [[ "$B" =~ $INVALIDPAT ]] && {
    echo "$SELF: bookmark contains invalid characters: $B" >&2
    false || ERROR
  }
  ( set -x
    jj bookmark set -r "$R" --allow-backwards "$B"
  ) || ERROR
  # jj git export --quiet
)
KEYBINDINGS["Alt-B"]="bookmark"

# Commit (full)
DOC['commit']='Use `jj commit` to describe the currently selected revision and create a new child revision as working-copy.'
commit()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  if test "$R" == "$W" -a "$IMMU" != true ; then
    user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
      test "$O" != "$MSG" ||
	ERROR "Commit cancelled by user"
      ( set -x
	jj commit --message="$MSG"
      ) || sleep 1
  else # R is not @, may be immutable
    [[ $IMMU =~ ^true ]] || {
      user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
	test "$O" != "$MSG" ||
	  ERROR "Commit cancelled by user"
      test "$O" != "$MSG" &&
	( set -x
	  jj describe --no-edit -r "$R" --message="$MSG"
	) || sleep 1
    }
    # open new empty working copy commit
    jj new "$R"
  fi
)
KEYBINDINGS["Alt-C"]="commit"		FIRSTS="$FIRSTS commit"

# Delete Bookmarks and Tags
DOC['delete-refs']='Use `jj bookmark list+delete` to list, selected and delete bookmarks and tags.'
delete-refs()
(
  R="$(xrev_or_commit "${1:-@}")"
  # find first local bookmark in $R, use as query arg
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T 'local_bookmarks++"\n"' | gsed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && B=(-q "${NEAREST[0]}") || B=()
  require_git_dir # exports GIT_DIR
  # select bookmark or tag
  DELETELINE=$(
    "${FZFPOPUP[@]}" \
      --border-label '-[ DELETE BOOKMARK/TAG ]-' --color=border:red,label:red \
      --prompt "Delete > " \
      --header $'\n'"Delete selected Bookmark or Tag" --header-first \
      --no-tac --no-sort +m \
      "${B[@]}" \
      < <(
      # list local bookmarks
      jj --ignore-working-copy bookmark list | # gsed reorders conflicted
	gsed -r ':0; /^\s/!s/ \(conflicted\):/: (conflicted)/; N; $!b0; s/\n\s+/ /g' |
	while read MARK rest ; do
	  printf "%-32s [bookmark] %s\n" "${MARK%:}" "$rest"
	done
      echo
      # list git tags
      git tag -n1 | while read MARK rest ; do
	printf "%-32s [tag] %s\n" "$MARK" "$rest"
      done
    ) )
  # delete given bookmark/tag line
  read MARK WHAT rest <<<"$DELETELINE"
  case "$WHAT" in
    "[bookmark]")	( set -x && jj bookmark delete exact:"$MARK" ) || ERROR ;;
    "[tag]")		( set -x && git tag -d "$MARK" ) || ERROR ;;
  esac
)
KEYBINDINGS["Alt-D"]="delete-refs"

# diffedit
DOC['diffedit']='Use `jj diffedit` to select parts of the content diff to be kept in the currently selected revision.'
diffedit()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj diffedit -r "$R"
  ) || sleep 1
)
KEYBINDINGS["Alt-E"]="diffedit"

# Reset commit author
DOC['author-reset']='Use `jj describe --reset-author` to reset the author and email of the currently selected revision.'
author-reset()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj describe --reset-author --no-edit -r "$R"
  ) ||
    sleep 1
)
KEYBINDINGS["Ctrl-A"]="author-reset"

# Describe Commit Message
DOC['describe']='Use `jj describe` to describe the currently selected revision (or divergent commit).'
describe()
(
  R="$(xrev_or_commit "${1:-@}")"
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  user_editor_on_var "CHANGE-$R.txt" MSG "$R" ||
    ERROR "Describe cancelled by user"
  test "$O" != "$MSG" ||
    return
  (set -x
   jj describe --no-edit -r "$R" --message="$MSG"
  ) || ERROR
)
KEYBINDINGS["Ctrl-D"]="describe"

# File Editor
DOC['file-editor']='Use `jj edit` to switch to the currently selected revision and opens the files touched by this revision in `$EDITOR`.'
file-editor()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  # read files edited by revision
  readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | gsed 's/^\w //')
  # make sure to edit revision
  test "$W" == "$R" || (
    IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
    [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
    set -x
    jj $CMD -r "$R"
  )
  ( set -x
    ${EDITOR:-nano} "${FILES[@]}"
  )
)
KEYBINDINGS["Ctrl-F"]="file-editor"

# Help with JJ commands
DOC['help']='Show the *jj-fzf* help and key binding commands.'
help()
(
  $SELF --help "$@"
)
KEYBINDINGS["Ctrl-H"]="help"

# Split change
DOC['split-interactive']='Use `jj split` to interactively select content diff hunks to be split into a new commit. No text editor is invoked and the new commit gets an empty description.'
split-interactive()
(
  R="$(xrev "${1:-@}")"
  # To avoid message editing, truncate all but the first (original) description
  temp_dir
  cat > $TEMPD/noeditor <<-\__EOF__
	#!/usr/bin/bash
	set -Eeuo pipefail #-x
	TRUNCATE=n
	test $TRUNCATE == y && echo -n > "$1" || :
	gsed 's/TRUNCATE=./TRUNCATE=y/' -i "$0"
	__EOF__
  chmod +x $TEMPD/noeditor
  export JJ_EDITOR="$TEMPD/noeditor" # Override ui.editor to implement --split-with-no-description
  ( set -x
    jj split --interactive -r "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-I"]="split-interactive"

# Diff Browser
DOC['diff']='Use `jj diff` to view differences between the currently selected revision and the working copy.'
diff()
(
  R="$(xrev_or_commit "${1:-@-}" 2>/dev/null)" || exit # invalid revision
  W="$(xrev_or_commit "@")" || ERROR
  REVS=( $(forward_chronologic "$R" "$W") )
  test "${#REVS[@]}" -ge 2 || REVS+=( "${REVS[0]}" )
  (
    # set -x
    jj --color=always log -r "${REVS[0]} | ${REVS[1]}" -T builtin_log_oneline # | gsed -r '/[k-xyz]/!d; s/ +/  /'
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}" --stat
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}"
  ) 2>&1 | $JJFZFPAGER
)
KEYBINDINGS["Ctrl-I"]="diff"

# Backout Commit
DOC['backout']='Use `jj backout` to create new commits that undo the changes made by the currently selected revisions and apply the changes on top of the working-copy.'
backout()
(
  local -a REVS && parse_xrevs_or_commits REVS '-r' "$@"
  ( set -x
    jj backout -d @ "${REVS[@]}"
  ) || die
)
MULTIKEYBINDINGS["Alt-K"]="backout"		FIRSTS="$FIRSTS backout"

# Line Blame: jj-fzf +<line> <gitfile>
if [[ $# == 2 ]] && [[ "${1:0:1}" == + ]] ; then
  absroot="$(readlink -f "$JJROOT")"
  absfile="$(readlink -f "$2")"
  [[ $absfile == $absroot/* ]] && {
    echo absroot=$absroot
    echo absf=$absfile
    file="${absfile:((1+${#absroot}))}"
    echo file=${absfile:((1+${#absroot}))}
    jj --no-pager status
    COMMIT="$(rev_commitid @)"
    EMPTY=$'^[| \033\[0-9;m]*$' # anchored pattern for empty line with git log graph chars
    SIGBY=$'^[| \033\[0-9;m]*Signed-off-by:.*@.*$' # anchored pattern for Signed-off-by
    grep -s -n '' "$file" /dev/null |
    "${FZFPOPUP[@]}" \
      --border-label '-[ LINE HISTORY (EXPERIMENTAL) ]-' --color=border:yellow,label:yellow \
      --preview " git log --graph --no-patch -M -C --find-copies-harder --pretty='%C(blue)%h %C(yellow)%aL %C(reset)%B' -L{2}:{1} --color $COMMIT | gsed -nre '/($EMPTY|$SIGBY)/!p; /$EMPTY/{ p; :NEXT n; /($EMPTY|$SIGBY)/b NEXT; p; }' " \
      --bind "enter:execute( git log -M -C --find-copies-harder -L{2},+7:{1} --color $COMMIT | $JJFZFPAGER)" \
      --header "File Line History" \
      --no-tac --no-sort +m -d: \
      --track --bind 'focus:clear-query+unbind(focus)' \
      -q "${absfile:((1+${#absroot}))}:${1:1}:"
  }
  exit 0
fi

# Start multi-select mode
DOC["multi-select"]='Select multiple revisions to operate on.'
multi-select()
(
  $SELF -m --postcmd-exit || :
)
KEYBINDINGS["Alt-M"]="multi-select"

# New --insert-before
DOC['new-before']='Use `jj new --no-edit --insert-before` to create and insert a new revision before the currently selected revision (or divergent commit).'
new-before()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new --no-edit --insert-before "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-Alt-N"]="new-before"

# New --insert-after
DOC['new-after']='Use `jj new --no-edit --insert-after` to create and insert a new revision after the currently selected revision (or divergent commit).'
new-after()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new --no-edit --insert-after "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-N"]="new-after"

# New
DOC['new']='Use `jj new` to create a new revision on top of the currently selected revision (or divergent commit).'
new()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new "$R"
  ) || sleep 1
)
KEYBINDINGS["Ctrl-N"]="new"		FIRSTS="$FIRSTS new"

# JJ_FZF_OP_LOG_ONELINE16 - Oneline op log with 16 character ids, parsed later on; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_OP_LOG_ONELINE16='
label(if(current_operation, "current_operation"),
  coalesce(
    if(root, format_root_operation(self)),
    concat(
      separate(" ", self.id().short(16), self.user(), self.time().start().ago()), " ",
      self.description().first_line(), " ",
      if(self.tags(), self.tags().first_line()),
    )
  )
)'
OP_LOG_FIRSTLINE='self.id() ++ ": " ++ self.description().first_line() ++ "\n"'

# Show `jj op log` but mark undone operations with '⋯'
op_log_oneline()
(
  temp_dir
  # Determine range of undo operations
  if LAST_OPID=$(jj --no-pager --ignore-working-copy config get jj-fzf.last-undo 2>/dev/null) &&
      jj --no-pager --ignore-working-copy op log -n1 --no-graph -T "$OP_LOG_FIRSTLINE" | grep -qF ": undo operation $LAST_OPID" ; then
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16" |
      gsed -r "1,/${LAST_OPID:0:16}\b/s/([@○])/⋯/" # ⮌ ⋯ ⤺↶
  else
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16"
  fi
)
FUNCTIONS+=( 'op_log_oneline' )

# Oplog
DOC['op-log']='Use `jj op log` to browse the recent operations log. Use hotkeys to change the preview between diff, history and oplog entry mode. Undo the selected operation or restore its working copy into a new commit.'
op-log()
(
  temp_dir
  echo > $TEMPD/oplog.env
  H=$'\n'
  H="$H"$'Ctrl-D: Preview the differences of an operation via `jj op diff -f <op> -t @`\n'
  H="$H"$'Ctrl-L: Preview history at a specific operation via `jj log -r ..`\n'
  H="$H"$'Ctrl-P: Preview changes in an operation with patch via `jj op show -p <op>`\n'
  H="$H"$'Ctrl-S: Preview "@" at a specific operation via `jj show @`\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject working copy of the selected operation as historic commit before @\n'
  H="$H"$'Alt-K: Kill undo memory (marked `⋯`), to restart undo at the top\n'
  H="$H"$'Alt-R: Restore repository to the selected operation via `jj op restore`\n'
  H="$H"$'Alt-Y: Undo/redo the selected operation entry\n'
  H="$H"$'Alt-Z: Undo the next operation (not already marked `⋯`)\n'
  echo 'VIEW=preview_oppatch'		>> $TEMPD/oplog.env
  export FZF_DEFAULT_COMMAND="$SELF op_log_oneline" TEMPD SELF
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label '-[ OP-LOG ]-' --color=border:bright-yellow,label:bright-yellow \
    --prompt "Operation > " \
    --header "$H" --header-first \
    --bind "ctrl-d:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_opdiff/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-l:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_oplog/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-p:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_oppatch/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-s:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_opshow/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "alt-j:execute( $SELF restore-commit {} )+abort" \
    --bind "alt-k:execute( $SELF undo-reset {} )+$RELOAD" \
    --bind "alt-r:execute( $SELF op-restore {} )+abort" \
    --bind "alt-w:execute( $SELF restore-commit {} )+abort" \
    --bind "alt-y:execute( $SELF undo-op {} )+$RELOAD" \
    --bind "alt-z:execute( $SELF undo )+$RELOAD" \
    --bind 'enter:execute( [[ {} =~ $OPPAT ]] || exit && COMMIT=$(jj log --no-pager --no-graph -T commit_id -r @ --at-operation "${BASH_REMATCH[1]}") && $SELF logrev "$COMMIT" )' \
    --preview-window 'nowrap,right,border-left' \
    --preview '[[ {} =~ $OPPAT ]] || exit; . $TEMPD/oplog.env && $SELF $VIEW {}' \
    --no-tac --no-sort +m
  # TODO: remove alt-w in jj-fzf-0.26
)
KEYBINDINGS["Ctrl-O"]="op-log"

undo-op()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  ( set -x
    jj op undo $OP
  ) || ERROR
)
FUNCTIONS+=( 'undo-op' )

restore-commit()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  COMMIT="$(jj --no-pager --ignore-working-copy --at-op $OP show --tool true -T commit_id -r @)"
  echo "# $SELF: insert working copy commit (${COMMIT:0:12}) from operation ${OP:0:12} before @"
  ( set -x
    jj new --no-edit --insert-before @
    jj restore --from "$COMMIT" --to @- --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'restore-commit' )

op-restore()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following jj op restore"
  ( set -x
    jj op restore "$OP"
  ) || ERROR
)
FUNCTIONS+=( 'op-restore' )

# Show `jj evolog`
evolog_oneline()
(
  R="$1"
  jj evolog --no-pager --ignore-working-copy --color=always -T "$EVOLOG_ONELINE" -r "$R"
)
FUNCTIONS+=( 'evolog_oneline' )

# Inject historic commit of a revision
evolog-inject()
(
  R="$(xrev "${1:-}")"
  [[ " $2 " =~ $HEX7PAT ]] || die "missing commit"
  C="$(xrev_as_commit "${BASH_REMATCH[1]}")"
  MSG="$(rev_description "$C")"
  NEWREV=
  jj_new_before_no_edit NEWREV "$R" "$MSG"
  ( set -x
    jj restore --from "$C" --to "$NEWREV" --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'evolog-inject' )

# Show `jj evolog`
evolog_pager()
(
  [[ " $* " =~ $HEX7PAT ]] && {
    # builtin_log_detailed
    jj --no-pager --ignore-working-copy evolog --color=always -p -r "${BASH_REMATCH[1]}" -T "$JJ_FZF_SHOWDETAILS" 2>&1 |
      $JJFZFPAGER
  }
)
FUNCTIONS+=( 'evolog_pager' )

# Evolog
DOC['evolog']='Use `jj evolog` to browse the evolution of the selected revision. Inject historic commits into the ancestry without changing descendants.'
evolog()
{
  R="$(xrev_or_commit "${1:-@}")"
  temp_dir
  H=$'\n'
  H="$H"$'Enter: Browse evolog with diff\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject evolog entry as historic commit before the revision without changing it.\n'
  export FZF_DEFAULT_COMMAND="$SELF evolog_oneline $R"
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label "-[ EVOLOG $R ]-" --color=border:yellow,label:bright-yellow \
    --prompt "Evolog > " \
    --header "$H" --header-first \
    --bind "enter:execute( $SELF evolog_pager {} )" \
    --bind "alt-j:execute( $SELF evolog-inject $R {} )+abort" \
    --preview-window 'nowrap,right,border-left' \
    --preview "$SELF preview_evolog {}" \
    --no-tac --no-sort +m
}
KEYBINDINGS["Ctrl-T"]="evolog"

# Split files
DOC['split-files']='Use `jj split` in a loop to split each file modified by the currently selected revision into its own commit.'
split-files()
(
  R="$(xrev "${1:-@}")"
  # read files affected by $R
  mapfile -t MAPFILE < <(jj diff --name-only -r "$(rev_commitid "$R")")
  [[ ${#MAPFILE[@]} -gt 1 ]] ||
    return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following split"
  # create n-1 new commits from n files
  while [[ ${#MAPFILE[@]} -gt 1 ]] ; do
    unset 'MAPFILE[-1]' # unset 'MAPFILE[${#MAPFILE[@]}-1]'
    export JJ_EDITOR='true' # Override ui.editor to implement --split-with-no-description
    ( set -x
      jj split -r "$R" -- "${MAPFILE[@]}"
    ) || ERROR
  done
)
KEYBINDINGS["Alt-F"]="split-files"

# Fetch and push to remote Git repositories
DOC['push-remote']='Use `jj git fetch` and `jj git push --tracked` to update the local and remote repositories. Pushing needs confirmation after a dry-run.'
push-remote()
(
  ( set -x
    jj git fetch
    jj git push --tracked --dry-run
  ) || ERROR
  read -p 'Try to push to remote? ' YN
  [[ "${YN:0:1}" =~ [yY] ]] ||
    exit
  ( set -x
    jj git push --tracked
  ) || ERROR
)
KEYBINDINGS["Ctrl-P"]="push-remote"

# Absorb a content diff into mutable ancestors
absorb()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj absorb --from "$R"
  ) || ERROR
)
DOC['absorb']='Use `jj absorb` to split the content diff of the current revision and squash pieces into related mutable ancestors.'
KEYBINDINGS["Alt-O"]="absorb"

# Squash Into Parent
DOC['squash-into-parent']='Use `jj squash` to move the changes from the currently selected revision(s) (or divergent commit) into its parent.'
squash-into-parent()
(
  local -a REVS && parse_xrevs_or_commits REVS '' "$@"
  W="$(xrev_or_commit "@")"
  PARENTS="roots( none() $(printf '|%s-::' "${REVS[@]}") )"
  PARENTS=( $(list_revset " $PARENTS ") )
  test ${#PARENTS[@]} -eq 1 ||
    ERROR "revisions lack common parent"
  if [[ " ${REVS[*]} " == *" $W "* ]]; then
    # The working copy @ is to be squashed.
    # Squashing without --keep-emptied would start a new branch at @- which is
    # undesired if @+ exists. But using --keep-emptied does not squash the
    # message. As a workaround, create a new @+, so we never squash directly
    # from @. This new working copy will receive any children from the original
    # squashed working copy.
    ( set -x
      jj new --insert-after @
    ) || ERROR
  fi
  ( set -x
    jj squash --from "$(IFS='|' && echo "${REVS[*]}")" --into "${PARENTS[0]}"
  ) || ERROR
)
MULTIKEYBINDINGS["Alt-Q"]="squash-into-parent"

# Squash @ Commit
DOC['squash-@-into']='Use `jj squash` to move the changes from the working copy into the currently selected revision.'
squash-@-into()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  test "$R" == "$W" && return
  # See squash-into-parent, for why we need `new --insert-before` when squashing @.
  ( set -x
    jj new --insert-before @
    jj squash --from "$W" --into "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-W"]="squash-@-into"

# Reparent a revision
DOC['reparenting']='Start a dialog to add/delete parents of the current revision. Also supports `jj simplify-parents` after reparenting.'
reparenting()
(
  SRC="$(xrev "${1:-@}")"
  IMMU=$($JJFZFSHOW -r "$SRC" -T 'if(immutable, "true")')
  test "$IMMU" != true || exit 0
  temp_dir
  jj --no-pager --ignore-working-copy log --no-graph -T 'change_id ++ "\n"' -r all:"$SRC-" > $TEMPD/reparenting.lst
  echo 'OP="|"'		>  $TEMPD/reparenting.env
  echo 'SIMPLIFY=false'	>> $TEMPD/reparenting.env
  export SRC TEMPD
  # Parse jj log lines into reparenting.revs
  reparenting_revs()
  {
    mapfile -t PARENTS < $TEMPD/reparenting.lst && PARENTS="( $(join_args '|' "${PARENTS[@]}") )"
    test "$OP" == '|' && FILTER="~ ($SRC|$PARENTS)" || FILTER="& $PARENTS"
    for ARG in "$@" ; do
      [[ "$ARG" =~ $REVPAT ]] || continue
      R=$(jj --no-pager --ignore-working-copy log --no-graph -r "${BASH_REMATCH[1]} $FILTER" -T change_id)
      test -z "$R" ||
	echo "$R"
    done > $TEMPD/reparenting.revs
    mapfile -t REVS < $TEMPD/reparenting.revs && EXPR="$SRC-"
    test "${#REVS[@]}" -ge 1 && EXPR="$SRC- $OP ( $(join_args '|' "${REVS[@]}") )"
    # sort, so we generally merge younger branches into older branches
    forward_chronologic "$EXPR" > $TEMPD/newparents.lst
  }
  # Preview reparenting command for reparenting.revs
  reparenting_cmd()
  (
    echo
    echo "CHANGE PARENTS:"
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst && NEWPARENTS="$(join_args ' | ' "${NEWPARENTS[@]}")"
    echo "jj rebase --source \"$SRC\" --destination \\"
    echo "  \"all: $NEWPARENTS\""
    $SIMPLIFY && echo "jj simplify-parents --revisions \"$SRC\\"
    echo
    echo "SOURCE REVISION:"
    jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r all:"$SRC | $SRC-"
    echo
    test "$OP" == '|' && deladd='ADD' || deladd='REMOVE'
    echo "$deladd PARENTS:"
    test "$OP" == '|' && deladd='+ ' || deladd='- '
    while read R ; do
      echo -n "$deladd"
      jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline -r "$R"
    done < $TEMPD/reparenting.revs
  )
  # Provide functions for FZF
  export -f reparenting_revs reparenting_cmd join_args forward_chronologic backward_chronologic reverse_array
  H=$'\n'
  H="$H""Alt-A: ADD    - Add currently selected revisions as new parents"$'\n'
  H="$H""Alt-D: DEL    - Delete selected revisions from current list of parents"$'\n'
  H="$H""Alt-P: SIMPLIFY-PARENTS - Use simplify-parents after reparenting"$'\n'
  export FZF_DEFAULT_COMMAND="$SELF fzflog"
  # FZF select parents
  "${FZFPOPUP[@]}" \
      --border-label '-[ CHANGE PARENTS ]-' --color=border:cyan,label:cyan \
      --preview ". $TEMPD/reparenting.env && reparenting_revs {+} && reparenting_cmd" \
      --prompt "Parents > " \
      --header "$H" --header-first \
      --bind "alt-a:execute-silent( gsed 's/^OP=.*/OP=\"|\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-d:execute-silent( gsed 's/^OP=.*/OP=\"~\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-p:execute-silent( gsed 's/^SIMPLIFY=false/SIMPLIFY_=/; s/^SIMPLIFY=true/SIMPLIFY=false/; s/^SIMPLIFY_=/SIMPLIFY=true/' -i $TEMPD/reparenting.env )+refresh-preview" \
      -m --color=pointer:grey \
      --no-tac --no-sort > $TEMPD/selections.txt &&
    mapfile -t selections < $TEMPD/selections.txt &&
    source $TEMPD/reparenting.env &&
    reparenting_revs "${selections[@]}" &&
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst &&
    test "${#NEWPARENTS[@]}" -gt 0 ||
      exit # Reparenting cancelled
  # Re-parent revisions
  ( set -x
    # Ordering is not preserved with 'all:(.|.|.)', only with -d. -d. -d.
    jj rebase --source "$SRC" "${NEWPARENTS[@]/#/-d}"
  ) || ERROR
  # simplify-parents
  ! $SIMPLIFY || (
    set -x
    jj simplify-parents --revisions "$SRC"
  ) || ERROR
)
KEYBINDINGS["Alt-P"]="reparenting"	FIRSTS="$FIRSTS reparenting"

# Rebase Branch/Source/Revision After/Before/Destination
DOC['rebase']='Start a dialog to `jj rebase` or `jj duplicate` a set of revisions (or branch) possibly with descendants, onto, before or after another revision. Also supports `jj simplify-parents` afterwards.'
rebase()
(
  local -a RBREVS && parse_xrevs_or_commits RBREVS '' "$@"
  temp_dir
  CREVS="${RBREVS[*]}" && CREVS="${CREVS// /|}" # combined revisions
  echo > $TEMPD/rebase.env
  echo 'DP='			>> $TEMPD/rebase.env
  echo 'CH='			>> $TEMPD/rebase.env
  echo 'TO=--destination'	>> $TEMPD/rebase.env
  echo 'SP=false'		>> $TEMPD/rebase.env
  echo 'II='			>> $TEMPD/rebase.env
  if test ${#RBREVS[@]} -le 1 ; then
    echo 'FR=--source'		>> $TEMPD/rebase.env
  else
    echo 'FR=--revisions'	>> $TEMPD/rebase.env
    CREVS="($CREVS)"		# parenthesis around multiple revisions
  fi
  QREVS="'$CREVS'"		# quoted revisions
  export JJFZFONELINE
  PREVIEW=". $TEMPD/rebase.env"
  PREVIEW="$PREVIEW"' && echo'
  PREVIEW="$PREVIEW"' && { test -z "$DP" || echo jj duplicate -r '"$QREVS"'$CH $TO $TARGET ; }'
  PREVIEW="$PREVIEW"' && { test -n "$DP" || echo jj rebase $II $FR '"$QREVS"' $TO $TARGET ; }'
  PREVIEW="$PREVIEW"' && { test $SP == true && echo && echo jj simplify-parents -r '"$QREVS"' || : ; } && echo'
  PREVIEW="$PREVIEW"' && F=${FR#--} && echo ${F^^}: && $JJFZFONELINE -r '"$QREVS"' && echo'
  PREVIEW="$PREVIEW"' && T=${TO#--} && echo ${T^^}: && $JJFZFONELINE -r '"$QREVS"' && echo'
  PREVIEW="$PREVIEW"' && echo COMMON: && $JJFZFONELINE -r "heads( ::('"$CREVS"') & ::$TARGET)"'
  H=''
  H="$H""Alt-B: BRANCH    - Rebase the whole branch relative to destination's ancestors"$'\n'
  H="$H""Alt-C: DUP-DESCENDANTS - duplicate the specified revisions and descendants"$'\n'
  H="$H""Alt-D: DUPLICATE - duplicate the specified revisions"$'\n'
  H="$H"'Alt-I: IGNORE-IMMUTABLE - Use `jj rebase --ignore-immutable` command'$'\n'
  H="$H"'Alt-P: SIMPLIFY-PARENTS - Use `jj simplify-parents` after rebasing'$'\n'
  H="$H""Alt-R: REVISION  - Rebase only given revision, moves descendants onto parent"$'\n'
  H="$H""Alt-S: SOURCE    - Rebase specified revision together with descendants"$'\n'
  H="$H""Ctrl-A: AFTER       - The revision to insert after"$'\n'
  H="$H""Ctrl-B: BEFORE      - The revision to insert before"$'\n'
  H="$H""Ctrl-D: DESTINATION - The revision to rebase onto"$'\n'
  export FZF_DEFAULT_COMMAND="$SELF fzflog"
  TARGET=$("${FZFPOPUP[@]}" \
	     --border-label '-[ REBASE ]-' --color=border:green,label:green \
	     --preview "[[ {} =~ $REVPAT ]] || exit; export TARGET=\"\${BASH_REMATCH[1]}\"; $PREVIEW " \
	     --prompt "Rebase > " \
	     --header "$H" --header-first \
	     --bind "alt-d:execute-silent( gsed 's/^CH=.*/CH=/;   s/^DP=.*/DP=1/; s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-c:execute-silent( gsed 's/^CH=.*/CH=::/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--source/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-b:execute-silent( gsed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--branch/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-s:execute-silent( gsed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--source/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-r:execute-silent( gsed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-p:execute-silent( gsed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-i:execute-silent( gsed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-d:execute-silent( gsed 's/^TO=.*/TO=--destination/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-a:execute-silent( gsed 's/^TO=.*/TO=--insert-after/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-b:execute-silent( gsed 's/^TO=.*/TO=--insert-before/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --no-tac --no-sort +m )
  [[ "$TARGET" =~ $REVPAT ]] &&
    TARGET="${BASH_REMATCH[1]}" ||
      exit 0
  TARGET="$(xrev_or_commit "$TARGET")"
  source $TEMPD/rebase.env
  rm -f TEMPD/rebase.env
  # duplicate revisions
  if test -n "$DP" ; then
    ( set -x
      jj duplicate -r "$CREVS$CH" $TO "$TARGET"
    ) || ERROR
  else # rebase revisions
    ( set -x
      jj rebase $II $FR all:"$CREVS" $TO "$TARGET"
    ) || ERROR
  fi
  # simplify-parents
  ! $SP ||
    ( set -x
      jj simplify-parents -r "$CREVS"
    ) || ERROR
)
MULTIKEYBINDINGS["Alt-R"]="rebase"			FIRSTS="$FIRSTS rebase"

# Restore File
DOC['restore-file']='Start a dialog to select a file to `jj restore` from the currently selected revision into the working copy.'
restore-file()
(
  R="$(xrev "${1:-@}")"
  temp_dir
  # find differing fileset
  mapfile -t MAPFILE < <(jj --no-pager --ignore-working-copy diff --name-only --from @ --to "$R")
  [[ ${#MAPFILE[@]} -ge 1 ]] ||
    return
  # file picker
  PREVIEW='read F <<<{} && test -n \"$F\" || exit '
  PREVIEW+='&& jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r "'"$R|@"'" && echo '
  PREVIEW+='&& jj --no-pager --ignore-working-copy diff --color=always -s --git --from @ --to "'"$R"'" -- cwd-file:"$F" '
  printf '%s\n' "${MAPFILE[@]}" |
    "${FZFPOPUP[@]}" \
      --border-label '-[ RESTORE-FILE ]-' --color=border:blue,label:blue \
      --preview "$PREVIEW" \
      --header "Restore File from $R into @" \
      > $TEMPD/restore-file.sel
  # restore file if any
  FILENAME="$(<$TEMPD/restore-file.sel)"
  test -z "$FILENAME" || (
    set -x
    jj restore --from "$R" -- cwd-file:"$FILENAME"
  ) || ERROR
)
KEYBINDINGS["Alt-S"]="restore-file"

# Tag Creation
DOC['tag']='EXPERIMENTAL: Enter a tag name to create a new unsigned, annotated tag at the selected revision with `git tag`.'
tag()
(
  R="$(xrev "${1:-@}")"
  C="$(rev_commitid "$R")"
  require_git_dir
  read -p 'Tag Name: ' B &&
    test -n "$B" ||
      return
  M="$(git log -1 --oneline "$C")"
  ( set -x
    git tag -a "$B" -m "$M" "$C"
  ) || ERROR
  #  jj git import --quiet
)
KEYBINDINGS["Alt-T"]="tag"

# Log single change
logrev()
(
  R="$(xrev_as_commit "${1:-@}")"
  (
    jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$R"
    jj_show_diff --color=always -T '"\n"' -r "$R"
  ) | $JJFZFPAGER
)
FUNCTIONS+=( 'logrev' )

# Log flat change history
DOC['log']='Use `jj log` to browse the history including patches, starting from the selected revision (or divergent commit).'
log()
{
  R="$(xrev_or_commit "${1:-@}")"
  jj log --ignore-working-copy --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -r "::$R" -s -p --ignore-space-change \
    | $JJFZFPAGER
}
KEYBINDINGS["Ctrl-L"]="log"

# vivifydivergent
DOC['vivifydivergent']='When a revision has more than one visible commit, it becomes a divergent revision. This command uses `jj new+squash …` to create a new *change_id* for the currently selected revision, effectively resolving the divergence.'
vivifydivergent()
(
  # fetch commit_id of a divergent revision
  COMMIT="$(xrev_as_commit "${1:-@}")" &&
    WCOPY="$(xrev_as_commit "@")" ||
      die 'no divergent revision'
  # leave working copy alone, unless it is $1
  test "$COMMIT" == "$WCOPY" && NOEDIT= || NOEDIT=--no-edit
  echo "# $SELF vivifydivergent $COMMIT" >&2
  jj --no-pager log --no-graph -T builtin_log_oneline -r "$COMMIT" # --ignore-working-copy
  export JJ_EDITOR='true' # Override ui.editor to implement --squash-with-no-description
  ( set -x
    jj new --insert-after "$COMMIT" $NOEDIT
    jj squash --from "$COMMIT" --into "$COMMIT+"
  ) || ERROR
)
KEYBINDINGS["Alt-V"]="vivifydivergent"	NEXTS="$NEXTS vivifydivergent"

# Gitk View
DOC['gitk']='DEPRECATED: Start `gitk` to browse the *Git* history of the repository.'
gitk()
(
  R="$(xrev "${1:-@}")"
  # jj git export --quiet
  COMMIT="$(rev_commitid "$R")"
  git update-index --refresh || :
  #test -e "$JJROOT/.jj/repo/store/git" && export GIT_DIR="$JJROOT/.jj/repo/store/git" || export GIT_DIR="$JJROOT/.git"
  # readarray -t HEADS < <( jj --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r ' heads(..) ' )
  # beware gitk is executable and sh function
  ( set -x
    exec gitk --branches --tags --remotes --select-commit=$COMMIT $COMMIT HEAD -- # "${HEADS[@]}"
  ) || ERROR
  # jj git import --quiet
)
KEYBINDINGS["Ctrl-V"]="gitk"

# Toggle between various diff formats
DOC['word-diff-toggle']='Toggle/cycle between various diff formats, including JJ, Git, word-diff, with and without whitespace.'
word-diff-toggle()
(
  currentmode=$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true)
  diffmodes=("jj-diff" "diff-b" "word-b")
  index=0
  for i in "${!diffmodes[@]}"; do
    [[ "${diffmodes[$i]}" == "$currentmode" ]] && { index=$i && break; }
  done
  next_index=$(( (index + 1) % ${#diffmodes[@]} ))
  jj --no-pager --ignore-working-copy config set --repo jj-fzf.diff-mode "${diffmodes[$next_index]}"
)
KEYBINDINGS["Ctrl-W"]="word-diff-toggle"	SILENTREFRESH="$SILENTREFRESH word-diff-toggle"

# Edit (New) Working Copy
DOC['edit']='Use `jj {edit|new}` to set the currently selected revision (or divergent commit) as the working-copy revision. Will create a new empty commit if the selected revision is immutable.'
edit()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
  ( set -x
    jj $CMD -r "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-E"]="edit"

# Swap Commits
DOC['swap-commits']='Use `jj rebase --insert-before` to quickly swap the currenly selected revision with the revision immediately before it.'
swap-commits()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj rebase -r "$R" --insert-before "$R-"
  ) || ERROR
)
KEYBINDINGS["Alt-X"]="swap-commits"

# Undo last JJ op
DOC['undo']='Use `jj op undo` to undo the last operation performed by `jj` that was not previously undone.'
undo()
(
  TSELFID='self.id() ++ "\n"'
  if LAST_OPID=$(jj --no-pager --ignore-working-copy config get jj-fzf.last-undo 2>/dev/null) &&
      jj --no-pager --ignore-working-copy op log -n1 --no-graph -T "$OP_LOG_FIRSTLINE" | grep -qF ": undo operation $LAST_OPID" ; then
    # last operation in op log was undo of operation $LAST_OPID
    NEXT_OP="$LAST_OPID-"
  else
    LAST_OPID="<none>"
    NEXT_OP="@"
  fi
  NEXT_OP_ID="$(jj --no-pager --ignore-working-copy op log --at-operation="$NEXT_OP" -n1 --no-graph -T "$TSELFID")"
  echo "# $SELF: jj-fzf.last-undo=${LAST_OPID:0:20} next-undo=${NEXT_OP_ID:0:20}"
  ( set -x
    jj op undo "$NEXT_OP_ID"
  ) || ERROR
  jj --no-pager --ignore-working-copy config set --repo jj-fzf.last-undo "$NEXT_OP_ID"
  # Known cases where the above multi-step undo logic breaks:
  # * Undo of an operation like "reconcile divergent operations" just gives "Error: Cannot undo a merge operation"
)
MULTIKEYBINDINGS["Alt-Z"]="undo"

# Reset undo memory
undo-reset()
(
  jj --no-pager --ignore-working-copy config unset --repo jj-fzf.last-undo
)
FUNCTIONS+=( 'undo-reset' )

# Minimal Markdown transformations for the terminal
sedmarkdown()
(
  B=$'\e[1m'              # Bold
  T=$'\e[32;1;4m'         # Title
  H=$'\e[1;4m'            # Heading
  C=$'\e[36m'             # Code
  I=$'\e[3m'              # Italic
  U=$'\e[4m'              # Underline
  Z=$'\e[0;24m'           # Reset
  W='[][<>{}A-Z| $@○◆a-z0-9/ ↑←↓→-⇿ :….()+-]'      # Word-like chars (english)
  SEDSCRIPT="
    s/\r\`\`\`+\w*(([^\`]*|\`[^\`])+)\r\`\`\`+/$C\1$Z\n/g       # Code block with backticks
    s/\r~~~+\w*(([^~]*|~[^~])+)\r~~~+/$C\1$Z\n/g                # Code block with tilde
    s/(^|\r)# ([^\r]+)[ #]*\r/\1$T\2$Z\r/g                      # Title Heading
    s/(^|\r)##+ ([^\r]+)[ #]*\r/\1$H\2$Z\r/g                    # Headings
    s/(\r\s?\s?)[-*] (\w+\b:)?/\1$B* \2$Z/g                     # List bullet
    s/(\s)\*\*($W+)\*\*/\1$B\2$Z/g                              # Bold
    s/(\s)\*($W+)\*([^*])/\1$I\2$Z\3/g                          # Italic
    s/(\s)_($W+)_([^_])/\1$U\2$Z\3/g                            # Underline
    s/(\s)\`($W+)\`([^\`])/\1$C\2$Z\3/g                         # Code
    s/\r?<!--([^-]|-[^-]|--[^>])*-->//g                         # Html Comments
    s,(\bhttps?://[^ ()\r]+),$U\1$Z,g                           # Link
  "
  tr \\n \\r |
    { $COLOR && gsed -re "$SEDSCRIPT" || cat ; } |
    tr \\r \\n
)

# == MULTISELECT ==
if test -n "$MULTISELECT" ; then
  KEYBINDINGS=$(declare -p MULTIKEYBINDINGS) && declare -A KEYBINDINGS="${KEYBINDINGS#*=}"	# copy MULTIKEYBINDINGS -> KEYBINDINGS
else
  for key in "${!MULTIKEYBINDINGS[@]}"; do							# merge MULTIKEYBINDINGS -> KEYBINDINGS
    KEYBINDINGS["$key"]="${MULTIKEYBINDINGS[$key]}"
  done
fi

# == --help ==
HELPKEYS=$(declare -p KEYBINDINGS) && declare -A HELPKEYS="${HELPKEYS#*=}"	# copy KEYBINDINGS -> HELPKEYS
if test -n "$HELPKEYBINDINGS" ; then
  # Key bdingins only shown in long form help
  HELPKEYS[Shift-↑]='preview-up'
  HELPKEYS[Ctrl-↑]='preview-up'
  DOC['preview-up']='Scroll the preview window.'
  HELPKEYS[Shift-↓]='preview-down'
  HELPKEYS[Ctrl-↓]='preview-down'
  DOC['preview-down']='Scroll the preview window.'
  HELPKEYS[Ctrl-U]='clear-filter'
  DOC['clear-filter']='Discard the current *fzf* query string.'
  HELPKEYS[Alt-H]='toggle-show-keys'
  DOC['toggle-show-keys']='Display or hide the list of avilable key bindings, persist the setting in `jj-fzf.show-keys` of the `jj` user config.'
else
  test -n "$MULTISELECT" && {
    HELPKEYS[Alt-M]='multi-select-exit'
    HELPKEYS[TAB]='toggle-select'
  }
fi
DISPLAYKEYS="${!HELPKEYS[@]}"
DISPLAYKEYS=$(sort <<<"${DISPLAYKEYS// /$'\n'}" | grep -vF 'Ctrl-Alt-')
if test -n "$HELPKEYBINDINGS" ; then
  tty -s <&1 && COLOR=true || { COLOR=false; JJFZFPAGER=cat; }
  test -z "$COLORALWAYS" || COLOR=true
  ( :
    for k in $DISPLAYKEYS ; do
      NAME="${HELPKEYS[$k]}"
      echo && echo "## $k: _$NAME""_"
      D="${DOC[$NAME]:-}"
      test -z "$D" ||
	echo "$D" | fold -s -w78 | gsed 's/^/  /'
    done
  )
  exit 0
fi

# == --key-bindings ==
list_key_bindings()
{
  LINES="${LINES:-$JJFZF_LINES}" COLUMNS="${COLUMNS:-$JJFZF_COLUMNS}" # unset by transform-header()
  test "$COLUMNS" -ge 218 && W=4 || {
      test "$COLUMNS" -ge 166 && W=3 || {
	  test "$COLUMNS" -ge 114 && W=2 || W=1; }; }
  [[ ${#DISPLAYKEYS} -gt $(($LINES * $W * 2)) ]] && {
    echo "Ctrl-H: help"		# no space left for jj-fzf.show-keys toggle
    exit 0
  }
  SHOW_KEYS="$(jj --ignore-working-copy config get 'jj-fzf.show-keys' 2>/dev/null || echo true)"
  [[ "$*" =~ --key-toggle ]] && {
    SHOW_KEYS="$(echo "$SHOW_KEYS" | gsed 's/^false/x/; s/^true/false/; s/^x/true/')"
    jj --ignore-working-copy config set --user 'jj-fzf.show-keys' "$SHOW_KEYS"
  }
  $SHOW_KEYS || {
    echo "Ctrl-H: help  Alt-H: show-keys"
    exit 0
  }
  OUTPUT=""
  i=0; WHITE="                                                                                "
  for k in $DISPLAYKEYS ; do
    S="$k: ${HELPKEYS[$k]}"	# printf(1) cannot count UTF-8 continuation chars (0x80-0xBF)
    test ${#S} -lt 26 && S="$S${WHITE:0:$(( 26 - ${#S} ))}"	# so, format like %-26s
    OUTPUT="$OUTPUT$S" #$HIGH"
    i=$(($i+1))
    test 0 == $(($i % $W)) &&
      OUTPUT="$OUTPUT"$'\n' ||
	OUTPUT="$OUTPUT "
  done
  echo -n "$OUTPUT"
}
if test -n "$SHOWKEYBINDINGS" ; then
  list_key_bindings "$@"
  exit 0
fi

# == Function calling ==
if [[ "${1:-}" =~ ^[a-z0-9A-Z_+@-]+ ]] && [[ " ${KEYBINDINGS[*]} ${FUNCTIONS[*]} " =~ \ $1\  ]] ; then
  # Sync JJ working-copy before and after func, according to user config, but avoid paging
  ( set -e
    jj status --no-pager >/dev/null
    trap 'jj status --no-pager >/dev/null' 0 HUP INT QUIT TRAP USR1 PIPE TERM
    FUNC="$1" "$@"
  ) # preserves $FUNC exit status
  exit $?
fi

# == Sync ==
# Sync JJ before starting FZF, so user snapshot config and snapshot errors take effect
( set -x
  jj --no-pager status
) || exit $?

# === TEMPD ==
if test -z "${TEMPD:-}" ; then
  temp_dir
  export JJFZF_OUTER_TEMPD="$TEMPD" JJFZF_COLUMNS="$COLUMNS" JJFZF_LINES="$LINES"
fi
FZFEXTRAS=()
EXECKILLME=
$ONESHOT && {
  echo > "$TEMPD/killme.0"		# ignore first :focus:
  echo "$$" > "$TEMPD/killme.pid"	# then kill FZF
  FZFEXTRAS+=(
    --bind "start:execute( ps -o ppid= \$\$ > $TEMPD/killme.pid )"
    --bind "focus:execute-silent( test -e $TEMPD/killme.0 && rm -f $TEMPD/killme.0 || rm -f $TEMPD/killme.pid )"
  )
  EXECKILLME="+execute( test -e $TEMPD/killme.pid && kill -1 \$(<$TEMPD/killme.pid) )"
}

# == BIND COMMANDS ==
RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
BIND=()
for k in "${!KEYBINDINGS[@]}" ; do
  fun="${KEYBINDINGS[$k]}"
  postcmd=""
  [[ " $FIRSTS " == *" $fun "* ]] && postcmd="+first"
  [[ " $SILENTREFRESH " == *" $fun "* ]] && EXECUTE=execute-silent || EXECUTE=execute
  [[ " $NEXTS " == *" $fun "* ]] && postcmd="+down"
  [[ " $SILENTREFRESH " == *" $fun "* ]] && REPAINT=refresh-preview || REPAINT="$RELOAD"
  [[ -v MULTIKEYBINDINGS["$k"] ]] && FUNARGS='{+}' || FUNARGS='{} {q}'
  [[ -n "$POSTCMDEXIT" ]] && postcmd=+abort
  BIND+=( --bind "${k,,}:$EXECUTE( $SELF $MULTISELECT $fun $FUNARGS )$EXECKILLME+$REPAINT$postcmd" )
done

# == FZF ==
export FZF_DEFAULT_COMMAND="$SELF fzflog"
fzflog 2>&1 |
  fzf \
  "${FZFSETTINGS[@]}" "${FZFEXTRAS[@]}" \
  --bind "ctrl-u:clear-query+clear-selection+clear-screen" \
  --bind "ctrl-z:execute( $JJSUBSHELL )+execute-silent( jj --no-pager status )+$RELOAD" \
  --bind "alt-m:abort" \
  --bind "f5:$RELOAD" \
  --bind "enter:execute( $SELF logrev {} {q} )$EXECKILLME+$RELOAD" \
  "${BIND[@]}" \
  --bind "ctrl-r:transform-query( $SELF revset-filter {q} )+become( exec $SELF )" \
  --preview " exec $SELF preview {} {q} " \
  --header "$(list_key_bindings)" --header-first \
  --bind "alt-h:transform-header:$SELF $MULTISELECT --key-bindings --key-toggle" \
  --prompt "  $(fzflog --revsetname) ${MULTISELECT/-m/(MULTI)}> " \
  --no-tac --no-sort +m $MULTISELECT

# Notes:
# * Do not use 'exec' as last command, otherwise trap-handlers are skipped.
# * Ctrl-R: This must be rebound to run transform-query, ideally we would just transform-query+transform-prompt+reload
#   but that crashes fzf-0.44.1 when the cursor position is after the new revset length, so we use become().
# * Avoid needless $($SELF...) invocations, these cause significant slowdowns during startup
