patch 9.1.0785: cannot preserve error position when setting quickfix list

Commit: 
https://github.com/vim/vim/commit/27fbf6e5e8bee5c6b61819a5e82a0b50b265f0b0
Author: Jeremy Fleischman <jeremyfleisch...@gmail.com>
Date:   Mon Oct 14 20:46:27 2024 +0200

    patch 9.1.0785: cannot preserve error position when setting quickfix list
    
    Problem:  cannot preserve error position when setting quickfix lists
    Solution: Add the 'u' action for setqflist()/setloclist() and try
              to keep the closes target position (Jeremy Fleischman)
    
    fixes: #15839
    closes: #15841
    
    Signed-off-by: Jeremy Fleischman <jeremyfleisch...@gmail.com>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 15de27db5..32167d67f 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt*  For Vim version 9.1.  Last change: 2024 Oct 12
+*builtin.txt*  For Vim version 9.1.  Last change: 2024 Oct 14
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -9766,6 +9766,8 @@ setqflist({list} [, {action} [, {what}]])         
*setqflist()*
                        clear the list: >
                                :call setqflist([], 'r')
 <
+               'u'     Like 'r', but tries to preserve the current selection
+                       in the quickfix list.
                'f'     All the quickfix lists in the quickfix stack are
                        freed.
 
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index a9434335c..400ccd74a 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt*  For Vim version 9.1.  Last change: 2024 Oct 08
+*version9.txt*  For Vim version 9.1.  Last change: 2024 Oct 14
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -41598,6 +41598,8 @@ Changed~
 - the regex engines match correctly case-insensitive multi-byte characters
   (and apply proper case folding)
 - |:keeppatterns| preserves the last substitute pattern when used with |:s|
+- |setqflist()| and |setloclist()| can optionally try to preserve the current
+  selection in the quickfix list with the "u" action.
 
                                                        *added-9.2*
 Added ~
diff --git a/src/quickfix.c b/src/quickfix.c
index 79c9fdf39..1008dd5ab 100644
--- a/src/quickfix.c
+++ b/src/quickfix.c
@@ -190,6 +190,7 @@ static buf_T        *load_dummy_buffer(char_u *fname, 
char_u *dirname_start, char_u *re
 static void    wipe_dummy_buffer(buf_T *buf, char_u *dirname_start);
 static void    unload_dummy_buffer(buf_T *buf, char_u *dirname_start);
 static qf_info_T *ll_get_or_alloc_list(win_T *);
+static int     entry_is_closer_to_target(qfline_T *entry, qfline_T 
*other_entry, int target_fnum, int target_lnum, int target_col);
 
 // Quickfix window check helper macro
 #define IS_QF_WINDOW(wp) (bt_quickfix((wp)->w_buffer) && (wp)->w_llist_ref == 
NULL)
@@ -7499,6 +7500,62 @@ qf_add_entry_from_dict(
     return status;
 }
 
