changeset 20631:d6827bd31d1d v8.2.0869

patch 8.2.0869: it is not possible to customize the quickfix window contents Commit: https://github.com/vim/vim/commit/858ba06d5f577b187da0367b231f7fa9461cb32d Author: Bram Moolenaar <Bram@vim.org> Date: Sun May 31 23:11:59 2020 +0200 patch 8.2.0869: it is not possible to customize the quickfix window contents Problem: It is not possible to customize the quickfix window contents. Solution: Add 'quickfixtextfunc'. (Yegappan Lakshmanan, closes https://github.com/vim/vim/issues/5465)
author Bram Moolenaar <Bram@vim.org>
date Sun, 31 May 2020 23:15:03 +0200
parents b7bfe0a2b961
children f8940db0dd14
files runtime/doc/eval.txt runtime/doc/options.txt runtime/doc/quickfix.txt src/option.h src/optiondefs.h src/quickfix.c src/testdir/test_quickfix.vim src/version.c
diffstat 8 files changed, 377 insertions(+), 81 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -5508,8 +5508,9 @@ getqflist([{what}])					*getqflist()*
 			id	get information for the quickfix list with
 				|quickfix-ID|; zero means the id for the
 				current list or the list specified by "nr"
-			idx	index of the current entry in the quickfix
-				list specified by 'id' or 'nr'.
+			idx	get information for the quickfix entry at this
+				index in the list specified by 'id' or 'nr'.
+				If set to zero, then uses the current entry.
 				See |quickfix-index|
 			items	quickfix list entries
 			lines	parse a list of lines using 'efm' and return
@@ -5545,7 +5546,7 @@ getqflist([{what}])					*getqflist()*
 				If not present, set to "".
 			id	quickfix list ID |quickfix-ID|. If not
 				present, set to 0.
-			idx	index of the current entry in the list. If not
+			idx	index of the quickfix entry in the list. If not
 				present, set to 0.
 			items	quickfix list entries. If not present, set to
 				an empty list.
