Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: [BUG] With CORRECT_ALL, an interrupted correct puts a truncated entry in history



On Sun, Dec 17, 2023 at 5:54 AM Vincent Lefevre <vincent@xxxxxxxxxx> wrote:
>
> On 2023-12-09 13:44:42 -0800, Bart Schaefer wrote:
> > Ah.  It's in the buffer managed in ZLE as $BUFFER, but corrections
> > apply left to right as that buffer is converted into a parse tree,
> > they don't alter the buffer itself.
>
> But if one does a first correction, for the second proposed
> correction, one types 'e' to edit, one gets the buffer with
> the first correction applied.

When you type 'e', correction (and alias expansion!) is temporarily
disabled, the parser is allowed to run to the end, then the parsed
result is reassembled and pushed onto the editor stack (like "print
-z") and zle restarts.

Watch what happens if you type 'e' for a correction at the PS2 prompt.
The parser hasn't reached the end yet, so you get back a PS2 with
empty input, and that will keep happening until a full valid statement
is entered or you interrupt, and then you get back the entire input so
far.

This is another reason 'a' doesn't throw away the history -- you could
lose many lines of typing above the point of canceling the correction.

> > > Or just use 'e', then immediately put the command in the history
> > > without running it. Is there a zle widget for that? I would find
> > > this useful even when there are no spelling corrections.
> >
> > Correction is not itself a ZLE action -- it happens after ZLE has
> > returned control to the parser, and works even if ZLE is disabled.  So
> > there's no widget to fiddle with it.
>
> But what matters to that after 'e', one is in ZLE.

You asked if there was a widget to immediately put the command in the
history after 'e'.  There's not a widget to do all of that, because
there's not a widget triggered on 'e'.

That said ... attached is an actual ZLE implementation of correctall.
I'm not entirely happy with it yet -- problems are:
 * the tricks using "zstyle -e" are rather fragile
 * _path_files alters the leading path and then offers only to correct
the file name at the end ...
 * ... which makes it a pain to prompt correctly for the 'n' action
 * quoted words muddy things.
Features of interest:
 * uses completion, so corrections are context-sensitive rather than
simple file globs
 * offers a menu for multiple corrections (which muddies what 'y'
should do, unfortunately)
 * 'u' (undo) backs out if you TAB too often at that menu
 * 'a' does in fact discard the current line from the history, but at
PS2 it keeps what went before
 * '!' skips all remaining corrections and runs the command

The patch includes a fix for an error I kept encountering in
_approximate ... I think this but happens when the "fake" style is
used, but haven't run it to ground other than working around it.

I will not be pushing this in its current state, use at your own risk
(which would be true even if it were pushed, I suppose).
diff --git a/Completion/Base/Completer/_approximate b/Completion/Base/Completer/_approximate
index 96860b5a7..3e19621d2 100644
--- a/Completion/Base/Completer/_approximate
+++ b/Completion/Base/Completer/_approximate
@@ -63,7 +63,7 @@ compadd() {
     PREFIX="(#a${_comp_correct})$PREFIX"
   fi
 
-  (( $_correct_group && ${${argv[1,(r)-(|-)]}[(I)-*[JV]]} )) &&
+  (( ${_correct_group:-0} && ${${argv[1,(r)-(|-)]}[(I)-*[JV]]} )) &&
       _correct_expl[_correct_group]=${argv[1,(r)-(-|)][(R)-*[JV]]}
 
   compadd@_approximate "$_correct_expl[@]" "$@"
diff --git a/Functions/Zle/correct-all-words b/Functions/Zle/correct-all-words
new file mode 100644
index 000000000..bef262472
--- /dev/null
+++ b/Functions/Zle/correct-all-words
@@ -0,0 +1,161 @@
+#autoload
+# Intended to be called during accept-line or in zle-line-finish, but can
+# be called by any widget to apply correction to all words in $BUFFER or
+# used as a widget itself
+
+# Compare modify-current-argument
+
+setopt localoptions noksharrays multibyte norecexact
+zmodload zsh/complist || return 1
+
+local -a reply cmdline
+local key REPLY REPLY2 MENUSELECT
+integer pos posword poschar
+unset MENUSELECT	# Bug with no-select plus yes=2 below
+
+local curcontext="${curcontext:-}"
+local widget="correct-${${WIDGET/correct-all-words/}:-all-words}"
+if [[ -z "$curcontext" ]]; then
+  curcontext="${widget}:::"
+else
+  curcontext="${widget}:${curcontext#*:}"
+fi
+local mycontext="${curcontext}"
+
+# This breaks out of read-from-minibuffer if only the original matches
+local shown_original
+zstyle -e ":completion:${widget}:*" original \
+       'if (( compstate[nmatches] == 0 )); \
+       then reply=(false);
+       elif [[ ${compstate[unambiguous]} = ${key} ]]; \
+       then shown_original=unambiguous; zle -U n; reply=(false); \
+       elif [[ -z ${shown_original} ]];
+       then shown_original=original; reply=(true);
+       else reply=(false); fi'
+
+# Would be nice to make these conditional, but it's hard to test for
+# these specifically if a more general context has the style defined
+zstyle ":completion:${widget}:*" menu no-select yes=2
+zstyle ":completion:${widget}:*" group-name ''
+zstyle ":completion:${widget}:*" group-order original corrections
+zstyle ":completion:${widget}:*" show-ambiguity true
+zstyle ":completion:${widget}:*" show-completer true
+zstyle ":completion:${widget}:*" accept-exact false
+
+# Keep completion functions out of the results
+zstyle ":completion:${widget}:*" ignored-patterns '_*'
+
+# This shows earliest the words with the fewest necessary corrections
+zstyle ":completion:${widget}:*" sort false
+
+# Overload fake description to produce a prompt for multiple corrections
+zstyle -e ":completion:${widget}:*:original" fake \
+	 'if (( compstate[nmatches] > 1 )) && \
+	     [[ -z ${shown_original} ]] ; \
+	 then bindkey -M correctall y _correct_word; \
+	 shown_original=fake-original; \
+	 reply=("${key//:/\\:}:TAB to choose, ENTER to accept, n to skip"); \
+	 fi'
+zstyle -e ":completion:${widget}:*:corrections" fake \
+	 'if [[ ${shown_original} = wanted ]] || \
+	     ( (( compstate[nmatches] > 0 )) && \
+	       [[ -z ${shown_original} ]] ); \
+	 then bindkey -M correctall y _correct_word; \
+	 shown_original=fake-corrections; \
+	 reply+=("${key//:/\\:}:TAB to choose, ENTER to accept, n to skip"); \
+	 fi'
+
+# There's no good semantics for 'y' when there are multiple possible
+# corrections.  Left as $'\t\n' it'll skip ahead after a correction is
+# chosen from the list.  Changed to .accept-line it becomes equivalent
+# to 'n' if no choice has been made yet.  The above makes it cycle the
+# menu, but maybe it would be better just to have it do nothing?
+
+autoload -Uz split-shell-arguments read-from-minibuffer mkshadow
+
+split-shell-arguments
+[[ ${#reply} -lt 2 ]] && return 1
+(( posword = REPLY, poschar = REPLY2 ))
+cmdline=("${reply[@]}")		# In case something else uses $reply ...
+
+bindkey -N correctall
+bindkey -M correctall $'\t' _correct_word
+bindkey -M correctall ' ' _correct_word
+for key in n a e \! $'\n' $'\r'
+do
+  bindkey -M correctall $key .accept-line
+done
+bindkey -M correctall u .undo
+bindkey -M correctall '^_' .undo
+bindkey -M correctall -s '^G' e
+bindkey -M correctall -s '^U' a		# Should copy from main XXX
+bindkey -M correctall -s y $'\t\n'
+
+# Work around a bug with recursive-edit from zle-line-finish
+[[ $WIDGET = zle-line-finish ]] && zle recursive-edit	# Fails
+
+local lmini rmini
+{
+  mkshadow -s all-words _original_file _correct_word
+
+  # Force unedited original to precede _path_files additions.
+  # Otherwise the "original" style above handles this.
+  function _original_file {
+    [[ ${key} = */* ]] && shown_original=wanted
+    return 1	# Force call to _correct
+  }
+
+  # This is to avoid having _correct_word stomp on $curcontext,
+  # plus break out of read-from-minibuffer when nothing matches
+  function _correct_word {
+    local shown_original ret REPLY
+    _main_complete _original_file _correct
+    ret=$?
+    [[ ${compstate[nmatches]} -eq 0 ]] && zle -U n
+    return ret
+  }
+
+  # "Real" words from reply[2], reply[4], etc., see split-shell-arguments
+  for pos in {2..${#cmdline}..2}
+  do
+    # Don't try to correct numbers and non-syntax punctuation
+    [[ ${cmdline[pos]} = *[A-Za-z]* ]] || continue
+    
+    {
+      key=${cmdline[pos]}
+      curcontext="${mycontext}"
+
+      # Can't use -K KEYMAP because read-from-minibuffer always resets
+      bindkey -A main llatcerroc
+      bindkey -A correctall main
+
+      # lmini="${PREBUFFER}${(j::)cmdline[1,pos-1]}"	# Too much?
+      lmini=${(j::)cmdline[1,pos-1]}
+      rmini=${(j::)cmdline[pos+1,-1]}
+
+      zle -U $'\t'	# Start minibuffer in completion
+      read-from-minibuffer "Correct $key [nyae!]: " "${lmini}${key}" "${rmini}"
+      REPLY=${${REPLY#${lmini}}%${rmini}}
+    } always { bindkey -A llatcerroc main }
+    case ${KEYS} in
+      (a) zle send-break; break;;
+      (n) continue;;
+      (y|$'\n'|$'\r') cmdline[pos]=${REPLY:-${cmdline[pos]}};;
+      (e) [[ ${WIDGET} = zle-line-finish ]] && {
+	    print -z "${(j::)cmdline}"
+	    cmdline=()
+	    (( posword = 0, poschar = 0 ))
+	  }
+	  ;&
+      (*) break;;
+    esac
+  done
+} always {
+  rmshadow
+  zle -R -c
+}
+
+BUFFER="${(j::)cmdline}"
+CURSOR=$(( ${#${(j::)cmdline[1,posword-1]}} + poschar ))
+
+[[ "${KEYS}" = \! ]] && zle .accept-line


Messages sorted by: Reverse Date, Date, Thread, Author