+/*
+ * Check if `entry` is closer to the target than `other_entry`.
+ *
+ * Only returns TRUE if `entry` is definitively closer. If it's further
+ * away, or there's not enough information to tell, return FALSE.
+ */
+    static int
+entry_is_closer_to_target(
+       qfline_T        *entry,
+       qfline_T        *other_entry,
+       int             target_fnum,
+       int             target_lnum,
+       int             target_col)
+{
+    // First, compare entries to target file.
+    if (!target_fnum)
+       // Without a target file, we can't know which is closer.
+       return FALSE;
+
+    int is_target_file = entry->qf_fnum && entry->qf_fnum == target_fnum;
+    int other_is_target_file = other_entry->qf_fnum && other_entry->qf_fnum == 
target_fnum;
+    if (!is_target_file && other_is_target_file)
+       return FALSE;
+    else if (is_target_file && !other_is_target_file)
+       return TRUE;
+
+    // Both entries are pointing at the exact same file. Now compare line
+    // numbers.
+    if (!target_lnum)
+       // Without a target line number, we can't know which is closer.
+       return FALSE;
+
+    int line_distance = entry->qf_lnum ? labs(entry->qf_lnum - target_lnum) : 
INT_MAX;
+    int other_line_distance = other_entry->qf_lnum ? labs(other_entry->qf_lnum 
- target_lnum) : INT_MAX;
+    if (line_distance > other_line_distance)
+       return FALSE;
+    else if (line_distance < other_line_distance)
+       return TRUE;
+
+    // Both entries are pointing at the exact same line number (or no line
+    // number at all). Now compare columns.
+    if (!target_col)
+       // Without a target column, we can't know which is closer.
+       return FALSE;
+
+    int column_distance = entry->qf_col ? abs(entry->qf_col - target_col) : 
INT_MAX;
+    int other_column_distance = other_entry->qf_col ? abs(other_entry->qf_col 
- target_col): INT_MAX;
+    if (column_distance > other_column_distance)
+       return FALSE;
+    else if (column_distance < other_column_distance)
+       return TRUE;
+
+    // It's a complete tie! The exact same file, line, and column.
+    return FALSE;
+}
+
 /*
  * Add list of entries to quickfix/location list. Each list entry is
  * a dictionary with item information.
@@ -7518,21 +7575,54 @@ qf_add_entries(
     int                retval = OK;
     int                valid_entry = FALSE;
 
+    // If there's an entry selected in the quickfix list, remember its location
+    // (file, line, column), so we can select the nearest entry in the updated
+    // quickfix list.
+    int prev_fnum = 0;
+    int prev_lnum = 0;
+    int prev_col = 0;
+    if (qfl->qf_ptr)
+    {
+       prev_fnum = qfl->qf_ptr->qf_fnum;
+       prev_lnum = qfl->qf_ptr->qf_lnum;
+       prev_col = qfl->qf_ptr->qf_col;
+    }
+
+    int select_first_entry = FALSE;
+    int select_nearest_entry = FALSE;
+
     if (action == ' ' || qf_idx == qi->qf_listcount)
     {
+       select_first_entry = TRUE;
        // make place for a new list
        qf_new_list(qi, title);
        qf_idx = qi->qf_curlist;
        qfl = qf_get_list(qi, qf_idx);
     }
-    else if (action == 'a' && !qf_list_empty(qfl))
-       // Adding to existing list, use last entry.
-       old_last = qfl->qf_last;
+    else if (action == 'a')
+    {
+       if (qf_list_empty(qfl))
+           // Appending to empty list, select first entry.
+           select_first_entry = TRUE;
+       else
+           // Adding to existing list, use last entry.
+           old_last = qfl->qf_last;
+    }
     else if (action == 'r')
     {
+       select_first_entry = TRUE;
        qf_free_items(qfl);
        qf_store_title(qfl, title);
     }
+    else if (action == 'u')
+    {
+       select_nearest_entry = TRUE;
+       qf_free_items(qfl);
+       qf_store_title(qfl, title);
+    }
+
+    qfline_T *entry_to_select = NULL;
+    int entry_to_select_index = 0;
 
     FOR_ALL_LIST_ITEMS(list, li)
     {
@@ -7547,6 +7637,18 @@ qf_add_entries(
                                                                &valid_entry);
        if (retval == QF_FAIL)
            break;
+
+       qfline_T *entry = qfl->qf_last;
+       if (
+           (select_first_entry && entry_to_select == NULL) ||
+           (select_nearest_entry &&
+               (entry_to_select == NULL ||
+                entry_is_closer_to_target(entry, entry_to_select, prev_fnum,
+                                          prev_lnum, prev_col))))
+       {
+           entry_to_select = entry;
+           entry_to_select_index = qfl->qf_count;
+        }
     }
 
     // Check if any valid error entries are added to the list.
@@ -7556,14 +7658,12 @@ qf_add_entries(
        // no valid entry
        qfl->qf_nonevalid = TRUE;
 
-    // If not appending to the list, set the current error to the first entry
-    if (action != 'a')
-       qfl->qf_ptr = qfl->qf_start;
-
-    // Update the current error index if not appending to the list or if the
-    // list was empty before and it is not empty now.
-    if ((action != 'a' || qfl->qf_index == 0) && !qf_list_empty(qfl))
-       qfl->qf_index = 1;
+    // Set the current error.
+    if (entry_to_select)
+    {
+       qfl->qf_ptr = entry_to_select;
+       qfl->qf_index = entry_to_select_index;
+    }
 
     // Don't update the cursor in quickfix window when appending entries
     qf_update_buffer(qi, old_last);
@@ -7700,7 +7800,7 @@ qf_setprop_items_from_lines(
     if (di->di_tv.v_type != VAR_LIST || di->di_tv.vval.v_list == NULL)
        return FAIL;
 
-    if (action == 'r')
+    if (action == 'r' || action == 'u')
        qf_free_items(&qi->qf_lists[qf_idx]);
     if (qf_init_ext(qi, qf_idx, NULL, NULL, &di->di_tv, errorformat,
                FALSE, (linenr_T)0, (linenr_T)0, NULL, NULL) >= 0)
@@ -7897,8 +7997,8 @@ qf_free_stack(win_T *wp, qf_info_T *qi)
 /*
  * Populate the quickfix list with the items supplied in the list
  * of dictionaries. "title" will be copied to w:quickfix_title.
- * "action" is 'a' for add, 'r' for replace.  Otherwise create a new list.
- * When "what" is not NULL then only set some properties.
+ * "action" is 'a' for add, 'r' for replace, 'u' for update.  Otherwise
+ * create a new list. When "what" is not NULL then only set some properties.
  */
     int
 set_errorlist(
@@ -8740,7 +8840,7 @@ set_qf_ll_list(
            act = tv_get_string_chk(action_arg);
            if (act == NULL)
                return;         // type error; errmsg already given
-           if ((*act == 'a' || *act == 'r' || *act == ' ' || *act == 'f') &&
+           if ((*act == 'a' || *act == 'r' || *act == 'u' || *act == ' ' || 
*act == 'f') &&
                    act[1] == NUL)
                action = *act;
            else
diff --git a/src/testdir/test_quickfix.vim b/src/testdir/test_quickfix.vim
index 7a00135b8..c611ce88d 100644
--- a/src/testdir/test_quickfix.vim
+++ b/src/testdir/test_quickfix.vim
@@ -6462,6 +6462,97 @@ func Test_quickfix_buffer_contents()
   call setqflist([], 'f')
 endfunc
 
+func Test_quickfix_update()
+  " Setup: populate a couple buffers
+  new
+  call setline(1, range(1, 5))
+  let b1 = bufnr()
+  new
+  call setline(1, range(1, 3))
+  let b2 = bufnr()
+  " Setup: set a quickfix list.
+  let items = [{'bufnr': b1, 'lnum': 1}, {'bufnr': b1, 'lnum': 2}, {'bufnr': 
b2, 'lnum': 1}, {'bufnr': b2, 'lnum': 2}]
+  call setqflist(items)
+
+  " Open the quickfix list, select the third entry.
+  copen
+  exe "normal jj\<CR>"
+  call assert_equal(3, getqflist({'idx' : 0}).idx)
+
+  " Update the quickfix list. Make sure the third entry is still selected.
+  call setqflist([], 'u', { 'items': items })
+  call assert_equal(3, getqflist({'idx' : 0}).idx)
+
+  " Update the quickfix list again, but this time with missing line number
+  " information. Confirm that we keep the current buffer selected.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2}], 'u')
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Cleanup the buffers we allocated during this test.
+  %bwipe!
+  %bwipe!
+endfunc
+
+func Test_quickfix_update_with_missing_coordinate_info()
+  new
+  call setline(1, range(1, 5))
+  let b1 = bufnr()
+
+  new
+  call setline(1, range(1, 3))
+  let b2 = bufnr()
+
+  new
+  call setline(1, range(1, 2))
+  let b3 = bufnr()
+
+  " Setup: set a quickfix list with no coordinate information at all.
+  call setqflist([{}, {}])
+
+  " Open the quickfix list, select the second entry.
+  copen
+  exe "normal j\<CR>"
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Update the quickfix list. As the previously selected entry has no
+  " coordinate information, we expect the first entry to now be selected.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2}, {'bufnr': b3}], 'u')
+  call assert_equal(1, getqflist({'idx' : 0}).idx)
+
+  " Select the second entry in the quickfix list.
+  copen
+  exe "normal j\<CR>"
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Update the quickfix list again. The currently selected entry does not have
+  " a line number, but we should keep the file selected.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2, 'lnum': 3}, {'bufnr': b3}], 'u')
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Update the quickfix list again. The currently selected entry (bufnr=b2, 
lnum=3)
+  " is no longer present. We should pick the nearest entry.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2, 'lnum': 1}, {'bufnr': b2, 
'lnum': 4}], 'u')
+  call assert_equal(3, getqflist({'idx' : 0}).idx)
+
+  " Set the quickfix list again, with a specific column number. The currently 
selected entry doesn't have a
+  " column number, but they share a line number.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2, 'lnum': 4, 'col': 5}, {'bufnr': 
b2, 'lnum': 4, 'col': 6}], 'u')
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Set the quickfix list again. The currently selected column number (6) is
+  " no longer present. We should select the nearest column number.
+  call setqflist([{'bufnr': b1}, {'bufnr': b2, 'lnum': 4, 'col': 2}, {'bufnr': 
b2, 'lnum': 4, 'col': 4}], 'u')
+  call assert_equal(3, getqflist({'idx' : 0}).idx)
+
+  " Now set the quickfix list, but without columns. We should still pick the
+  " same line.
+  call setqflist([{'bufnr': b2, 'lnum': 3}, {'bufnr': b2, 'lnum': 4}, 
{'bufnr': b2, 'lnum': 4}], 'u')
+  call assert_equal(2, getqflist({'idx' : 0}).idx)
+
+  " Cleanup the buffers we allocated during this test.
+  %bwipe!
+endfunc
+
 " Test for "%b" in "errorformat"
 func Test_efm_format_b()
   call setqflist([], 'f')
diff --git a/src/version.c b/src/version.c
index ec23e70cd..7345bd5b8 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    785,
 /**/
     784,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/vim_dev/E1t0QIV-00H0II-Ju%40256bit.org.

Raspunde prin e-mail lui