083cc2c8e8742bab8cce8c73a3e96f398e6b2da7

Author
Marc Cornellà <marc@mcornella.com>
Committer
GitHub <noreply@github.com>
Date

Message

feat(async)!: implement async prompt API and apply to git prompt (#12257)

BREAKING CHANGE: the `git_prompt_info` prompt function has been
reworked by default to use the new async prompt feature. If you're
experiencing issues see #12257.

Co-authored-by: Carlo Sala <carlosalag@protonmail.com>

Diff

This diff is truncated to protect this page.

  1diff --git a/lib/async_prompt.zsh b/lib/async_prompt.zsh
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..c6d03328b16f4a64e80ee344b0399de8c205bd9b
  4--- /dev/null
  5+++ b/lib/async_prompt.zsh
  6@@ -0,0 +1,140 @@
  7+# The async code is taken from
  8+# https://github.com/zsh-users/zsh-autosuggestions/blob/master/src/async.zsh
  9+# https://github.com/woefe/git-prompt.zsh/blob/master/git-prompt.zsh
 10+
 11+zmodload zsh/system
 12+
 13+# For now, async prompt function handlers are set up like so:
 14+# First, define the async function handler and add the function name
 15+# to the _omz_async_functions array:
 16+#
 17+#  function _git_prompt_status_async {
 18+#    # Do some expensive operation that outputs to stdout
 19+#  }
 20+#  _omz_register_handler _git_prompt_status_async
 21+#
 22+# Then add a stub prompt function in `$PROMPT` or similar prompt variables,
 23+# which will show the output of "$_OMZ_ASYNC_OUTPUT[handler_name]":
 24+#
 25+#  function git_prompt_status {
 26+#    echo -n $_OMZ_ASYNC_OUTPUT[_git_prompt_status]
 27+#  }
 28+#
 29+#  RPROMPT='$(git_prompt_status)'
 30+#
 31+# This API is subject to change and optimization. Rely on it at your own risk.
 32+
 33+function _omz_register_handler {
 34+  setopt localoptions noksharrays
 35+  typeset -ga _omz_async_functions
 36+  # we want to do nothing if there's no $1 function or we already set it up
 37+  if [[ -z "$1" ]] || (( ! ${+functions[$1]} )) \
 38+    || (( ${_omz_async_functions[(Ie)$1]} )); then
 39+    return
 40+  fi
 41+  _omz_async_functions+=("$1")
 42+  # let's add the hook to async_request if it's not there yet
 43+  if (( ! ${precmd_functions[(Ie)_omz_async_request]} )) \
 44+    && (( ${+functions[_omz_async_request]})); then
 45+    autoload -Uz add-zsh-hook
 46+    add-zsh-hook precmd _omz_async_request
 47+  fi
 48+}
 49+
 50+# Set up async handlers and callbacks
 51+function _omz_async_request {
 52+  typeset -gA _OMZ_ASYNC_FDS _OMZ_ASYNC_PIDS _OMZ_ASYNC_OUTPUT
 53+
 54+  # executor runs a subshell for all async requests based on key
 55+  local handler
 56+  for handler in ${_omz_async_functions}; do
 57+    (( ${+functions[$handler]} )) || continue
 58+
 59+    local fd=${_OMZ_ASYNC_FDS[$handler]:--1}
 60+    local pid=${_OMZ_ASYNC_PIDS[$handler]:--1}
 61+
 62+    # If we've got a pending request, cancel it
 63+    if (( fd != -1 && pid != -1 )) && { true <&$fd } 2>/dev/null; then
 64+      # Close the file descriptor and remove the handler
 65+      exec {fd}<&-
 66+      zle -F $fd
 67+
 68+      # Zsh will make a new process group for the child process only if job
 69+      # control is enabled (MONITOR option)
 70+      if [[ -o MONITOR ]]; then
 71+        # Send the signal to the process group to kill any processes that may
 72+        # have been forked by the async function handler
 73+        kill -TERM -$pid 2>/dev/null
 74+      else
 75+        # Kill just the child process since it wasn't placed in a new process
 76+        # group. If the async function handler forked any child processes they may
 77+        # be orphaned and left behind.
 78+        kill -TERM $pid 2>/dev/null
 79+      fi
 80+    fi
 81+
 82+    # Define global variables to store the file descriptor, PID and output
 83+    _OMZ_ASYNC_FDS[$handler]=-1
 84+    _OMZ_ASYNC_PIDS[$handler]=-1
 85+
 86+    # Fork a process to fetch the git status and open a pipe to read from it
 87+    exec {fd}< <(
 88+      # Tell parent process our PID
 89+      builtin echo ${sysparams[pid]}
 90+      # Store handler name for callback
 91+      builtin echo $handler
 92+      # Run the async function handler
 93+      $handler
 94+    )
 95+
 96+    # Save FD for handler
 97+    _OMZ_ASYNC_FDS[$handler]=$fd
 98+
 99+    # There's a weird bug here where ^C stops working unless we force a fork
100+    # See https://github.com/zsh-users/zsh-autosuggestions/issues/364
101+    command true
102+
103+    # Save the PID from the handler child process
104+    read pid <&$fd
105+    _OMZ_ASYNC_PIDS[$handler]=$pid
106diff --git a/lib/git.zsh b/lib/git.zsh
107index f049f73c2029ee21018a4f2b9d31e49f2a09b27f..6f48234580f6b2bb88ff552670ff581ae0fc657b 100644
108--- a/lib/git.zsh
109+++ b/lib/git.zsh
110@@ -9,14 +9,18 @@ function __git_prompt_git() {
111   GIT_OPTIONAL_LOCKS=0 command git "$@"
112 }
113 
114-function git_prompt_info() {
115+function _omz_git_prompt_status() {
116   # If we are on a folder not tracked by git, get out.
117   # Otherwise, check for hide-info at global and local repository level
118   if ! __git_prompt_git rev-parse --git-dir &> /dev/null \
119-     || [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
120+    || [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
121     return 0
122   fi
123 
124+  # Get either:
125+  # - the current branch name
126+  # - the tag name if we are on a tag
127+  # - the short SHA of the current commit
128   local ref
129   ref=$(__git_prompt_git symbolic-ref --short HEAD 2> /dev/null) \
130   || ref=$(__git_prompt_git describe --tags --exact-match HEAD 2> /dev/null) \
131@@ -33,6 +37,20 @@ function git_prompt_info() {
132   echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref:gs/%/%%}${upstream:gs/%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
133 }
134 
135+# Enable async prompt by default unless the setting is at false / no
136+if zstyle -T ':omz:alpha:lib:git' async-prompt; then
137+  function git_prompt_info() {
138+    _omz_register_handler _omz_git_prompt_status
139+    if [[ -n "$_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]" ]]; then
140+      echo -n "$_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]"
141+    fi
142+  }
143+else
144+  function git_prompt_info() {
145+    _omz_git_prompt_status
146+  }
147+fi
148+
149 # Checks if working tree is dirty
150 function parse_git_dirty() {
151   local STATUS