PATCH: User-defined completion listing

Peter Stephenson (pws@ifh.de)
Wed, 19 Nov 1997 18:00:55 +0100

Message-Id: <199711191700.SAA08643@hydra.ifh.de>
To: zsh-workers@math.gatech.edu (Zsh hackers list), zsh@peak.org
Subject: PATCH: User-defined completion listing
Date: Wed, 19 Nov 1997 18:00:55 +0100
From: Peter Stephenson <pws@ifh.de>

This implements something like the outcome of the recent discussion of
a new option for showing a user-defined list instead of the actual
completions. First, the bad news: this patch is simply against my own
current adaption of 3.1.2, which probably doesn't correspond to anyone
else's. In particular, it has my compctl -/ and -W patches in, but it
doesn't have Sven's big patch, which may make a difference. I could
probably do it against vanilla 3.1.2 if that becomes sensible.

Now the good news: the syntax (see manual patch for more) is either

compctl ... -Y '$array'

or

compctl ... -Y 'func'

In the first case, $array is, err, an array, in the second, func is
called and must set $reply, just like a -K function. (They're
evaluated at different times, so there's no clash.) func is also
passed the complete list of matches, with full prefix expansion, so it
can process the matches to produce a display. For example,

llfn() { reply=("$(ls -fld $*)"); }
compctl -f -Y llfn foo

foo has ordinary file name completion, but the list of matches will be
made line by line in ls -l format. read -c and read -l are available
if you want them.

The output is literal in this case, since you have control over what
comes out, though only newline is specially handled; other control
sequences could mess up the formatting and are best avoided (at least
with always_last_prompt set). Note, however, that all the other
formatting capabilities are retained, so a -Y function like

func() { reply=($*); }

produces full columnated output just as without the -Y func (assuming
there are no funny characters in the $* which would be massaged in
that case).

So the suggestion for job completion is along the lines of:

joblist() {
local jf=/tmp/zjobs$$ line
jobs >& $jf
reply=() joblist=()
while read line; do
joblist=($joblist $line)
reply=($reply ${${line#\[}%%\]*})
done < $jf
# The above allows the output to appear in columns. If you want the
# exact output of 'jobs', you can instead use:
# joblist=("$(<$jf)")
# It must still be an array, though.
rm -f $jf
}
compctl -P% -K joblist -Y '$joblist' \
-x 's[-]' -k signals -- kill # ... and friends

Note that in the current implementation you need a completion list: if
there are no completions, nothing is displayed, so you can't just
display a memo and let the user complete by hand. That could probably
be remedied. Obviously, '$joblist' could be a static variable as well.

One thing to check for with the patch is that I haven't somehow gummed
up the logic for displaying other types of completion in my zeal to
get -Y displays to appear OK; I obviously couldn't check everything
directly myself. (I think I caught the only memory leak, though.)
Also, a -Y func is blindly passed the fully expanded completion
possibilities (necessary for the llfn example above, which needs the
full path to the candidate file): maybe there are occasions where
that's wrong.

Another thing under the `could perhaps be better, but it's time I did
some work' heading: literal arrays are allowed as with -k, i.e. -Y
'(option1 option2 ...)', but there's no way of getting a literal
string there. It might be nice to enhance get_user_var() to understand
a double quote at the beginning and return the text as an array with a
single element. (The name of a real array with just one element is
fine, of course.)

While I'm getting picky: the fact that the completion function gets
the full path could be something of a memory hog if you are completing
in a full directory with a long path name, but until the Commodore 64
version of zsh appears...

*** Doc/Zsh/compctl.yo.YoY Thu Sep 18 09:58:39 1997
--- Doc/Zsh/compctl.yo Wed Nov 19 17:34:51 1997
***************
*** 103,111 ****
[ tt(-s) var(subststring) ])
list([ tt(-K) var(function) ] [ tt(-H) var(num pattern) ])
list([ tt(-Q) ] [ tt(-P) var(prefix) ] [ tt(-S) var(suffix) ])
! liat([ tt(-W) var(file-prefix) ]
list([ tt(-q) ] [ tt(-X) var(explanation) ])
! list([ tt(-l) var(cmd) ] [ tt(-U) ])
endlist()

The remaining var(options) specify the type of command arguments
--- 103,111 ----
[ tt(-s) var(subststring) ])
list([ tt(-K) var(function) ] [ tt(-H) var(num pattern) ])
list([ tt(-Q) ] [ tt(-P) var(prefix) ] [ tt(-S) var(suffix) ])
! list([ tt(-W) var(file-prefix) ])
list([ tt(-q) ] [ tt(-X) var(explanation) ])
! list([ tt(-Y) var(func-or-var) ] [ tt(-l) var(cmd) ] [ tt(-U) ])
endlist()

The remaining var(options) specify the type of command arguments
***************
*** 360,365 ****
--- 360,385 ----
options. A `tt(%n)' in this string is replaced by the number of matches.
The explanation only appears if completion was tried and there was
no unique match.
+ )
+ item(tt(-Y) var(func-or-var))(
+ The list provided by var(func-or-var) is displayed instead of the list
+ of completions whenever a listing is required; the actual completions
+ to be inserted are not affected. It can be provided in two
+ ways. Firstly, if var(func-or-var) begins with a tt($) it defines an
+ array variable, or if it begins with a left parenthesis a literal
+ array, which contains the list. A variable may have been set by a
+ call to a function using the tt(-K) option. Otherwise it contains the
+ name of a function which will be executed to create the list. The
+ function will be passed as an argument list all matching completions,
+ including prefixes and suffixes expanded in full, and should set the
+ array var(reply) to the result. In both cases, the display list will
+ only be retrieved after a complete list of matches has been created.
+
+ Note that the returned list does not have to correspond, even in
+ length, to the original set of matches; the use of an array is purely
+ a convenience for formatting. No special formatting of characters is
+ performed on the output in this case; in particular, newlines are
+ printed literally and if they appear output in columns is suppressed.
)
enditem()
texinode(Alternative Completion)(Extended Completion)(Option Flags)(Programmable Completion)
*** Src/Zle/comp.h.YoY Thu Sep 18 09:58:39 1997
--- Src/Zle/comp.h Wed Nov 19 11:45:01 1997
***************
*** 102,107 ****
--- 102,108 ----
char *str; /* for -s (expansion) */
char *func; /* for -K (function) */
char *explain; /* for -X (explanation) */
+ char *ylist; /* for -Y (user-defined desc. for listing) */
char *prefix, *suffix; /* for -P and -S (prefix, suffix) */
char *subcmd; /* for -l (command name to use) */
char *withd; /* for -w (with directory */
*** Src/Zle/comp1.c.YoY Thu Sep 18 09:58:40 1997
--- Src/Zle/comp1.c Wed Nov 19 12:18:44 1997
***************
*** 80,85 ****
--- 80,86 ----
zsfree(cc->str);
zsfree(cc->func);
zsfree(cc->explain);
+ zsfree(cc->ylist);
zsfree(cc->prefix);
zsfree(cc->suffix);
zsfree(cc->hpat);
*** Src/Zle/compctl.c.YoY Thu Sep 18 09:59:17 1997
--- Src/Zle/compctl.c Wed Nov 19 12:18:41 1997
***************
*** 217,222 ****
--- 217,234 ----
*argv = "" - 1;
}
break;
+ case 'Y':
+ if ((*argv)[1]) {
+ cct.ylist = (*argv) + 1;
+ *argv = "" - 1;
+ } else if (!argv[1]) {
+ zwarnnam(name, "function/variable expect after -%c",
+ NULL, **argv);
+ } else {
+ cct.ylist = *++argv;
+ *argv = "" - 1;
+ }
+ break;
case 'P':
if ((*argv)[1]) {
cct.prefix = (*argv) + 1;
***************
*** 687,693 ****
Compctl cc;

if (cct->subcmd && (cct->keyvar || cct->glob || cct->str ||
! cct->func || cct->explain || cct->prefix)) {
zwarnnam(name, "illegal combination of options", NULL, 0);
return 1;
}
--- 699,706 ----
Compctl cc;

if (cct->subcmd && (cct->keyvar || cct->glob || cct->str ||
! cct->func || cct->explain || cct->ylist ||
! cct->prefix)) {
zwarnnam(name, "illegal combination of options", NULL, 0);
return 1;
}
***************
*** 725,730 ****
--- 738,744 ----
zsfree(cc->str);
zsfree(cc->func);
zsfree(cc->explain);
+ zsfree(cc->ylist);
zsfree(cc->prefix);
zsfree(cc->suffix);
zsfree(cc->subcmd);
***************
*** 740,745 ****
--- 754,760 ----
cc->str = ztrdup(cct->str);
cc->func = ztrdup(cct->func);
cc->explain = ztrdup(cct->explain);
+ cc->ylist = ztrdup(cct->ylist);
cc->prefix = ztrdup(cct->prefix);
cc->suffix = ztrdup(cct->suffix);
cc->subcmd = ztrdup(cct->subcmd);
***************
*** 857,862 ****
--- 872,878 ----
printif(cc->keyvar, 'k');
printif(cc->func, 'K');
printif(cc->explain, 'X');
+ printif(cc->ylist, 'Y');
printif(cc->prefix, 'P');
printif(cc->suffix, 'S');
printif(cc->glob, 'g');
*** Src/Zle/zle_tricky.c.YoY Thu Sep 18 10:01:00 1997
--- Src/Zle/zle_tricky.c Wed Nov 19 17:46:32 1997
***************
*** 123,128 ****
--- 123,133 ----

static int nmatches;

+ /* A list of user-defined explanations for the completions to be shown *
+ * instead of amatches when listing completions. */
+
+ static char **aylist;
+
/* !=0 if we have a valid completion list. */

static int validlist;
***************
*** 2996,3003 ****
ccsuffix = cc->suffix;

validlist = 1;
! if ((nmatches || expl) && !errflag)
return 0;

if ((isf || cc->xor) && !parampre) {
/* We found no matches, but there is a xor'ed completion: *
--- 3001,3045 ----
ccsuffix = cc->suffix;

validlist = 1;
! if ((nmatches || expl) && !errflag) {
! /* generating the user-defined explanations must happen last: *
! * if anything fails, we silently allow the normal completion *
! * list to be used. */
! if (cc->ylist) {
! char **yaptr, *uv = NULL;
! List list;
!
! if (cc->ylist[0] == '$' || cc->ylist[0] == '(') {
! /* from variable: must be an array */
! uv = cc->ylist + (cc->ylist[0] == '$');
! } else if ((list = getshfunc(cc->ylist)) != &dummy_list) {
! /* from function: pass completions as arg list */
! LinkList args = newlinklist();
! int addlen = strlen(rpre) + strlen(rsuf) + 1;
!
! addlinknode(args, cc->ylist);
! for (yaptr = amatches; *yaptr; yaptr++) {
! /* can't use tricat(). rats. */
! char *ptr = (char *)halloc(addlen + strlen(*yaptr));
! sprintf(ptr, "%s%s%s", rpre, *yaptr, rsuf);
! addlinknode(args, ptr);
! }
!
! /* No harm in allowing read -l and -c here, too */
! incompctlfunc = 1;
! doshfunc(list, args, 0, 1);
! incompctlfunc = 0;
! uv = "reply";
! } else
! return 0;
! if (uv && (yaptr = get_user_var(uv))) {
! PERMALLOC {
! aylist = arrdup(yaptr);
! } LASTALLOC;
! }
! }
return 0;
+ }

if ((isf || cc->xor) && !parampre) {
/* We found no matches, but there is a xor'ed completion: *
***************
*** 3045,3050 ****
--- 3087,3095 ----
listmatches();
if(validlist) {
freearray(amatches);
+ if (aylist)
+ freearray(aylist);
+ aylist = 0;
zsfree(rpre);
zsfree(rsuf);
zsfree(lpre);
***************
*** 3516,3526 ****
listmatches(void)
{
int longest = 1, fct, fw, colsz, t0, t1, ct, up, cl, xup = 0;
! int off, boff, nboff;
! int of = (isset(LISTTYPES) && !(haswhat & HAS_MISC));
char **arr, **ap, sav;
int nfpl, nfsl, nlpl, nlsl;
! int listmax = getiparam("LISTMAX");

#ifdef DEBUG
/* Sanity check */
--- 3561,3572 ----
listmatches(void)
{
int longest = 1, fct, fw, colsz, t0, t1, ct, up, cl, xup = 0;
! int off = 0, boff = 0, nboff = 0;
! int of = (!aylist && isset(LISTTYPES) && !(haswhat & HAS_MISC));
char **arr, **ap, sav;
int nfpl, nfsl, nlpl, nlsl;
! int listmax = getiparam("LISTMAX"), litnl = 0;
! size_t (*strlenfn) _((char const *));

#ifdef DEBUG
/* Sanity check */
***************
*** 3538,3549 ****

/* Calculate the lengths of the prefixes/suffixes we have to ignore
during printing. */
! off = ispattern && ppre && *ppre &&
! !(haswhat & (HAS_MISC | HAS_PATHPAT)) ? strlen(ppre) : 0;
! boff = ispattern && psuf && *psuf &&
! !(haswhat & (HAS_MISC | HAS_PATHPAT)) ? strlen(psuf) : 0;
! nboff = ispattern && psuf && *psuf &&
! !(haswhat & (HAS_MISC | HAS_PATHPAT)) ? niceztrlen(psuf) : 0;

/* When called from expandorcompleteprefix, we probably have to
remove a space now. */
--- 3584,3597 ----

/* Calculate the lengths of the prefixes/suffixes we have to ignore
during printing. */
! if (ispattern && !aylist && !(haswhat & (HAS_MISC | HAS_PATHPAT))) {
! if (ppre && *ppre)
! off = strlen(ppre);
! if (psuf && *psuf) {
! boff = strlen(psuf);
! nboff = niceztrlen(psuf);
! }
! }

/* When called from expandorcompleteprefix, we probably have to
remove a space now. */
***************
*** 3558,3595 ****

/* Set the cursor below the prompt. */
trashzle();
- ct = nmatches;
showinglist = 0;

clearflag = (isset(USEZLE) && !termflags &&
(isset(ALWAYSLASTPROMPT) && zmult == 1)) ||
(unset(ALWAYSLASTPROMPT) && zmult != 1);

! arr = amatches;

! /* Calculate the column width, the number of columns and the number
! of lines. */
! for (ap = arr; *ap; ap++)
! if ((cl = niceztrlen(*ap + off) - nboff +
! (ispattern ? 0 :
! (!(haswhat & HAS_MISC) ? nfpl + nfsl : nlpl + nlsl))) > longest)
! longest = cl;
! if (of)
! longest++;
!
! fw = longest + 2;
! fct = (columns + 1) / fw;
! if (fct == 0) {
! fct = 1;
! colsz = ct;
! up = colsz + nlnct - clearflag;
for (ap = arr; *ap; ap++)
! up += (niceztrlen(*ap + off) - nboff + of +
! (ispattern ? 0 :
! (!(haswhat & HAS_MISC) ? nfpl + nfsl : nlpl + nlsl))) / columns;
! } else {
! colsz = (ct + fct - 1) / fct;
! up = colsz + nlnct - clearflag + (ct == 0);
}

/* Print the explanation string, if any. */
--- 3606,3683 ----

/* Set the cursor below the prompt. */
trashzle();
showinglist = 0;

clearflag = (isset(USEZLE) && !termflags &&
(isset(ALWAYSLASTPROMPT) && zmult == 1)) ||
(unset(ALWAYSLASTPROMPT) && zmult != 1);

! /* just to keep gcc happy */
! fw = colsz = up = 0;
! if (aylist) {
! arr = aylist;
! /* If no literal newlines, the remaining code should use strlen() */
! strlenfn = (size_t (*) _((char const *)))strlen;
!
! /* The hard bit here is that we are handling newlines literally. *
! * In fact, we are in principle handling all characters literally, *
! * but it's quite enough work with just newlines. *
! * If there are such, we give up trying to print the list as *
! * columns and print as rows, counting the extra newlines. */
! ct = 0;
! for (ap = arr; *ap; ap++) {
! ct++;
! if (strchr(*ap, '\n'))
! litnl++;
! }
! if (litnl) {
! colsz = ct;
! up = colsz + nlnct - clearflag;
! /* Count real newlines, as well as overflowing lines. */
! for (ap = arr; *ap; ap++) {
! char *nlptr, *sptr = *ap;
! while (sptr && *sptr) {
! up += (nlptr = strchr(sptr, '\n'))
! ? 1 + (nlptr-sptr)/columns
! : strlen(sptr)/columns;
! sptr = nlptr ? nlptr+1 : NULL;
! }
! }
! }
! } else {
! arr = amatches;
! ct = nmatches;
! strlenfn = niceztrlen;
! }

!
! if (!litnl) {
! /* Calculate the column width, the number of columns and the
! number of lines. */
for (ap = arr; *ap; ap++)
! if ((cl = strlenfn(*ap + off) - nboff +
! ((ispattern || aylist) ? 0 :
! (!(haswhat & HAS_MISC) ?
! nfpl + nfsl : nlpl + nlsl))) > longest)
! longest = cl;
! if (of)
! longest++;
!
! fw = longest + 2;
! fct = (columns + 1) / fw;
! if (fct == 0) {
! fct = 1;
! colsz = ct;
! up = colsz + nlnct - clearflag;
! for (ap = arr; *ap; ap++)
! up += (strlenfn(*ap + off) - nboff + of +
! ((ispattern || aylist) ? 0 :
! (!(haswhat & HAS_MISC) ?
! nfpl + nfsl : nlpl + nlsl))) / columns;
! } else {
! colsz = (ct + fct - 1) / fct;
! up = colsz + nlnct - clearflag + (ct == 0);
! }
}

/* Print the explanation string, if any. */
***************
*** 3671,3677 ****
while (*ap) {
int t2;

! if (ispattern) {
int cut = strlen(*ap) - boff;

sav = ap[0][cut];
--- 3759,3768 ----
while (*ap) {
int t2;

! if (aylist) {
! zputs(*ap, shout);
! t2 = strlen(*ap);
! } else if (ispattern) {
int cut = strlen(*ap) - boff;

sav = ap[0][cut];

-- 
Peter Stephenson <pws@ifh.de>       Tel: +49 33762 77366
WWW:  http://www.ifh.de/~pws/       Fax: +49 33762 77413
Deutsches Elektronen-Synchrotron --- Institut fuer Hochenergiephysik Zeuthen
DESY-IfH, Platanenallee 6, 15738 Zeuthen, Germany.