@@ -8841,6 +8842,11 @@ setqflist({list} [, {action} [, {what}]]
 		    nr		list number in the quickfix stack; zero
 				means the current quickfix list and "$" means
 				the last quickfix list.
+		    quickfixtextfunc
+				function to get the text to display in the
+				quickfix window.  Refer to
+				|quickfix-window-function| for an explanation
+				of how to write the function and an example.
 		    title	quickfix list title text. See |quickfix-title|
 		Unsupported keys in {what} are ignored.
 		If the "nr" item is not present, then the current quickfix list
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -5901,6 +5901,21 @@ A jump table for the options with a shor
 	This option cannot be set from a |modeline| or in the |sandbox|, for
 	security reasons.
 
+						*'quickfixtextfunc'* *'qftf'*
+'quickfixtextfunc' 'qftf'	string (default "")
+			global
+			{only available when compiled with the |+quickfix|
+			feature}
+	This option specifies a function to be used to get the text to display
+	in the quickfix and location list windows.  This can be used to
+	customize the information displayed in the quickfix or location window
+	for each entry in the corresponding quickfix or location list.  See
+	|quickfix-window-function| for an explanation of how to write the
+	function and an example.
+
+	This option cannot be set from a |modeline| or in the |sandbox|, for
+	security reasons.
+
 						*'quoteescape'* *'qe'*
 'quoteescape' 'qe'	string	(default "\")
 			local to buffer
--- a/runtime/doc/quickfix.txt
+++ b/runtime/doc/quickfix.txt
@@ -15,6 +15,7 @@ 6. Selecting a compiler			|compiler-sele
 7. The error format			|error-file-format|
 8. The directory stack			|quickfix-directory-stack|
 9. Specific error file formats		|errorformats|
+10. Customizing the quickfix window	|quickfix-window-function|
 
 The quickfix commands are not available when the |+quickfix| feature was
 disabled at compile time.
@@ -1921,6 +1922,59 @@ error messages into a format that quickf
 start of the file about how to use it.  (This script is deprecated, see
 |compiler-perl|.)
 
+=============================================================================
+10. Customizing the quickfix window		*quickfix-window-function*
 
+The default format for the lines displayed in the quickfix window and location
+list window is:
+
+    <filename>|<lnum> col <col>|<text>
+
+The values displayed in each line correspond to the "bufnr", "lnum", "col" and
+"text" fields returned by the |getqflist()| function.
+
+For some quickfix/location lists, the displayed text need to be customized.
+For example, if only the filename is present for a quickfix entry, then the
+two "|" field separator characters after the filename are not needed.  Another
+use case is to customize the path displayed for a filename. By default, the
+complete path (which may be too long) is displayed for files which are not
+under the current directory tree. The file path may need to be simplified to a
+common parent directory.
+
+The displayed text can be customized by setting the 'quickfixtextfunc' option
+to a Vim function.  This function will be called with a dict argument for
+every entry in a quickfix or a location list. The dict argument will have the
+following fields:
+
+    quickfix	set to 1 when called for a quickfix list and 0 when called for
+		a location list.
+    id		quickfix or location list identifier
+    idx		index of the entry in the quickfix or location list
+
+The function should return a single line of text to display in the quickfix
+window for the entry identified by idx. The function can obtain information
+about the current entry using the |getqflist()| function and specifying the
+quickfix list identifier "id" and the entry index "idx".
+
+If a quickfix or location list specific customization is needed, then the
+'quickfixtextfunc' attribute of the list can be set using the |setqflist()| or
+|setloclist()| function. This overrides the global 'quickfixtextfunc' option.
+
+The example below displays the list of old files (|v:oldfiles|) in a quickfix
+window. As there is no line, column number and error text information
+associated with each entry, the 'quickfixtextfunc' function returns only the
+filename.
+Example: >
+    " create a quickfix list from v:oldfiles
+    call setqflist([], ' ', {'lines' : v:oldfiles, 'efm' : '%f',
+					\ 'quickfixtextfunc' : 'QfOldFiles'})
+    func QfOldFiles(info)
+	" get information about the specific quickfix entry
+	let e = getqflist({'id' : a:info.id, 'idx' : a:info.idx,
+						\ 'items' : 1}).items[0]
+	" return the simplified file name
+	return fnamemodify(bufname(e.bufnr), ':p:.')
+    endfunc
+<
 
  vim:tw=78:ts=8:noet:ft=help:norl:
--- a/src/option.h
+++ b/src/option.h
@@ -820,6 +820,7 @@ EXTERN int	p_ru;		// 'ruler'
 EXTERN char_u	*p_ruf;		// 'rulerformat'
 #endif
 EXTERN char_u	*p_pp;		// 'packpath'
+EXTERN char_u	*p_qftf;	// 'quickfixtextfunc'
 EXTERN char_u	*p_rtp;		// 'runtimepath'
 EXTERN long	p_sj;		// 'scrolljump'
 #if defined(MSWIN) && defined(FEAT_GUI)
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -2045,6 +2045,15 @@ static struct vimoption options[] =
 #endif
 			    {(char_u *)DEFAULT_PYTHON_VER, (char_u *)0L}
 			    SCTX_INIT},
+    {"quickfixtextfunc", "qftf", P_STRING|P_ALLOCED|P_VI_DEF|P_VIM|P_SECURE,
+#if defined(FEAT_QUICKFIX) && defined(FEAT_EVAL)
+			    (char_u *)&p_qftf, PV_NONE,
+			    {(char_u *)"", (char_u *)0L}
+#else
+			    (char_u *)NULL, PV_NONE,
+			    {(char_u *)NULL, (char_u *)NULL}
+#endif
+			    SCTX_INIT},
     {"quoteescape", "qe",   P_STRING|P_ALLOCED|P_VI_DEF,
 #ifdef FEAT_TEXTOBJ
 			    (char_u *)&p_qe, PV_QE,
--- a/src/quickfix.c
+++ b/src/quickfix.c
@@ -82,6 +82,7 @@ typedef struct qf_list_S
     char_u	*qf_title;	// title derived from the command that created
 				// the error list or set by setqflist
     typval_T	*qf_ctx;	// context set by setqflist/setloclist
+    char_u	*qf_qftf;	// 'quickfixtextfunc' setting for this list
 
     struct dir_stack_T	*qf_dir_stack;
     char_u		*qf_directory;
@@ -2277,6 +2278,10 @@ copy_loclist(qf_list_T *from_qfl, qf_lis
     }
     else
 	to_qfl->qf_ctx = NULL;
+    if (from_qfl->qf_qftf != NULL)
+	to_qfl->qf_qftf = vim_strsave(from_qfl->qf_qftf);
+    else
+	to_qfl->qf_qftf = NULL;
 
     if (from_qfl->qf_count)
 	if (copy_loclist_entries(from_qfl, to_qfl) == FAIL)
@@ -3812,6 +3817,7 @@ qf_free(qf_list_T *qfl)
     VIM_CLEAR(qfl->qf_title);
     free_tv(qfl->qf_ctx);
     qfl->qf_ctx = NULL;
+    VIM_CLEAR(qfl->qf_qftf);
     qfl->qf_id = 0;
     qfl->qf_changedtick = 0L;
 }
@@ -4399,74 +4405,115 @@ qf_update_buffer(qf_info_T *qi, qfline_T
  * Add an error line to the quickfix buffer.
  */
     static int
-qf_buf_add_line(buf_T *buf, linenr_T lnum, qfline_T *qfp, char_u *dirname)
+qf_buf_add_line(
+	qf_list_T	*qfl,		// quickfix list
+	buf_T		*buf,		// quickfix window buffer
+	linenr_T	lnum,
+	qfline_T	*qfp,
+	char_u		*dirname)
 {
     int		len;
     buf_T	*errbuf;
-
-    if (qfp->qf_module != NULL)
-    {
-	vim_strncpy(IObuff, qfp->qf_module, IOSIZE - 1);
-	len = (int)STRLEN(IObuff);
-    }
-    else if (qfp->qf_fnum != 0
-	    && (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
-	    && errbuf->b_fname != NULL)
-    {
-	if (qfp->qf_type == 1)	// :helpgrep
-	    vim_strncpy(IObuff, gettail(errbuf->b_fname), IOSIZE - 1);
-	else
-	{
-	    // shorten the file name if not done already
-	    if (errbuf->b_sfname == NULL
-		    || mch_isFullName(errbuf->b_sfname))
-	    {
-		if (*dirname == NUL)
-		    mch_dirname(dirname, MAXPATHL);
-		shorten_buf_fname(errbuf, dirname, FALSE);
-	    }
-	    vim_strncpy(IObuff, errbuf->b_fname, IOSIZE - 1);
-	}
-	len = (int)STRLEN(IObuff);
+    char_u	*qftf;
+
+    // If 'quickfixtextfunc' is set, then use the user-supplied function to get
+    // the text to display
+    qftf = p_qftf;
+    // Use the local value of 'quickfixtextfunc' if it is set.
+    if (qfl->qf_qftf != NULL)
+	qftf = qfl->qf_qftf;
+    if (qftf != NULL && *qftf != NUL)
+    {
+	char_u		*qfbuf_text;
+	typval_T	args[1];
+	dict_T		*d;
+
+	// create 'info' dict argument
+	if ((d = dict_alloc_lock(VAR_FIXED)) == NULL)
+	    return FAIL;
+	dict_add_number(d, "quickfix", (long)IS_QF_LIST(qfl));
+	dict_add_number(d, "id", (long)qfl->qf_id);
+	dict_add_number(d, "idx", (long)(lnum + 1));
+	++d->dv_refcount;
+	args[0].v_type = VAR_DICT;
+	args[0].vval.v_dict = d;
+
+	qfbuf_text = call_func_retstr(qftf, 1, args);
+	--d->dv_refcount;
+
+	if (qfbuf_text == NULL)
+	    return FAIL;
+
+	vim_strncpy(IObuff, qfbuf_text, IOSIZE - 1);
+	vim_free(qfbuf_text);
     }
     else
-	len = 0;
-
-    if (len < IOSIZE - 1)
-	IObuff[len++] = '|';
-
-    if (qfp->qf_lnum > 0)
-    {
-	vim_snprintf((char *)IObuff + len, IOSIZE - len, "%ld", qfp->qf_lnum);
-	len += (int)STRLEN(IObuff + len);
-
-	if (qfp->qf_col > 0)
+    {
+	if (qfp->qf_module != NULL)
+	{
+	    vim_strncpy(IObuff, qfp->qf_module, IOSIZE - 1);
+	    len = (int)STRLEN(IObuff);
+	}
+	else if (qfp->qf_fnum != 0
+		&& (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
+		&& errbuf->b_fname != NULL)
 	{
-	    vim_snprintf((char *)IObuff + len, IOSIZE - len,
-						       " col %d", qfp->qf_col);
+	    if (qfp->qf_type == 1)	// :helpgrep
+		vim_strncpy(IObuff, gettail(errbuf->b_fname), IOSIZE - 1);
+	    else
+	    {
+		// shorten the file name if not done already
+		if (errbuf->b_sfname == NULL
+			|| mch_isFullName(errbuf->b_sfname))
+		{
+		    if (*dirname == NUL)
+			mch_dirname(dirname, MAXPATHL);
+		    shorten_buf_fname(errbuf, dirname, FALSE);
+		}
+		vim_strncpy(IObuff, errbuf->b_fname, IOSIZE - 1);
+	    }
+	    len = (int)STRLEN(IObuff);
+	}
+	else
+	    len = 0;
+
+	if (len < IOSIZE - 1)
+	    IObuff[len++] = '|';
+
+	if (qfp->qf_lnum > 0)
+	{
+	    vim_snprintf((char *)IObuff + len, IOSIZE - len, "%ld",
+		    qfp->qf_lnum);
+	    len += (int)STRLEN(IObuff + len);
+
+	    if (qfp->qf_col > 0)
+	    {
+		vim_snprintf((char *)IObuff + len, IOSIZE - len,
+			" col %d", qfp->qf_col);
+		len += (int)STRLEN(IObuff + len);
+	    }
+
+	    vim_snprintf((char *)IObuff + len, IOSIZE - len, "%s",
+		    (char *)qf_types(qfp->qf_type, qfp->qf_nr));
 	    len += (int)STRLEN(IObuff + len);
 	}
-
-	vim_snprintf((char *)IObuff + len, IOSIZE - len, "%s",
-		(char *)qf_types(qfp->qf_type, qfp->qf_nr));
-	len += (int)STRLEN(IObuff + len);
-    }
-    else if (qfp->qf_pattern != NULL)
-    {
-	qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
-	len += (int)STRLEN(IObuff + len);
-    }
-    if (len < IOSIZE - 2)
-    {
-	IObuff[len++] = '|';
-	IObuff[len++] = ' ';
-    }
-
-    // Remove newlines and leading whitespace from the text.
-    // For an unrecognized line keep the indent, the compiler may
-    // mark a word with ^^^^.
-    qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
-	    IObuff + len, IOSIZE - len);
+	else if (qfp->qf_pattern != NULL)
+	{
+	    qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
+	    len += (int)STRLEN(IObuff + len);
+	}
+	if (len < IOSIZE - 2)
+	{
+	    IObuff[len++] = '|';
+	    IObuff[len++] = ' ';
+	}
+
+	// Remove newlines and leading whitespace from the text.
+	// For an unrecognized line keep the indent, the compiler may
+	// mark a word with ^^^^.
+	qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
+		IObuff + len, IOSIZE - len);
+    }
 
     if (ml_append_buf(buf, lnum, IObuff,
 		(colnr_T)STRLEN(IObuff) + 1, FALSE) == FAIL)
@@ -4522,7 +4569,7 @@ qf_fill_buffer(qf_list_T *qfl, buf_T *bu
 	}
 	while (lnum < qfl->qf_count)
 	{
-	    if (qf_buf_add_line(buf, lnum, qfp, dirname) == FAIL)
+	    if (qf_buf_add_line(qfl, buf, lnum, qfp, dirname) == FAIL)
 		break;
 
 	    ++lnum;
@@ -6369,9 +6416,16 @@ get_qfline_items(qfline_T *qfp, list_T *
 /*
  * Add each quickfix error to list "list" as a dictionary.
  * If qf_idx is -1, use the current list. Otherwise, use the specified list.
+ * If eidx is not 0, then return only the specified entry. Otherwise return
+ * all the entries.
  */
     static int
-get_errorlist(qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
+get_errorlist(
+	qf_info_T	*qi_arg,
+	win_T		*wp,
+	int		qf_idx,
+	int		eidx,
+	list_T		*list)
 {
     qf_info_T	*qi = qi_arg;
     qf_list_T	*qfl;
@@ -6389,6 +6443,9 @@ get_errorlist(qf_info_T *qi_arg, win_T *
 	}
     }
 
+    if (eidx < 0)
+	return OK;
+
     if (qf_idx == INVALID_QFIDX)
 	qf_idx = qi->qf_curlist;
 
@@ -6401,7 +6458,12 @@ get_errorlist(qf_info_T *qi_arg, win_T *
 
     FOR_ALL_QFL_ITEMS(qfl, qfp, i)
     {
-	if (get_qfline_items(qfp, list) == FAIL)
+	if (eidx > 0)
+	{
+	    if (eidx == i)
+		return get_qfline_items(qfp, list);
+	}
+	else if (get_qfline_items(qfp, list) == FAIL)
 	    return FAIL;
     }
 
@@ -6461,7 +6523,7 @@ qf_get_list_from_lines(dict_T *what, dic
 	    if (qf_init_ext(qi, 0, NULL, NULL, &di->di_tv, errorformat,
 			TRUE, (linenr_T)0, (linenr_T)0, NULL, NULL) > 0)
 	    {
-		(void)get_errorlist(qi, NULL, 0, l);
+		(void)get_errorlist(qi, NULL, 0, 0, l);
 		qf_free(&qi->qf_lists[0]);
 	    }
 	    free(qi);
@@ -6679,16 +6741,17 @@ qf_getprop_filewinid(win_T *wp, qf_info_
 }
 
 /*
- * Return the quickfix list items/entries as 'items' in retdict
+ * Return the quickfix list items/entries as 'items' in retdict.
+ * If eidx is not 0, then return the item at the specified index.
  */
     static int
-qf_getprop_items(qf_info_T *qi, int qf_idx, dict_T *retdict)
+qf_getprop_items(qf_info_T *qi, int qf_idx, int eidx, dict_T *retdict)
 {
     int		status = OK;
     list_T	*l = list_alloc();
     if (l != NULL)
     {
-	(void)get_errorlist(qi, NULL, qf_idx, l);
+	(void)get_errorlist(qi, NULL, qf_idx, eidx, l);
 	dict_add_list(retdict, "items", l);
     }
     else
@@ -6726,16 +6789,20 @@ qf_getprop_ctx(qf_list_T *qfl, dict_T *r
 }
 
 /*
- * Return the current quickfix list index as 'idx' in retdict
+ * Return the current quickfix list index as 'idx' in retdict.
+ * If a specific entry index (eidx) is supplied, then use that.
  */
     static int
-qf_getprop_idx(qf_list_T *qfl, dict_T *retdict)
-{
-    int curidx = qfl->qf_index;
-    if (qf_list_empty(qfl))
-	// For empty lists, current index is set to 0
-	curidx = 0;
-    return dict_add_number(retdict, "idx", curidx);
+qf_getprop_idx(qf_list_T *qfl, int eidx, dict_T *retdict)
+{
+    if (eidx == 0)
+    {
+	eidx = qfl->qf_index;
+	if (qf_list_empty(qfl))
+	    // For empty lists, current index is set to 0
+	    eidx = 0;
+    }
+    return dict_add_number(retdict, "idx", eidx);
 }
 
 /*
@@ -6750,6 +6817,7 @@ qf_get_properties(win_T *wp, dict_T *wha
     qf_list_T	*qfl;
     int		status = OK;
     int		qf_idx = INVALID_QFIDX;
+    int		eidx = 0;
     dictitem_T	*di;
     int		flags = QF_GETLIST_NONE;
 
@@ -6770,6 +6838,14 @@ qf_get_properties(win_T *wp, dict_T *wha
 
     qfl = qf_get_list(qi, qf_idx);
 
+    // If an entry index is specified, use that
+    if ((di = dict_find(what, (char_u *)"idx", -1)) != NULL)
+    {
+	if (di->di_tv.v_type != VAR_NUMBER)
+	    return FAIL;
+	eidx = di->di_tv.vval.v_number;
+    }
+
     if (flags & QF_GETLIST_TITLE)
 	status = qf_getprop_title(qfl, retdict);
     if ((status == OK) && (flags & QF_GETLIST_NR))
@@ -6777,13 +6853,13 @@ qf_get_properties(win_T *wp, dict_T *wha
     if ((status == OK) && (flags & QF_GETLIST_WINID))
 	status = dict_add_number(retdict, "winid", qf_winid(qi));
     if ((status == OK) && (flags & QF_GETLIST_ITEMS))
-	status = qf_getprop_items(qi, qf_idx, retdict);
+	status = qf_getprop_items(qi, qf_idx, eidx, retdict);
     if ((status == OK) && (flags & QF_GETLIST_CONTEXT))
 	status = qf_getprop_ctx(qfl, retdict);
     if ((status == OK) && (flags & QF_GETLIST_ID))
 	status = dict_add_number(retdict, "id", qfl->qf_id);
     if ((status == OK) && (flags & QF_GETLIST_IDX))
-	status = qf_getprop_idx(qfl, retdict);
+	status = qf_getprop_idx(qfl, eidx, retdict);
     if ((status == OK) && (flags & QF_GETLIST_SIZE))
 	status = dict_add_number(retdict, "size", qfl->qf_count);
     if ((status == OK) && (flags & QF_GETLIST_TICK))
@@ -7148,6 +7224,20 @@ qf_setprop_curidx(qf_info_T *qi, qf_list
 }
 
 /*
+ * Set the current index in the specified quickfix list
+ */
+    static int
+qf_setprop_qftf(qf_info_T *qi UNUSED, qf_list_T *qfl, dictitem_T *di)
+{
+    VIM_CLEAR(qfl->qf_qftf);
+    if (di->di_tv.v_type == VAR_STRING
+	    && di->di_tv.vval.v_string != NULL)
+	qfl->qf_qftf = vim_strsave(di->di_tv.vval.v_string);
+
+    return OK;
+}
+
+/*
  * Set quickfix/location list properties (title, items, context).
  * Also used to add items from parsing a list of lines.
  * Used by the setqflist() and setloclist() Vim script functions.
@@ -7186,6 +7276,8 @@ qf_set_properties(qf_info_T *qi, dict_T 
 	retval = qf_setprop_context(qfl, di);
     if ((di = dict_find(what, (char_u *)"idx", -1)) != NULL)
 	retval = qf_setprop_curidx(qi, qfl, di);
+    if ((di = dict_find(what, (char_u *)"quickfixtextfunc", -1)) != NULL)
+	retval = qf_setprop_qftf(qi, qfl, di);
 
     if (retval == OK)
 	qf_list_changed(qfl);
@@ -7900,7 +7992,7 @@ get_qf_loc_list(int is_qf, win_T *wp, ty
     {
 	if (rettv_list_alloc(rettv) == OK)
 	    if (is_qf || wp != NULL)
-		(void)get_errorlist(NULL, wp, -1, rettv->vval.v_list);
+		(void)get_errorlist(NULL, wp, -1, 0, rettv->vval.v_list);
     }
     else
     {
--- a/src/testdir/test_quickfix.vim
+++ b/src/testdir/test_quickfix.vim
@@ -4766,4 +4766,121 @@ func Test_cquit()
   call assert_fails('-3cquit', 'E16:')
 endfunc
 
+" Test for getting a specific item from a quickfix list
+func Xtest_getqflist_by_idx(cchar)
+  call s:setup_commands(a:cchar)
+  " Empty list
+  call assert_equal([], g:Xgetlist({'idx' : 1, 'items' : 0}).items)
+  Xexpr ['F1:10:L10', 'F1:20:L20']
+  let l = g:Xgetlist({'idx' : 2, 'items' : 0}).items
+  call assert_equal(bufnr('F1'), l[0].bufnr)
+  call assert_equal(20, l[0].lnum)
+  call assert_equal('L20', l[0].text)
+  call assert_equal([], g:Xgetlist({'idx' : -1, 'items' : 0}).items)
+  call assert_equal([], g:Xgetlist({'idx' : 3, 'items' : 0}).items)
+  %bwipe!
+endfunc
+
+func Test_getqflist_by_idx()
+  call Xtest_getqflist_by_idx('c')
+  call Xtest_getqflist_by_idx('l')
+endfunc
+
+" Test for the 'quickfixtextfunc' setting
+func Tqfexpr(info)
+  if a:info.quickfix
+    let qfl = getqflist({'id' : a:info.id, 'idx' : a:info.idx,
+          \ 'items' : 1}).items
+  else
+    let qfl = getloclist(0, {'id' : a:info.id, 'idx' : a:info.idx,
+          \ 'items' : 1}).items
+  endif
+
+  let e = qfl[0]
+  let s = ''
+  if e.bufnr != 0
+    let bname = bufname(e.bufnr)
+    let s ..= fnamemodify(bname, ':.')
+  endif
+  let s ..= '-'
+  let s ..= 'L' .. string(e.lnum) .. 'C' .. string(e.col) .. '-'
+  let s ..= e.text
+
+  return s
+endfunc
+
+func Xtest_qftextfunc(cchar)
+  call s:setup_commands(a:cchar)
+
+  set efm=%f:%l:%c:%m
+  set quickfixtextfunc=Tqfexpr
+  Xexpr ['F1:10:2:green', 'F1:20:4:blue']
+  Xwindow
+  call assert_equal('F1-L10C2-green', getline(1))
+  call assert_equal('F1-L20C4-blue', getline(2))
+  Xclose
+  set quickfixtextfunc&vim
+  Xwindow
+  call assert_equal('F1|10 col 2| green', getline(1))
+  call assert_equal('F1|20 col 4| blue', getline(2))
+  Xclose
+  set efm&
+  set quickfixtextfunc&
+
+  " Test for per list 'quickfixtextfunc' setting
+  func PerQfText(info)
+    if a:info.quickfix
+      let qfl = getqflist({'id' : a:info.id, 'idx' : a:info.idx,
+            \ 'items' : 1}).items
+    else
+      let qfl = getloclist(0, {'id' : a:info.id, 'idx' : a:info.idx,
+            \ 'items' : 1}).items
+    endif
+    if empty(qfl)
+      return ''
+    endif
+    return 'Line ' .. qfl[0].lnum .. ', Col ' .. qfl[0].col
+  endfunc
+  set quickfixtextfunc=Tqfexpr
+  call g:Xsetlist([], ' ', {'quickfixtextfunc' : "PerQfText"})
+  Xaddexpr ['F1:10:2:green', 'F1:20:4:blue']
+  Xwindow
+  call assert_equal('Line 10, Col 2', getline(1))
+  call assert_equal('Line 20, Col 4', getline(2))
+  Xclose
+  call g:Xsetlist([], 'r', {'quickfixtextfunc' : ''})
+  set quickfixtextfunc&
+  delfunc PerQfText
+
+  " Non-existing function
+  set quickfixtextfunc=Tabc
+  call assert_fails("Xexpr ['F1:10:2:green', 'F1:20:4:blue']", 'E117:')
+  call assert_fails("Xwindow", 'E117:')
+  Xclose
+  set quickfixtextfunc&
+
+  " set option to a non-function
+  set quickfixtextfunc=[10,\ 20]
+  call assert_fails("Xexpr ['F1:10:2:green', 'F1:20:4:blue']", 'E117:')
+  call assert_fails("Xwindow", 'E117:')
+  Xclose
+  set quickfixtextfunc&
+
+  " set option to a function with different set of arguments
+  func Xqftext(a, b, c)
+    return a:a .. a:b .. a:c
+  endfunc
+  set quickfixtextfunc=Xqftext
+  call assert_fails("Xexpr ['F1:10:2:green', 'F1:20:4:blue']", 'E119:')
+  call assert_fails("Xwindow", 'E119:')
+  Xclose
+  set quickfixtextfunc&
+  delfunc Xqftext
+endfunc
+
+func Test_qftextfunc()
+  call Xtest_qftextfunc('c')
+  call Xtest_qftextfunc('l')
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/version.c
+++ b/src/version.c
@@ -747,6 +747,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    869,
+/**/
     868,
 /**/
     867,