From 98ded081dddc5c5070535b045116eb5f63a11d95 Mon Sep 17 00:00:00 2001 From: Jessica Clarke Date: Mon, 22 Jun 2026 17:58:04 +0000 Subject: [PATCH 01/33] patch 9.2.0700: configure: -lrt requirement for timer_create not detected Problem: configure does not actually check whether -lrt is needed for timer_create(); the test only compiles instead of linking, so the requirement is missed when cross-compiling Solution: Use AC_LINK_IFELSE instead of AC_COMPILE_IFELSE for the timer_create checks so the link actually decides whether -lrt is required (Jessica Clarke) AC_COMPILE_IFELSE won't try to link, so if the function exists in the system headers, we will always detect that -lrt is not needed, as the code will compile regardless of linker flags. If not cross-compiling, the following AC_RUN_IFELSE will end up trying to link, so if -lrt is needed it will fail to link and we will interpret timer_create as not working, rather than that we just need to link with -lrt. But when we are cross-compiling we will skip the AC_RUN_IFELSE and assume that it works, failing to link when we later build if -lrt is in fact needed. closes: #20605 related: 2cf145b78b88 ("patch 9.1.0837: cross-compiling has some issues") Signed-off-by: Jessica Clarke Signed-off-by: Christian Brabandt --- src/auto/configure | 10 ++++++---- src/configure.ac | 4 ++-- src/version.c | 2 ++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/auto/configure b/src/auto/configure index a20eea1667..f5d6dea01c 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -15078,7 +15078,7 @@ main (void) return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO" +if ac_fn_c_try_link "$LINENO" then : vim_cv_timer_create=yes else case e in #( @@ -15086,7 +15086,8 @@ else case e in #( ;; esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $vim_cv_timer_create" >&5 @@ -15117,7 +15118,7 @@ main (void) return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO" +if ac_fn_c_try_link "$LINENO" then : vim_cv_timer_create_with_lrt=yes else case e in #( @@ -15125,7 +15126,8 @@ else case e in #( ;; esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $vim_cv_timer_create_with_lrt" >&5 diff --git a/src/configure.ac b/src/configure.ac index 710cb317fc..b71e553394 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -4037,7 +4037,7 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM( dnl Check for timer_create. It probably requires the 'rt' library. AC_CACHE_CHECK([for timer_create without -lrt], [vim_cv_timer_create], [ - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + AC_LINK_IFELSE([AC_LANG_PROGRAM([ #include ], [ timer_create(CLOCK_MONOTONIC, NULL, NULL); @@ -4051,7 +4051,7 @@ if test "x$vim_cv_timer_create" = "xno" ; then save_LIBS="$LIBS" LIBS="$LIBS -lrt" AC_CACHE_CHECK([for timer_create with -lrt], [vim_cv_timer_create_with_lrt], [ - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + AC_LINK_IFELSE([AC_LANG_PROGRAM([ #include ], [ timer_create(CLOCK_MONOTONIC, NULL, NULL); diff --git a/src/version.c b/src/version.c index b93bdd9e1c..ddd8a02ca6 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 700, /**/ 699, /**/ From caab0767f99007b23c44127e543920a9bd9d2636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Marek?= Date: Mon, 22 Jun 2026 18:04:06 +0000 Subject: [PATCH 02/33] patch 9.2.0701: tests: test_terminal.vim does not wait for job to finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: tests: Test_terminal_eof_arg() and Test_terminal_duplicate_eof_arg() do not wait until the python job finishes Solution: Wait for the job to be dead before checking its exit value (Vladimír Marek). closes: #20571 Signed-off-by: Vladimír Marek Signed-off-by: Christian Brabandt --- src/testdir/test_terminal.vim | 8 ++++++-- src/version.c | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/testdir/test_terminal.vim b/src/testdir/test_terminal.vim index fced4d158b..98e82fcb4c 100644 --- a/src/testdir/test_terminal.vim +++ b/src/testdir/test_terminal.vim @@ -942,7 +942,9 @@ func Test_terminal_eof_arg() call WaitFor({-> getline('$') =~ 'hello'}) call assert_equal('hello', getline('$')) endif - let exitval = bufnr()->term_getjob()->job_info().exitval + let job = bufnr()->term_getjob() + call WaitForAssert({-> assert_equal('dead', job_status(job))}) + let exitval = job->job_info().exitval if !has('win32') call assert_equal(123, exitval) else @@ -984,7 +986,9 @@ func Test_terminal_duplicate_eof_arg() call WaitFor({-> getline('$') =~ 'hello'}) call assert_equal('hello', getline('$')) endif - let exitval = bufnr()->term_getjob()->job_info().exitval + let job = bufnr()->term_getjob() + call WaitForAssert({-> assert_equal('dead', job_status(job))}) + let exitval = job->job_info().exitval if !has('win32') call assert_equal(123, exitval) else diff --git a/src/version.c b/src/version.c index ddd8a02ca6..db18e0cc92 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 701, /**/ 700, /**/ From 8513982a5ed5a84ba8e4e532505b07b4fa1efbdb Mon Sep 17 00:00:00 2001 From: yilisharcs Date: Mon, 22 Jun 2026 18:54:44 +0000 Subject: [PATCH 03/33] runtime(fennel): add more ";" comment leaders to 'comments' closes: #20579 Signed-off-by: yilisharcs Signed-off-by: Christian Brabandt --- runtime/ftplugin/fennel.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/ftplugin/fennel.vim b/runtime/ftplugin/fennel.vim index 2a9623faff..054089d92d 100644 --- a/runtime/ftplugin/fennel.vim +++ b/runtime/ftplugin/fennel.vim @@ -10,7 +10,7 @@ endif let b:did_ftplugin = 1 setlocal commentstring=;\ %s -setlocal comments=:;;,:; +setlocal comments=:;;;;,:;;;,:;;,:; setlocal formatoptions-=t setlocal suffixesadd=.fnl setlocal lisp From 8c670b3a5116e247ad4e481083a9eb4a90b9a89e Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Mon, 22 Jun 2026 19:05:36 +0000 Subject: [PATCH 04/33] runtime(fennel): Update Last Update header forgotten from commit 8513982a5ed5a84ba8e4e532505b07b4fa1efbdb Signed-off-by: Christian Brabandt --- runtime/ftplugin/fennel.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/ftplugin/fennel.vim b/runtime/ftplugin/fennel.vim index 054089d92d..8f2f22506b 100644 --- a/runtime/ftplugin/fennel.vim +++ b/runtime/ftplugin/fennel.vim @@ -3,6 +3,7 @@ " Maintainer: Gregory Anders " Last Update: 2023 Jun 9 " 2024 May 24 by Riley Bruins ('commentstring') +" 2026 Jun 22 by yilisharcs, add all more lisp 'comments' #20579 if exists('b:did_ftplugin') finish From 5767d80b3794b17e5b61afec966fdeb7c0c4506f Mon Sep 17 00:00:00 2001 From: ShivaPriyanShanmuga Date: Mon, 22 Jun 2026 19:07:09 +0000 Subject: [PATCH 05/33] patch 9.2.0702: :windo and :tabdo create an extra window with 'winfixbuf' Problem: With 'winfixbuf' set in the current window, :windo and :tabdo create an extra split window, even though they only visit existing windows/tabpages and don't change the current window's buffer (Collin Kennedy) Solution: Skip the 'winfixbuf' escape in ex_listdo() for :windo and :tabdo (ShivaPriyanShanmuga) fixes: #14301 closes: #20600 Signed-off-by: ShivaPriyanShanmuga Signed-off-by: Christian Brabandt --- src/ex_cmds2.c | 6 ++++- src/testdir/test_winfixbuf.vim | 41 ++++++++++++++++++++++------------ src/version.c | 2 ++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/ex_cmds2.c b/src/ex_cmds2.c index afe9c4b58a..f867c3c9c9 100644 --- a/src/ex_cmds2.c +++ b/src/ex_cmds2.c @@ -478,7 +478,11 @@ ex_listdo(exarg_T *eap) buf_T *buf = curbuf; int next_fnum = 0; - if (curwin->w_p_wfb) + // ":windo" and ":tabdo" only visit existing windows/tabpages, they don't + // change the current window's buffer, so they can't escape a 'winfixbuf' + // window (which would create a split). + if (curwin->w_p_wfb && eap->cmdidx != CMD_windo && + eap->cmdidx != CMD_tabdo) { if ((eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) && !eap->forceit) diff --git a/src/testdir/test_winfixbuf.vim b/src/testdir/test_winfixbuf.vim index 898443d9b5..ba5cb979b7 100644 --- a/src/testdir/test_winfixbuf.vim +++ b/src/testdir/test_winfixbuf.vim @@ -2857,36 +2857,34 @@ func Test_tNext() set tags& endfunc -" Call :tabdo and choose the next available 'nowinfixbuf' window. -func Test_tabdo_choose_available_window() +" Call :tabdo and stay in the 'winfixbuf' window: it only visits tabpages and +" doesn't change the current buffer, so it must not switch to another window +" even when a 'nowinfixbuf' window is available. +func Test_tabdo_stay_in_winfixbuf_window() call s:reset_all_buffers() let [l:first, _] = s:make_args_list() - " Make a split window that is 'nowinfixbuf' but make it the second-to-last - " window so that :tabdo will first try the 'winfixbuf' window, pass over it, - " and prefer the other 'nowinfixbuf' window, instead. - " " +-------------------+ " | 'nowinfixbuf' | " +-------------------+ " | 'winfixbuf' | <-- Cursor is here " +-------------------+ split - let l:nowinfixbuf_window = win_getid() " Move to the 'winfixbuf' window now exe "normal \j" let l:winfixbuf_window = win_getid() let l:expected_windows = s:get_windows_count() tabdo echo '' - call assert_equal(l:nowinfixbuf_window, win_getid()) + call assert_equal(l:winfixbuf_window, win_getid()) call assert_equal(l:first, bufnr()) call assert_equal(l:expected_windows, s:get_windows_count()) endfunc -" Call :tabdo and create a new split window if all available windows are 'winfixbuf'. -func Test_tabdo_make_new_window() +" Call :tabdo and do not create a new window even when the only window is +" 'winfixbuf'. +func Test_tabdo_no_new_window() call s:reset_all_buffers() let [l:first, _] = s:make_buffers_list() @@ -2896,11 +2894,9 @@ func Test_tabdo_make_new_window() let l:current_windows = s:get_windows_count() tabdo echo '' - call assert_notequal(l:current, win_getid()) + call assert_equal(l:current, win_getid()) call assert_equal(l:first, bufnr()) - exe "normal \j" - call assert_equal(l:first, bufnr()) - call assert_equal(l:current_windows + 1, s:get_windows_count()) + call assert_equal(l:current_windows, s:get_windows_count()) endfunc " Fail :tag but :tag! is allowed @@ -3214,6 +3210,23 @@ func Test_windo() call assert_equal(l:current_window, win_getid()) endfunc +" Call :windo and do not create a new window even when the only window is +" 'winfixbuf'. +func Test_windo_no_new_window() + call s:reset_all_buffers() + + let [l:first, _] = s:make_buffers_list() + exe $"buffer! {l:first}" + + let l:current = win_getid() + let l:current_windows = s:get_windows_count() + + windo echo '' + call assert_equal(l:current, win_getid()) + call assert_equal(l:first, bufnr()) + call assert_equal(l:current_windows, s:get_windows_count()) +endfunc + " Fail :wnext but :wnext! is allowed func Test_wnext() call s:reset_all_buffers() diff --git a/src/version.c b/src/version.c index db18e0cc92..67a0fa6508 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 702, /**/ 701, /**/ From a82ed3a6f37e0dcc821e0c3c0d5764b227e40901 Mon Sep 17 00:00:00 2001 From: Miguel Barro Date: Mon, 22 Jun 2026 19:19:24 +0000 Subject: [PATCH 06/33] patch 9.2.0703: session file does not store relative Vim9 autoload imports Problem: mksession misses relative or absolute imports in the session, homonymous autoload scripts imported from different scripts cause errors (after v9.2.0579). Solution: Correctly write scripts imported via :import autoload, skip any that are no longer readable, and comment out imports whose autoload prefix or file name tail would conflict with an earlier one (Miguel Barro). fixes: #12641 closes: #20564 Signed-off-by: Miguel Barro Signed-off-by: Christian Brabandt --- src/session.c | 38 +++++++++- src/testdir/test_mksession.vim | 124 +++++++++++++++++++++++++++++++++ src/version.c | 2 + 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/src/session.c b/src/session.c index 090448937f..0fa03e556d 100644 --- a/src/session.c +++ b/src/session.c @@ -1333,10 +1333,42 @@ ex_mkrc(exarg_T *eap) for (sid = 1; sid <= script_items.ga_len; ++sid) { si = SCRIPT_ITEM(sid); - if (si->sn_autoload_prefix && - (fprintf(fd, "import autoload '%s'", si->sn_name) < 0 || - put_eol(fd) == FAIL)) + + // Autoload script paths may be absolute, relative to the + // current script or relative to a 'runtimepath' directory + // Ignore if missing + if ((si->sn_autoload_prefix || si->sn_import_autoload) + && file_is_readable(si->sn_name)) + { + // Check if conflicts with a previous import + int b_sid = sid - 1; + char_u *name = gettail(si->sn_name); + + for (; b_sid; --b_sid) + { + scriptitem_T *b_si = SCRIPT_ITEM(b_sid); + + // Only autoload may conflict. Ignore if missing + if ((!b_si->sn_autoload_prefix && !b_si->sn_import_autoload) + || !file_is_readable(b_si->sn_name)) + continue; + + // compare prefixes if available + if (si->sn_autoload_prefix != NULL && b_si->sn_autoload_prefix != NULL + && (STRCMP(si->sn_autoload_prefix, b_si->sn_autoload_prefix) == 0)) + break; + + // otherwise compare tails + char_u *b_name = gettail(b_si->sn_name); + if (STRCMP(name, b_name) == 0) + break; + } + + // import the auto script if there are no conflicts + if (fprintf(fd, "%simport autoload '%s'", b_sid ? "# " : "", si->sn_name) < 0 || + put_eol(fd) == FAIL) failed = TRUE; + } } #endif } diff --git a/src/testdir/test_mksession.vim b/src/testdir/test_mksession.vim index 4dec8815a4..0bccad1c66 100644 --- a/src/testdir/test_mksession.vim +++ b/src/testdir/test_mksession.vim @@ -1598,4 +1598,128 @@ func Test_mksession_localmappings() endfunc +" Test vim9 script relative auto imports (issue #12641) +func Test_mksession_vim9_relative_auto_import() + + " Dummy vim9 script + let script_sources =<< trim END + vim9script + import autoload './XAuto.vim' + nnoremap dummy-test XAuto.Test() + END + call writefile(script_sources, 'XScript.vim', 'D') + + let auto_sources =<< trim END + vim9script + const ref_txt = 'Hello from vim9 dummy relative auto script!' + export def Test() + if !has("gui_running") + exe $"echomsg '{ref_txt}'" + endif + writefile([ref_txt], 'XDummyOutput') + enddef + END + call writefile(auto_sources, 'XAuto.vim', 'D') + + " Source the script + const ref_txt = 'Hello from vim9 dummy relative auto script!' + source XScript.vim + + " Execute mapping + normal dummy-test + + if !has('gui_running') + call assert_match(ref_txt, execute('messages'), 'No vim9 auto script XAuto.Test() execution') + endif + call assert_true(filereadable('XDummyOutput'), 'Output file was not created by Vim9 auto script') + call assert_equal([ref_txt], readfile('XDummyOutput')) + call delete('XDummyOutput') + + " Create a session file + mksession! XDummySession.vim + defer delete('XDummySession.vim') + call assert_true(filereadable('XDummySession.vim'), 'Session file was not created') + + " Check the session file mappings are operational + let test_sources =<< trim END + " Load session + source XDummySession.vim + " Execute mapping + normal dummy-test + " on my way + cq + END + call writefile(test_sources, 'XTest.vim', 'D') + " spawn a new Vim instance to load the session and execute the mapping + call system(GetVimCommand('XTest.vim')) + call assert_true(filereadable('XDummyOutput'), + \ 'Expected output file was not created by Vim9 auto script mapping') + defer delete('XDummyOutput') + call assert_equal([ref_txt], readfile('XDummyOutput')) + +endfunc + +" Test vim9 script avoid homonimous auto imports +func Test_mksession_vim9_duplicate_import() + + " Auto script + let auto_sources =<< trim END + vim9script + const ref_txt = 'Hello from a duplicated vim9 script!' + export def Test() + writefile([ref_txt], 'XDummyOutput') + enddef + END + + " Dummy vim9 script + let script_sources =<< trim END + vim9script + import autoload './XAuto.vim' + nnoremap dummy-test XAuto.Test() + END + + for i in range(1, 5) + let dir = $'XDir{i}' + call mkdir(dir, 'p') + defer delete(dir, 'rf') + + let autofile = dir . '/XAuto.vim' + call writefile(auto_sources, autofile, 'D') + + let scriptfile = dir . '/XScript.vim' + call writefile(script_sources, scriptfile, 'D') + exe "source " . scriptfile + endfor + + " Create a session file + mksession! XDummySession.vim + defer delete('XDummySession.vim') + call assert_true(filereadable('XDummySession.vim'), 'Session file was not created') + + " Check there are commented imports in the session file + let commented_imports = filter(readfile('XDummySession.vim'), + \ {_, v -> v =~ '^# import autoload'}) + call assert_equal(4, len(commented_imports), + \ 'Session file does not contain the expected number of commented imports') + + " Check the session file mappings are operational + const ref_txt = 'Hello from a duplicated vim9 script!' + let test_sources =<< trim END + " Load session + source XDummySession.vim + " Execute mapping + normal dummy-test + " on my way + cq + END + call writefile(test_sources, 'XTest.vim', 'D') + " spawn a new Vim instance to load the session and execute the mapping + call system(GetVimCommand('XTest.vim')) + call assert_true(filereadable('XDummyOutput'), + \ 'Expected output file was not created by Vim9 auto script mapping') + defer delete('XDummyOutput') + call assert_equal([ref_txt], readfile('XDummyOutput')) + +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/version.c b/src/version.c index 67a0fa6508..45b222e7ad 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 703, /**/ 702, /**/ From d1f7c37656763c3cccc7e3d4198ffe567054b76d Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Mon, 22 Jun 2026 19:30:28 +0000 Subject: [PATCH 07/33] patch 9.2.0704: GTK4: not handling mouse events Problem: GTK4: not handling mouse events Solution: Emit mouse moved events and handle mouse dragging (Foxe Chen). closes: #20545 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui_gtk4.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/version.c | 2 ++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index 4874b2f626..bcadb31a56 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -2046,6 +2046,10 @@ modifiers_gdk2mouse(guint state) // -1 means no button is pressed (MOUSE_LEFT is 0x00, so can't use 0). static int mouse_pressed_button = -1; +static guint motion_repeat_timer = 0; +static gboolean motion_repeat_offset = FALSE; +static int mouse_press_x = -1, mouse_press_y = -1; + static void button_press_event(GtkGestureClick *gesture, int n_press UNUSED, double x, double y, gpointer data UNUSED) @@ -2083,6 +2087,9 @@ button_press_event(GtkGestureClick *gesture, int n_press UNUSED, default: return; } + mouse_press_x = (int)x; + mouse_press_y = (int)y; + mouse_pressed_button = button; vim_modifiers = modifiers_gdk2mouse(state); gui_send_mouse_event(button, (int)x, (int)y, repeated_click, vim_modifiers); @@ -2101,6 +2108,13 @@ button_release_event(GtkGestureClick *gesture, int n_press UNUSED, state = gdk_event_get_modifier_state(event); vim_modifiers = modifiers_gdk2mouse(state); + // If we are repeating motion events, then stop + if (motion_repeat_timer != 0) + { + timeout_remove(motion_repeat_timer); + motion_repeat_timer = 0; + } + mouse_pressed_button = -1; gui_send_mouse_event(MOUSE_RELEASE, (int)x, (int)y, FALSE, vim_modifiers); } @@ -2108,6 +2122,43 @@ button_release_event(GtkGestureClick *gesture, int n_press UNUSED, static double prev_mouse_x = -1.0; static double prev_mouse_y = -1.0; +static GdkModifierType cur_state = 0; + + static timeout_cb_type +mouse_repeat_timer_cb(gpointer data) +{ + int x, y; + + if (mouse_pressed_button == -1) + { + motion_repeat_timer = 0; + return G_SOURCE_REMOVE; + } + + // If there already is a mouse click in the input buffer, wait another + // time (otherwise we would create a backlog of clicks) + if (vim_used_in_input_buf() > 10) + return G_SOURCE_CONTINUE; + + x = (int)prev_mouse_x; + y = (int)prev_mouse_y; + + // Fake a motion event. + // + // Trick: Pretend the mouse moved to the next character on every other + // event, otherwise drag events will be discarded, because they are still in + // the same character. + if (motion_repeat_offset) + x += gui.char_width; + motion_repeat_offset = !motion_repeat_offset; + + gui_send_mouse_event(MOUSE_DRAG, x, y, FALSE, + modifiers_gdk2mouse(cur_state)); + + // Always continue, when button is released, then this timer is removed. + return G_SOURCE_CONTINUE; +} + static void motion_notify_event(GtkEventControllerMotion *controller UNUSED, double x, double y, gpointer data UNUSED) @@ -2115,17 +2166,54 @@ motion_notify_event(GtkEventControllerMotion *controller UNUSED, if (mouse_pressed_button >= 0) { GdkModifierType state; - GdkEvent *event; + GdkEvent *event; + int w, h; event = gtk_event_controller_get_current_event( GTK_EVENT_CONTROLLER(controller)); + if (event != NULL) { - state = gdk_event_get_modifier_state(event); + cur_state = state = gdk_event_get_modifier_state(event); gui_send_mouse_event(MOUSE_DRAG, (int)x, (int)y, FALSE, modifiers_gdk2mouse(state)); } + + // Only start repeating motion if pointer is outside of draw area (and + // mouse button is being pressed down). Make frequency of motion + // repeats depend on how far away the pointer is from the start of the + // drag. + // + w = gtk_widget_get_width(gui.drawarea); + h = gtk_widget_get_height(gui.drawarea); + + if (motion_repeat_timer > 0) + timeout_remove(motion_repeat_timer); + + if (x < 0 || y < 0 || x >= w || y >= h) + { + int dx, dy; + int offshoot; + int delay; + + dx = x < 0 ? -x + mouse_press_x : x - mouse_press_x; + dy = y < 0 ? -y + mouse_press_y : y - mouse_press_y; + + offshoot = dx > dy ? dx : dy; + + if (offshoot > 127) + delay = 5; + else + delay = (130 * (127 - offshoot)) / 127 + 5; + + motion_repeat_timer = timeout_add(delay, + mouse_repeat_timer_cb, NULL); + } + else + motion_repeat_timer = 0; } + else + gui_mouse_moved((int)x, (int)y); // Only unhide if mouse actually moved. GTK seems to send a motion event // when switching tabs, causing the cursor to unhide. @@ -2139,7 +2227,7 @@ motion_notify_event(GtkEventControllerMotion *controller UNUSED, static void enter_notify_event(GtkEventControllerMotion *controller UNUSED, - double x UNUSED, double y UNUSED, gpointer data UNUSED) + double x, double y, gpointer data UNUSED) { prev_mouse_x = x; prev_mouse_y = y; diff --git a/src/version.c b/src/version.c index 45b222e7ad..6662a529e7 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 704, /**/ 703, /**/ From 7aeab74687f92acff3ef3ab83a3c75dc0d3c2887 Mon Sep 17 00:00:00 2001 From: Doug Kearns Date: Mon, 22 Jun 2026 19:36:59 +0000 Subject: [PATCH 08/33] patch 9.2.0705: :delete # silently fails to update "# and clobbers "0 Problem: ':delete #' silently fails to update "# and clobbers "0. Solution: Treat "# like "/, writable only with :let and setreg(). closes: #20592 Signed-off-by: Doug Kearns Signed-off-by: Christian Brabandt --- runtime/doc/change.txt | 18 ++++++++++-------- runtime/doc/tags | 1 + src/errors.h | 4 ++-- src/po/vim.pot | 7 +++---- src/register.c | 9 ++++----- src/testdir/test_excmd.vim | 13 +++++++++++++ src/testdir/test_registers.vim | 1 + src/version.c | 2 ++ src/vim9compile.c | 2 +- 9 files changed, 37 insertions(+), 20 deletions(-) diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt index afcdca16b0..639484e339 100644 --- a/runtime/doc/change.txt +++ b/runtime/doc/change.txt @@ -1,4 +1,4 @@ -*change.txt* For Vim version 9.2. Last change: 2026 Jun 18 +*change.txt* For Vim version 9.2. Last change: 2026 Jun 22 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1332,10 +1332,11 @@ and ":put" commands and with CTRL-R. the command was executed completely from a mapping. {not available when compiled without the |+cmdline_hist| feature} - *quote_#* *quote#* -6. Alternate file register "# + +6. Alternate file register "# *quote_#* *quote#* *@#* Contains the |alternate-file| name for current window -This register is writeable and changes which buffer CTRL-^ enters. +This register is writable with `:let` and |setreg()|. This changes which buffer +CTRL-^ enters. You can't yank or delete into this register. A String is matched against existing buffer names, like |:buffer|: > let @# = 'buffer_name' Also supports using buffer number and |file-pattern|. @@ -1398,11 +1399,12 @@ When writing to this register, nothing happens. This can be used to delete text without affecting the normal registers. When reading from this register, nothing is returned. -10. Last search pattern register "/ *quote_/* *quote/* +10. Last search pattern register "/ *quote_/* *quote/* Contains the most recent search-pattern. This is used for "n" and 'hlsearch'. -It is writable with `:let`, you can change it to have 'hlsearch' highlight -other matches without actually searching. You can't yank or delete into this -register. The search direction is available in |v:searchforward|. +This register is writable with `:let` and |setreg()|. You can change it to have +'hlsearch' highlight other matches without actually searching. You can't yank +or delete into this register. The search direction is available in +|v:searchforward|. Note that the value is restored when returning from a function |function-search-undo|. diff --git a/runtime/doc/tags b/runtime/doc/tags index 405e1254d4..11ae058bbc 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -4015,6 +4015,7 @@ $quote eval.txt /*$quote* ? pattern.txt /*?* ?? eval.txt /*??* @ repeat.txt /*@* +@# change.txt /*@#* @/ change.txt /*@\/* @: repeat.txt /*@:* @= change.txt /*@=* diff --git a/src/errors.h b/src/errors.h index 530e47c1d9..e1e7ada84d 100644 --- a/src/errors.h +++ b/src/errors.h @@ -2287,8 +2287,8 @@ EXTERN char e_line_count_changed_unexpectedly[] #ifdef FEAT_EVAL EXTERN char e_uniq_compare_function_failed[] INIT(= N_("E882: Uniq compare function failed")); -EXTERN char e_search_pattern_and_expression_register_may_not_contain_two_or_more_lines[] - INIT(= N_("E883: Search pattern and expression register may not contain two or more lines")); +EXTERN char e_register_char_cannot_contain_multiple_lines[] + INIT(= N_("E883: Register '%c' cannot contain multiple lines")); EXTERN char e_function_name_cannot_contain_colon_str[] INIT(= N_("E884: Function name cannot contain a colon: %s")); #endif diff --git a/src/po/vim.pot b/src/po/vim.pot index b185a51444..bbbf9f0cc2 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-06-16 19:21+0000\n" +"POT-Creation-Date: 2026-06-22 19:36+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -6678,9 +6678,8 @@ msgstr "" msgid "E882: Uniq compare function failed" msgstr "" -msgid "" -"E883: Search pattern and expression register may not contain two or more " -"lines" +#, c-format +msgid "E883: Register '%c' cannot contain multiple lines" msgstr "" #, c-format diff --git a/src/register.c b/src/register.c index 95a1145e43..5d22dc34a8 100644 --- a/src/register.c +++ b/src/register.c @@ -181,12 +181,11 @@ valid_yank_reg( if ( (regname > 0 && ASCII_ISALNUM(regname)) || (!writing && vim_strchr((char_u *) #ifdef FEAT_EVAL - "/.%:=" + "/#.%:=" #else - "/.%:" + "/#.%:" #endif , regname) != NULL) - || regname == '#' || regname == '"' || regname == '-' || regname == '_' @@ -3093,7 +3092,7 @@ write_reg_contents_lst( { yankreg_T *old_y_previous, *old_y_current; - if (name == '/' || name == '=') + if (name == '/' || name == '=' || name == '#') { char_u *s; @@ -3101,7 +3100,7 @@ write_reg_contents_lst( s = (char_u *)""; else if (strings[1] != NULL) { - emsg(_(e_search_pattern_and_expression_register_may_not_contain_two_or_more_lines)); + semsg(_(e_register_char_cannot_contain_multiple_lines), name); return; } else diff --git a/src/testdir/test_excmd.vim b/src/testdir/test_excmd.vim index 0de0771f78..eea75d7e5d 100644 --- a/src/testdir/test_excmd.vim +++ b/src/testdir/test_excmd.vim @@ -4,11 +4,24 @@ source util/screendump.vim func Test_ex_delete() new + call setline(1, ['a', 'b', 'c']) 2 " :dl is :delete with the "l" flag, not :dlist .dl call assert_equal(['a', 'c'], getline(1, 2)) + %delete _ + + " :delete # used to clobber "0 + call setreg('#', '') + call setreg('0', '') + call setline(1, ['a', 'b', 'c']) + call assert_fails("1delete #", 'E488:') + call assert_equal(['a', 'b', 'c'], getline(1, '$')) + call assert_equal('', getreg('#')) + call assert_equal('', getreg('0')) + + bw! endfunc func Test_range_error() diff --git a/src/testdir/test_registers.vim b/src/testdir/test_registers.vim index e1c493ac4b..d59da86b33 100644 --- a/src/testdir/test_registers.vim +++ b/src/testdir/test_registers.vim @@ -423,6 +423,7 @@ func Test_set_register() call assert_equal('', @=) call assert_fails("call setreg('/', ['a', 'b'])", 'E883:') call assert_fails("call setreg('=', ['a', 'b'])", 'E883:') + call assert_fails("call setreg('#', ['a', 'b'])", 'E883:') call assert_equal(0, setreg('_', ['a', 'b'])) " Test for recording to a invalid register diff --git a/src/version.c b/src/version.c index 6662a529e7..14fb4ea489 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 705, /**/ 704, /**/ diff --git a/src/vim9compile.c b/src/vim9compile.c index aa9384bbe9..181df2a6fb 100644 --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -1481,7 +1481,7 @@ valid_dest_reg(int name) { if (name == '@') name = '"'; - if (name == '/' || name == '=' || valid_yank_reg(name, TRUE)) + if (name == '/' || name == '=' || name == '#' || valid_yank_reg(name, TRUE)) return TRUE; emsg_invreg(name); return FAIL; From fc6d0d418d7508a0c9ec55306b937ba18d2cca25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E4=BA=91?= Date: Mon, 22 Jun 2026 19:43:17 +0000 Subject: [PATCH 09/33] runtime(beancount): Add support for non-ASCII account names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes: #20597 Signed-off-by: 依云 Signed-off-by: Christian Brabandt --- runtime/syntax/beancount.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/syntax/beancount.vim b/runtime/syntax/beancount.vim index 4909c4bc08..78d505eebb 100644 --- a/runtime/syntax/beancount.vim +++ b/runtime/syntax/beancount.vim @@ -2,6 +2,7 @@ " Language: beancount " Maintainer: Nathan Grigg " Latest Revision: 2024-11-25 +" 2026 Jun 22 by Vim Project: allow non-ASCII account names if exists("b:current_syntax") finish @@ -16,7 +17,7 @@ syn match beanAmount "\v[-+]?[[:digit:].,]+" nextgroup=beanCurrency contained \ skipwhite syn match beanCurrency "\v\w+" contained " Account name: alphanumeric with at least one colon. -syn match beanAccount "\v[[:alnum:]]+:[-[:alnum:]:]+" contained +syn match beanAccount "\v[[:alnum:]]+:\S+" contained syn match beanTag "\v#[-[:alnum:]]+" contained syn match beanLink "\v\^\S+" contained " We must require a space after the flag because you can have flags per From 6fd0a9cc303c94c9f894f7f52181bd0b1f06f4b3 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Mon, 22 Jun 2026 19:45:05 +0000 Subject: [PATCH 10/33] patch 9.2.0706: tests: test_terminal3 may fail when $SHELL is zsh Problem: tests: test_terminal3 may fail when the shell ($SHELL) is zsh with a custom prompt, because the prompt wraps the terminal line and shifts the expected output. Solution: Reset $HOME and $PS1 to sane defaults so the shell uses a minimal prompt, and adjust the expected window height. closes: #20599 Signed-off-by: Christian Brabandt --- src/testdir/test_terminal3.vim | 8 +++++--- src/version.c | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/testdir/test_terminal3.vim b/src/testdir/test_terminal3.vim index 22d19909a5..cf87038ef1 100644 --- a/src/testdir/test_terminal3.vim +++ b/src/testdir/test_terminal3.vim @@ -1197,7 +1197,7 @@ func Test_terminal_max_combining_chars() " somehow doesn't work on MS-Windows CheckUnix let cmd = "cat samples/terminal_max_combining_chars.txt\" - let buf = Run_shell_in_terminal({'term_rows': 15, 'term_cols': 35}) + let buf = Run_shell_in_terminal({'term_rows': 15, 'term_cols': 35, 'env': {'HOME': '/nonexisting', 'PS1':''}}) call TermWait(buf) call term_sendkeys(buf, cmd) " last char is a space with many combining chars @@ -1214,6 +1214,8 @@ func Test_term_getpos() defer delete('XTest_getpos_result') let lines =<< trim EOL + let $PS1='' + let $HOME='/nonexisting' term ++curwin sh EOL call writefile(lines, 'XTest_getpos', 'D') @@ -1243,8 +1245,8 @@ func Test_term_getpos() call WaitForAssert({-> assert_true(filereadable('XTest_getpos_result'))}) call WaitForAssert({-> assert_equal(2, len(readfile('XTest_getpos_result')))}) let result = readfile('XTest_getpos_result') - " 15 - 1: statusline - 1: for prompt line - call assert_equal(13, str2nr(result[1]) - str2nr(result[0])) + " 15 - 1: statusline - 1: for prompt line, w$-w0 = 12 + call assert_equal(12, str2nr(result[1]) - str2nr(result[0])) call assert_true(str2nr(result[0]) > 1) " Regression: line('w0') and line('w$') must not move cursor position diff --git a/src/version.c b/src/version.c index 14fb4ea489..e663c2117f 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 706, /**/ 705, /**/ From 4ed61e0a199d635c6cdaaff6c0657db3a8d3d445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Marek?= Date: Mon, 22 Jun 2026 19:55:32 +0000 Subject: [PATCH 11/33] runtime(dtrace): handle DTrace probe highlighting before action blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recognize DTrace probe descriptions that are followed immediately by an action block, such as: BEGIN{ trace(1); } syscall::open:entry{ trace(1); } The fourth probe field now consumes the remaining non-whitespace text, and the lookahead allows zero or more whitespace before the following token. closes: #20560 Signed-off-by: Vladimír Marek Signed-off-by: Christian Brabandt --- runtime/syntax/dtrace.vim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/syntax/dtrace.vim b/runtime/syntax/dtrace.vim index 392fa1c1c9..fcaa216fa7 100644 --- a/runtime/syntax/dtrace.vim +++ b/runtime/syntax/dtrace.vim @@ -4,6 +4,7 @@ " http://docs.sun.com/app/docs/doc/817-6223 " Version: 1.5 " Last Change: 2008/04/05 +" 2026 Jun 22 by Vim project: handle DTrace probe descriptions that are followed immediately by an action block " Maintainer: Nicolas Weber " dtrace lexer and parser are at @@ -35,8 +36,8 @@ syn match dtraceComment "\%^#!.*-s.*" " XXX: This allows a probe description to end with ',', even if it's not " followed by another probe. " XXX: This doesn't work if followed by a comment. -let s:oneProbe = '\%(BEGIN\|END\|ERROR\|\S\{-}:\S\{-}:\S\{-}:\S\{-}\)\_s*' -exec 'syn match dtraceProbe "'.s:oneProbe.'\%(,\_s*'.s:oneProbe.'\)*\ze\_s\%({\|\/[^*]\|\%$\)"' +let s:oneProbe = '\%(BEGIN\|END\|ERROR\|\S\{-}:\S\{-}:\S\{-}:\S*\)\_s*' +exec 'syn match dtraceProbe "'.s:oneProbe.'\%(,\_s*'.s:oneProbe.'\)*\ze\_s*\%({\|\/[^*]\|\_s*\S\|\%$\)"' " Note: We have to be careful to not make this match /* */ comments. " Also be careful not to eat `c = a / b; b = a / 2;`. We use the same From 4ed08ee60060ea7515422af97d251a96d6e54695 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Mon, 22 Jun 2026 20:05:58 +0000 Subject: [PATCH 12/33] runtime(doc): document Solaris as supported OS related: #20567 Signed-off-by: Christian Brabandt --- runtime/doc/vi_diff.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/doc/vi_diff.txt b/runtime/doc/vi_diff.txt index d87db7cc59..7b85800add 100644 --- a/runtime/doc/vi_diff.txt +++ b/runtime/doc/vi_diff.txt @@ -1,4 +1,4 @@ -*vi_diff.txt* For Vim version 9.2. Last change: 2026 Mar 08 +*vi_diff.txt* For Vim version 9.2. Last change: 2026 Jun 22 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1388,6 +1388,7 @@ macOS: | fully supported up until v10.6 (?) MS-Windows 7, 8, 10, 11: | fully supported OpenVMS: | supported QNX: | still supported (?) +Solaris: | supported (on maintained versions) UNIX: | supported (on maintained versions) zOS/OS390: | still supported (?) From d167c50de4b954e5ddd97208187c5b3b77dd89cc Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 22 Jun 2026 20:13:22 +0000 Subject: [PATCH 13/33] patch 9.2.0707: completion: popup misplaced when text before it is concealed Problem: When the cursor line has concealed text before the start of the completion, the insert-mode completion popup is drawn at the wrong screen column and the cursor no longer lines up with the completed text. Solution: Record the concealed width before the cursor on its screen line in a new `win_T` field while `win_line()` draws it, subtract it in `pum_display()` to place the menu over the visible text, and redraw the cursor line so `win_line()` corrects the cursor too. closes: #20539 Signed-off-by: Barrett Ruth Signed-off-by: Christian Brabandt --- src/drawline.c | 4 + src/insexpand.c | 9 ++ src/popupmenu.c | 11 +++ src/structs.h | 4 + ...est_pum_position_with_concealed_match.dump | 10 ++ .../Test_pum_position_with_concealed_rl.dump | 10 ++ ...Test_pum_position_with_concealed_text.dump | 10 ++ ...Test_pum_position_with_concealed_wrap.dump | 10 ++ src/testdir/test_ins_complete.vim | 98 +++++++++++++++++++ src/version.c | 2 + 10 files changed, 168 insertions(+) create mode 100644 src/testdir/dumps/Test_pum_position_with_concealed_match.dump create mode 100644 src/testdir/dumps/Test_pum_position_with_concealed_rl.dump create mode 100644 src/testdir/dumps/Test_pum_position_with_concealed_text.dump create mode 100644 src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump diff --git a/src/drawline.c b/src/drawline.c index 86911cefc2..a2679e1467 100644 --- a/src/drawline.c +++ b/src/drawline.c @@ -3813,6 +3813,10 @@ win_line( else # endif wp->w_wcol = wlv.col - wlv.boguscols; + // Screen cells concealed before the cursor on this screen line, so + // pum_display() can line the menu up with the visible text; + // "skip_cells" is the concealed cell at the cursor not yet counted. + wp->w_wcol_conceal_off = wlv.vcol_off_co + skip_cells; if (wlv.vcol + skip_cells < wp->w_virtcol) // Cursor beyond end of the line with 'virtualedit'. wp->w_wcol += wp->w_virtcol - wlv.vcol - skip_cells; diff --git a/src/insexpand.c b/src/insexpand.c index 5adad45309..c7186b9297 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -1896,6 +1896,15 @@ ins_compl_show_pum(void) pum_display(compl_match_array, compl_match_arraysize, cur); curwin->w_cursor.col = col; +#ifdef FEAT_CONCEAL + // The cursor was temporarily moved to "compl_col" above to position the + // menu, so the screen update left w_wcol conceal-corrected for that column + // rather than for the real cursor. Redraw the cursor line so the caret is + // positioned correctly when the cursor line has concealed text. + if (curwin->w_p_cole > 0 && conceal_cursor_line(curwin)) + redrawWinline(curwin, curwin->w_cursor.lnum); +#endif + // After adding leader, set the current match to shown match. if (compl_started && compl_curr_match != compl_shown_match) compl_curr_match = compl_shown_match; diff --git a/src/popupmenu.c b/src/popupmenu.c index b7929607d9..bed2495b87 100644 --- a/src/popupmenu.c +++ b/src/popupmenu.c @@ -361,6 +361,17 @@ pum_display( { // w_wcol includes virtual text "above" int wcol = curwin->w_wcol % curwin->w_width; +#ifdef FEAT_CONCEAL + // w_wcol does not account for text concealed before the cursor; + // shift by the offset win_line() recorded for the cursor line so the + // menu lines up with the visible text. + if (curwin->w_p_cole > 0 && conceal_cursor_line(curwin)) + { + wcol -= curwin->w_wcol_conceal_off; + if (wcol < 0) + wcol = 0; + } +#endif #ifdef FEAT_RIGHTLEFT if (pum_rl) cursor_col = curwin->w_wincol + curwin->w_width - wcol - 1; diff --git a/src/structs.h b/src/structs.h index 5d6511a5a8..70010567d2 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4367,6 +4367,10 @@ struct window_S * buffer, thus w_wrow is relative to w_winrow. */ int w_wrow, w_wcol; // cursor position in window +#ifdef FEAT_CONCEAL + int w_wcol_conceal_off; // screen cells concealed before w_wcol on + // the cursor's screen line, set by win_line() +#endif /* * Info about the lines currently in the window is remembered to avoid diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_match.dump b/src/testdir/dumps/Test_pum_position_with_concealed_match.dump new file mode 100644 index 0000000000..f8d39c8bff --- /dev/null +++ b/src/testdir/dumps/Test_pum_position_with_concealed_match.dump @@ -0,0 +1,10 @@ +|++0#e0e0e08#6c6c6c255|f+0#0000000#ffffff0|o@1|b|a|r| @67 +|++0#e0e0e08#6c6c6c255|f+0#0000000#ffffff0|o@1|b|a|r> @67 +| +0#0000001#e0e0e08|f|o@1|b|a|r| @8| +0#4040ff13#ffffff0@58 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25 diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump b/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump new file mode 100644 index 0000000000..c2f497a871 --- /dev/null +++ b/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump @@ -0,0 +1,10 @@ +| +0&#ffffff0@68|r|a|b|o@1|f +| @67> |r|a|b|o@1|f +| +0#4040ff13&@59| +0#0000001#e0e0e08@8|r|a|b|o@1|f +| +0#4040ff13#ffffff0@73|~ +| @73|~ +| @73|~ +| @73|~ +| @73|~ +| @73|~ +|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25 diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_text.dump b/src/testdir/dumps/Test_pum_position_with_concealed_text.dump new file mode 100644 index 0000000000..a30e2f86bb --- /dev/null +++ b/src/testdir/dumps/Test_pum_position_with_concealed_text.dump @@ -0,0 +1,10 @@ +|f+0&#ffffff0|o@1|b|a|r| @68 +|f|o@1|b|a|r> @68 +|f+0#0000001#e0e0e08|o@1|b|a|r| @8| +0#4040ff13#ffffff0@59 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25 diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump b/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump new file mode 100644 index 0000000000..0f3a3cbe0f --- /dev/null +++ b/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump @@ -0,0 +1,10 @@ +|f+0&#ffffff0|o@1|b|a|r| @13 +|a@19 +| |f|o@1|b|a|r> @12 +| +0#0000001#e0e0e08|f|o@1|b|a|r| @8| +0#4040ff13#ffffff0@3 +|~| @18 +|~| @18 +|~| @18 +|~| @18 +|~| @18 +|-+2#0000000&@1| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@2 diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 93ca66f5a9..cb901d609f 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -870,6 +870,104 @@ func Test_pum_stopped_by_timer() call StopVimInTerminal(buf) endfunc +" The completion popup menu must line up with the start of the completed text +" on screen, also when there is concealed text before it on the line. +func Test_pum_position_with_concealed_text() + CheckScreendump + + let lines =<< trim END + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconceal', 'D') + let buf = RunVimInTerminal('-S Xpumconceal', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_text', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" Same alignment when the concealed text comes from a match and is shown as a +" replacement character with 'conceallevel' 2. +func Test_pum_position_with_concealed_match() + CheckScreendump + + let lines =<< trim END + call setline(1, ['XXX foobar', 'XXX foo']) + call matchadd('Conceal', 'XXX ', 10, -1, {'conceal': '+'}) + setlocal conceallevel=2 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealmatch', 'D') + let buf = RunVimInTerminal('-S Xpumconcealmatch', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_match', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" The menu lines up with the visible text in a 'rightleft' window too, where +" the cursor screen column is mirrored. +func Test_pum_position_with_concealed_rl() + CheckScreendump + CheckFeature rightleft + + let lines =<< trim END + set rightleft + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealrl', 'D') + let buf = RunVimInTerminal('-S Xpumconcealrl', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_rl', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" The recorded offset is per screen line, so the menu also lines up when the +" concealed text and the completion are on a wrapped continuation line. +func Test_pum_position_with_concealed_wrap() + CheckScreendump + + let lines =<< trim END + call setline(1, ['foobar', 'aaaaaaaaaaaaaaaaaaaa CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealwrap', 'D') + let buf = RunVimInTerminal('-S Xpumconcealwrap', #{rows: 10, cols: 20}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_wrap', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + func Test_complete_stopinsert_startinsert() nnoremap startinsert inoremap stopinsert diff --git a/src/version.c b/src/version.c index e663c2117f..ff0e68e1c7 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 707, /**/ 706, /**/ From ccdc81701459a1e5d8f6d84221f34fcb7e127e50 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Tue, 23 Jun 2026 17:49:27 +0000 Subject: [PATCH 14/33] CI: Restore daily Coverity Scan I finally got a note that Coverity is online back again. This partially reverts commit 0abffbff23adc4ae6e3c78af921a8c9a8cb6670f. ("CI: Remove Cirrus CI and Coverity Scan") related: #20431 Signed-off-by: Christian Brabandt --- .github/workflows/coverity.yml | 85 ++++++++++++++++++++++++++++++++++ Filelist | 1 + README.md | 1 + ci/lychee.toml | 1 + runtime/doc/todo.txt | 5 +- 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/coverity.yml diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml new file mode 100644 index 0000000000..b4c64f3f28 --- /dev/null +++ b/.github/workflows/coverity.yml @@ -0,0 +1,85 @@ +name: Coverity +on: + schedule: + - cron: '42 0 * * *' # Run once per day, to avoid Coverity's submission limits + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + scan: + runs-on: ubuntu-24.04 + + env: + CC: gcc + DEBIAN_FRONTEND: noninteractive + TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} + + steps: + - name: Checkout repository from github + if: env.TOKEN + uses: actions/checkout@v6.0.2 + + - name: Download Coverity + if: env.TOKEN + run: | + wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=$TOKEN&project=vim" -O coverity_tool.tgz + mkdir cov-scan + tar ax -f coverity_tool.tgz --strip-components=1 -C cov-scan + + - name: Install packages + if: env.TOKEN + run: | + sudo apt-get update && sudo apt-get install -y \ + autoconf \ + gettext \ + libcanberra-dev \ + libperl-dev \ + python3-dev \ + liblua5.4-dev \ + lua5.4 \ + ruby-dev \ + tcl-dev \ + libgtk2.0-dev \ + desktop-file-utils \ + libtool-bin \ + libsodium-dev + + - name: Set up environment + if: env.TOKEN + run: | + echo "$(pwd)/cov-scan/bin" >> $GITHUB_PATH + ( + echo "NPROC=$(getconf _NPROCESSORS_ONLN)" + echo "CONFOPT=--enable-perlinterp --enable-python3interp --enable-rubyinterp --enable-luainterp --enable-tclinterp" + ) >> $GITHUB_ENV + + - name: Configure + if: env.TOKEN + run: | + ./configure --with-features=huge ${CONFOPT} --enable-fail-if-missing + # Append various warning flags to CFLAGS. + sed -i -f ci/config.mk.sed src/auto/config.mk + sed -i -f ci/config.mk.${CC}.sed src/auto/config.mk + # -O2 gives false warning and turns it into an error: + # warning: function may return address of local variable [-Wreturn-local-addr] + sed -i 's/-O2 \?//' src/auto/config.mk + + - name: Build/scan vim + if: env.TOKEN + run: | + cov-build --dir cov-int make -j${NPROC} + + - name: Submit results + if: env.TOKEN + run: | + tar zcf cov-scan.tgz cov-int + curl --form token=$TOKEN \ + --form email=$EMAIL \ + --form file=@cov-scan.tgz \ + --form version="$(git rev-parse HEAD)" \ + --form description="Automatic GHA scan" \ + 'https://scan.coverity.com/builds?project=vim' + env: + EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }} diff --git a/Filelist b/Filelist index 8f36b1c66f..019fa1aeea 100644 --- a/Filelist +++ b/Filelist @@ -14,6 +14,7 @@ SRC_ALL = \ .github/workflows/ci-windows.yml \ .github/workflows/ci.yml \ .github/workflows/codeql-analysis.yml \ + .github/workflows/coverity.yml \ .github/workflows/link-check.yml \ .github/actions/build_vim_on_linux/action.yml \ .github/actions/test_artifacts/action.yml \ diff --git a/README.md b/README.md index 387e5b2bca..569aed94d4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Github Build status](https://github.com/vim/vim/workflows/GitHub%20CI/badge.svg)](https://github.com/vim/vim/actions?query=workflow%3A%22GitHub+CI%22) [![Coverage Status](https://codecov.io/gh/vim/vim/coverage.svg?branch=master)](https://codecov.io/gh/vim/vim?branch=master) +[![Coverity Scan](https://scan.coverity.com/projects/241/badge.svg)](https://scan.coverity.com/projects/vim) [![Debian CI](https://badges.debian.net/badges/debian/testing/vim/version.svg)](https://buildd.debian.org/vim) [![Packages](https://repology.org/badge/tiny-repos/vim.svg)](https://repology.org/metapackage/vim) [![Fossies codespell report](https://fossies.org/linux/test/vim-master.tar.gz/codespell.svg)](https://fossies.org/linux/test/vim-master.tar.gz/codespell.html) diff --git a/ci/lychee.toml b/ci/lychee.toml index 515e4f54d9..8b9f0099f2 100644 --- a/ci/lychee.toml +++ b/ci/lychee.toml @@ -26,6 +26,7 @@ exclude = [ '^file://.*', '^https?://(www\.)?badges\.debian\.net/.*$', '^https?://(www\.)?repology\.org/.*$', + '^https?://scan\.coverity\.com/.*$', '^https?://(www\.)?img\.shields\.io/.*$', '^https?://(www\.)?fossies\.org/.*$', '^https?://(www\.)?adobe\.com.*$', diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt index 66b8f96ed5..f265b39689 100644 --- a/runtime/doc/todo.txt +++ b/runtime/doc/todo.txt @@ -1,4 +1,4 @@ -*todo.txt* For Vim version 9.2. Last change: 2026 Jun 12 +*todo.txt* For Vim version 9.2. Last change: 2026 Jun 23 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1999,6 +1999,9 @@ Dominique can't reproduce it. ":function f(x) keepjumps" creates a function where every command is executed like it has ":keepjumps" before it. +Coverity: Check if there are new reported defects: +https://scan.coverity.com/projects/241 + Problem with editing file in binary mode. (Ingo Krabbe, 2009 Oct 8) Display error when 'tabline' that includes a file name with double-width From 8dfde7b3363151a228a69793ca8c940dd874506f Mon Sep 17 00:00:00 2001 From: Pooyan Khanjankhani Date: Tue, 23 Jun 2026 19:22:53 +0000 Subject: [PATCH 15/33] runtime(dnsmasq): add new keywords and order existing keywords alphabetically closes: #20616 Signed-off-by: Pooyan Khanjankhani Signed-off-by: Christian Brabandt --- runtime/syntax/dnsmasq.vim | 331 ++++++++++++++++++++++--------------- 1 file changed, 195 insertions(+), 136 deletions(-) diff --git a/runtime/syntax/dnsmasq.vim b/runtime/syntax/dnsmasq.vim index a4cc8b577b..c7064ea2ca 100644 --- a/runtime/syntax/dnsmasq.vim +++ b/runtime/syntax/dnsmasq.vim @@ -6,6 +6,7 @@ " File: runtime/syntax/dnsmasq.vim " Version: 2.76 " Last Change: 2015 Sep 27 +" 2026 Jun 23 by Vim project update dnsmasq keywords #20616 " Modeline: vim: ts=8:sw=2:sts=2: " " License: VIM License @@ -78,142 +79,200 @@ syn match DnsmasqKeywordSpecial "\:"me=e-1 syn match DnsmasqKeywordSpecial ",\"hs=s+1 contains=DnsmasqSpecial syn match DnsmasqKeywordSpecial "\:"me=e-1 -syn match DnsmasqKeyword "^\s*add-mac\>" -syn match DnsmasqKeyword "^\s*add-subnet\>" -syn match DnsmasqKeyword "^\s*addn-hosts\>" -syn match DnsmasqKeyword "^\s*address\>" -syn match DnsmasqKeyword "^\s*alias\>" -syn match DnsmasqKeyword "^\s*all-servers\>" -syn match DnsmasqKeyword "^\s*auth-zone\>" -syn match DnsmasqKeyword "^\s*bind-dynamic\>" -syn match DnsmasqKeyword "^\s*bind-interfaces\>" -syn match DnsmasqKeyword "^\s*bogus-nxdomain\>" -syn match DnsmasqKeyword "^\s*bogus-priv\>" -syn match DnsmasqKeyword "^\s*bootp-dynamic\>" -syn match DnsmasqKeyword "^\s*bridge-interface\>" -syn match DnsmasqKeyword "^\s*cache-size\>" -syn match DnsmasqKeyword "^\s*clear-on-reload\>" -syn match DnsmasqKeyword "^\s*cname\>" -syn match DnsmasqKeyword "^\s*conf-dir\>" -syn match DnsmasqKeyword "^\s*conf-file\>" -syn match DnsmasqKeyword "^\s*conntrack\>" -syn match DnsmasqKeyword "^\s*dhcp-alternate-port\>" -syn match DnsmasqKeyword "^\s*dhcp-authoritative\>" -syn match DnsmasqKeyword "^\s*dhcp-boot\>" -syn match DnsmasqKeyword "^\s*dhcp-broadcast\>" -syn match DnsmasqKeyword "^\s*dhcp-circuitid\>" -syn match DnsmasqKeyword "^\s*dhcp-client-update\>" -syn match DnsmasqKeyword "^\s*dhcp-duid\>" -syn match DnsmasqKeyword "^\s*dhcp-fqdn\>" -syn match DnsmasqKeyword "^\s*dhcp-generate-names\>" -syn match DnsmasqKeyword "^\s*dhcp-host\>" -syn match DnsmasqKeyword "^\s*dhcp-hostsfile\>" -syn match DnsmasqKeyword "^\s*dhcp-ignore\>" -syn match DnsmasqKeyword "^\s*dhcp-ignore-names\>" -syn match DnsmasqKeyword "^\s*dhcp-lease-max\>" -syn match DnsmasqKeyword "^\s*dhcp-leasefile\>" -syn match DnsmasqKeyword "^\s*dhcp-luascript\>" -syn match DnsmasqKeyword "^\s*dhcp-mac\>" -syn match DnsmasqKeyword "^\s*dhcp-match\>" -syn match DnsmasqKeyword "^\s*dhcp-no-override\>" -syn match DnsmasqKeyword "^\s*dhcp-option\>" -syn match DnsmasqKeyword "^\s*dhcp-option-force\>" -syn match DnsmasqKeyword "^\s*dhcp-optsfile\>" -syn match DnsmasqKeyword "^\s*dhcp-proxy\>" -syn match DnsmasqKeyword "^\s*dhcp-range\>" -syn match DnsmasqKeyword "^\s*dhcp-relay\>" -syn match DnsmasqKeyword "^\s*dhcp-remoteid\>" -syn match DnsmasqKeyword "^\s*dhcp-script\>" -syn match DnsmasqKeyword "^\s*dhcp-scriptuser\>" -syn match DnsmasqKeyword "^\s*dhcp-sequential-ip\>" -syn match DnsmasqKeyword "^\s*dhcp-subscrid\>" -syn match DnsmasqKeyword "^\s*dhcp-userclass\>" -syn match DnsmasqKeyword "^\s*dhcp-vendorclass\>" -syn match DnsmasqKeyword "^\s*dhcp-hostsdir\>" -syn match DnsmasqKeyword "^\s*dns-rr\>" -syn match DnsmasqKeyword "^\s*dnssec\>" -syn match DnsmasqKeyword "^\s*dnssec-check-unsigned\>" -syn match DnsmasqKeyword "^\s*dnssec-no-timecheck\>" -syn match DnsmasqKeyword "^\s*dnssec-timestamp\>" -syn match DnsmasqKeyword "^\s*dns-forward-max\>" -syn match DnsmasqKeyword "^\s*domain\>" -syn match DnsmasqKeyword "^\s*domain-needed\>" -syn match DnsmasqKeyword "^\s*edns-packet-max\>" -syn match DnsmasqKeyword "^\s*enable-dbus\>" -syn match DnsmasqKeyword "^\s*enable-ra\>" -syn match DnsmasqKeyword "^\s*enable-tftp\>" -syn match DnsmasqKeyword "^\s*except-interface\>" -syn match DnsmasqKeyword "^\s*expand-hosts\>" -syn match DnsmasqKeyword "^\s*filterwin2k\>" -syn match DnsmasqKeyword "^\s*group\>" -syn match DnsmasqKeyword "^\s*host-record\>" -syn match DnsmasqKeyword "^\s*interface\>" -syn match DnsmasqKeyword "^\s*interface-name\>" -syn match DnsmasqKeyword "^\s*ipset\>" -syn match DnsmasqKeyword "^\s*ignore-address\>" -syn match DnsmasqKeyword "^\s*keep-in-foreground\>" -syn match DnsmasqKeyword "^\s*leasefile-ro\>" -syn match DnsmasqKeyword "^\s*listen-address\>" -syn match DnsmasqKeyword "^\s*local\>" -syn match DnsmasqKeyword "^\s*localmx\>" -syn match DnsmasqKeyword "^\s*local-ttl\>" -syn match DnsmasqKeyword "^\s*local-service\>" -syn match DnsmasqKeyword "^\s*localise-queries\>" -syn match DnsmasqKeyword "^\s*log-async\>" -syn match DnsmasqKeyword "^\s*log-dhcp\>" -syn match DnsmasqKeyword "^\s*log-facility\>" -syn match DnsmasqKeyword "^\s*log-queries\>" -syn match DnsmasqKeyword "^\s*max-ttl\>" -syn match DnsmasqKeyword "^\s*max-cache-ttl\>" -syn match DnsmasqKeyword "^\s*min-cache-ttl\>" -syn match DnsmasqKeyword "^\s*min-port\>" -syn match DnsmasqKeyword "^\s*mx-host\>" -syn match DnsmasqKeyword "^\s*mx-target\>" -syn match DnsmasqKeyword "^\s*naptr-record\>" -syn match DnsmasqKeyword "^\s*neg-ttl\>" -syn match DnsmasqKeyword "^\s*no-daemon\>" -syn match DnsmasqKeyword "^\s*no-dhcp-interface\>" -syn match DnsmasqKeyword "^\s*no-hosts\>" -syn match DnsmasqKeyword "^\s*no-negcache\>" -syn match DnsmasqKeyword "^\s*no-ping\>" -syn match DnsmasqKeyword "^\s*no-poll\>" -syn match DnsmasqKeyword "^\s*no-resolv\>" -syn match DnsmasqKeyword "^\s*pid-file\>" -syn match DnsmasqKeyword "^\s*port\>" -syn match DnsmasqKeyword "^\s*proxy-dnssec\>" -syn match DnsmasqKeyword "^\s*ptr-record\>" -syn match DnsmasqKeyword "^\s*pxe-prompt\>" -syn match DnsmasqKeyword "^\s*pxe-service\>" -syn match DnsmasqKeyword "^\s*query-port\>" -syn match DnsmasqKeyword "^\s*quiet-ra\>" -syn match DnsmasqKeyword "^\s*quiet-dhcp\>" -syn match DnsmasqKeyword "^\s*quiet-dhcp6\>" -syn match DnsmasqKeyword "^\s*ra-param\>" -syn match DnsmasqKeyword "^\s*read-ethers\>" -syn match DnsmasqKeyword "^\s*rebind-domain-ok\>" -syn match DnsmasqKeyword "^\s*rebind-localhost-ok\>" -syn match DnsmasqKeyword "^\s*resolv-file\>" -syn match DnsmasqKeyword "^\s*rev-server\>" -syn match DnsmasqKeyword "^\s*selfmx\>" -syn match DnsmasqKeyword "^\s*server\>" -syn match DnsmasqKeyword "^\s*servers-file\>" -syn match DnsmasqKeyword "^\s*srv-host\>" -syn match DnsmasqKeyword "^\s*stop-dns-rebind\>" -syn match DnsmasqKeyword "^\s*strict-order\>" -syn match DnsmasqKeyword "^\s*synth-domain\>" -syn match DnsmasqKeyword "^\s*tag-if\>" -syn match DnsmasqKeyword "^\s*test\>" -syn match DnsmasqKeyword "^\s*tftp-max\>" -syn match DnsmasqKeyword "^\s*tftp-lowercase\>" -syn match DnsmasqKeyword "^\s*tftp-no-blocksize\>" -syn match DnsmasqKeyword "^\s*tftp-no-fail\>" -syn match DnsmasqKeyword "^\s*tftp-port-range\>" -syn match DnsmasqKeyword "^\s*tftp-root\>" -syn match DnsmasqKeyword "^\s*tftp-secure\>" -syn match DnsmasqKeyword "^\s*tftp-unique-root\>" -syn match DnsmasqKeyword "^\s*txt-record\>" -syn match DnsmasqKeyword "^\s*user\>" -syn match DnsmasqKeyword "^\s*version\>" +syn match DnsmasqKeyword "^\s*\zsadd-cpe-id\>" +syn match DnsmasqKeyword "^\s*\zsadd-mac\>" +syn match DnsmasqKeyword "^\s*\zsadd-subnet\>" +syn match DnsmasqKeyword "^\s*\zsaddn-hosts\>" +syn match DnsmasqKeyword "^\s*\zsaddress\>" +syn match DnsmasqKeyword "^\s*\zsalias\>" +syn match DnsmasqKeyword "^\s*\zsall-servers\>" +syn match DnsmasqKeyword "^\s*\zsauth-peer\>" +syn match DnsmasqKeyword "^\s*\zsauth-sec-servers\>" +syn match DnsmasqKeyword "^\s*\zsauth-server\>" +syn match DnsmasqKeyword "^\s*\zsauth-soa\>" +syn match DnsmasqKeyword "^\s*\zsauth-ttl\>" +syn match DnsmasqKeyword "^\s*\zsauth-zone\>" +syn match DnsmasqKeyword "^\s*\zsbind-dynamic\>" +syn match DnsmasqKeyword "^\s*\zsbind-interfaces\>" +syn match DnsmasqKeyword "^\s*\zsbogus-nxdomain\>" +syn match DnsmasqKeyword "^\s*\zsbogus-priv\>" +syn match DnsmasqKeyword "^\s*\zsbootp-dynamic\>" +syn match DnsmasqKeyword "^\s*\zsbridge-interface\>" +syn match DnsmasqKeyword "^\s*\zscaa-record\>" +syn match DnsmasqKeyword "^\s*\zscache-rr\>" +syn match DnsmasqKeyword "^\s*\zscache-size\>" +syn match DnsmasqKeyword "^\s*\zsclear-on-reload\>" +syn match DnsmasqKeyword "^\s*\zscname\>" +syn match DnsmasqKeyword "^\s*\zsconf-dir\>" +syn match DnsmasqKeyword "^\s*\zsconf-file\>" +syn match DnsmasqKeyword "^\s*\zsconf-script\>" +syn match DnsmasqKeyword "^\s*\zsconnmark-allowlist-enable\>" +syn match DnsmasqKeyword "^\s*\zsconnmark-allowlist\>" +syn match DnsmasqKeyword "^\s*\zsconntrack\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-alternate-port\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-authoritative\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-boot\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-broadcast\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-circuitid\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-client-update\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-duid\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-fqdn\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-generate-names\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-host\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-hostsdir\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-hostsfile\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-ignore-clid\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-ignore-names\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-ignore\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-lease-max\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-leasefile\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-luascript\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-mac\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-match\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-name-match\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-no-override\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-option-force\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-option-pxe\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-option\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-optsdir\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-optsfile\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-proxy\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-pxe-vendor\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-range\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-rapid-commit\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-relay\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-remoteid\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-reply-delay\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-script\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-scriptuser\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-sequential-ip\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-split-relay\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-subscrid\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-ttl\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-userclass\>" +syn match DnsmasqKeyword "^\s*\zsdhcp-vendorclass\>" +syn match DnsmasqKeyword "^\s*\zsdns-forward-max\>" +syn match DnsmasqKeyword "^\s*\zsdns-loop-detect\>" +syn match DnsmasqKeyword "^\s*\zsdns-rr\>" +syn match DnsmasqKeyword "^\s*\zsdnssec-check-unsigned\>" +syn match DnsmasqKeyword "^\s*\zsdnssec-debug\>" +syn match DnsmasqKeyword "^\s*\zsdnssec-limits\>" +syn match DnsmasqKeyword "^\s*\zsdnssec-no-timecheck\>" +syn match DnsmasqKeyword "^\s*\zsdnssec-timestamp\>" +syn match DnsmasqKeyword "^\s*\zsdnssec\>" +syn match DnsmasqKeyword "^\s*\zsdo-0x20-encode\>" +syn match DnsmasqKeyword "^\s*\zsdo-x20-encode\>" +syn match DnsmasqKeyword "^\s*\zsdomain-needed\>" +syn match DnsmasqKeyword "^\s*\zsdomain\>" +syn match DnsmasqKeyword "^\s*\zsdumpfile\>" +syn match DnsmasqKeyword "^\s*\zsdumpmask\>" +syn match DnsmasqKeyword "^\s*\zsdynamic-host\>" +syn match DnsmasqKeyword "^\s*\zsedns-packet-max\>" +syn match DnsmasqKeyword "^\s*\zsenable-dbus\>" +syn match DnsmasqKeyword "^\s*\zsenable-ra\>" +syn match DnsmasqKeyword "^\s*\zsenable-tftp\>" +syn match DnsmasqKeyword "^\s*\zsenable-ubus\>" +syn match DnsmasqKeyword "^\s*\zsexcept-interface\>" +syn match DnsmasqKeyword "^\s*\zsexpand-hosts\>" +syn match DnsmasqKeyword "^\s*\zsfast-dns-retry\>" +syn match DnsmasqKeyword "^\s*\zsfilter-AAAA\>" +syn match DnsmasqKeyword "^\s*\zsfilter-A\>" +syn match DnsmasqKeyword "^\s*\zsfilter-rr\>" +syn match DnsmasqKeyword "^\s*\zsfilterwin2k\>" +syn match DnsmasqKeyword "^\s*\zsgroup\>" +syn match DnsmasqKeyword "^\s*\zshelp\>" +syn match DnsmasqKeyword "^\s*\zshost-record\>" +syn match DnsmasqKeyword "^\s*\zshostsdir\>" +syn match DnsmasqKeyword "^\s*\zsignore-address\>" +syn match DnsmasqKeyword "^\s*\zsinterface-name\>" +syn match DnsmasqKeyword "^\s*\zsinterface\>" +syn match DnsmasqKeyword "^\s*\zsipset\>" +syn match DnsmasqKeyword "^\s*\zskeep-in-foreground\>" +syn match DnsmasqKeyword "^\s*\zsleasefile-ro\>" +syn match DnsmasqKeyword "^\s*\zsleasequery\>" +syn match DnsmasqKeyword "^\s*\zslisten-address\>" +syn match DnsmasqKeyword "^\s*\zslocal-service\>" +syn match DnsmasqKeyword "^\s*\zslocal-ttl\>" +syn match DnsmasqKeyword "^\s*\zslocal\>" +syn match DnsmasqKeyword "^\s*\zslocalise-queries\>" +syn match DnsmasqKeyword "^\s*\zslocalmx\>" +syn match DnsmasqKeyword "^\s*\zslog-async\>" +syn match DnsmasqKeyword "^\s*\zslog-debug\>" +syn match DnsmasqKeyword "^\s*\zslog-dhcp\>" +syn match DnsmasqKeyword "^\s*\zslog-facility\>" +syn match DnsmasqKeyword "^\s*\zslog-malloc\>" +syn match DnsmasqKeyword "^\s*\zslog-queries\>" +syn match DnsmasqKeyword "^\s*\zsmax-cache-ttl\>" +syn match DnsmasqKeyword "^\s*\zsmax-port\>" +syn match DnsmasqKeyword "^\s*\zsmax-tcp-connections\>" +syn match DnsmasqKeyword "^\s*\zsmax-ttl\>" +syn match DnsmasqKeyword "^\s*\zsmin-cache-ttl\>" +syn match DnsmasqKeyword "^\s*\zsmin-port\>" +syn match DnsmasqKeyword "^\s*\zsmx-host\>" +syn match DnsmasqKeyword "^\s*\zsmx-target\>" +syn match DnsmasqKeyword "^\s*\zsnaptr-record\>" +syn match DnsmasqKeyword "^\s*\zsneg-ttl\>" +syn match DnsmasqKeyword "^\s*\zsnftset\>" +syn match DnsmasqKeyword "^\s*\zsno-0x20-encode\>" +syn match DnsmasqKeyword "^\s*\zsno-daemon\>" +syn match DnsmasqKeyword "^\s*\zsno-dhcp-interface\>" +syn match DnsmasqKeyword "^\s*\zsno-dhcpv4-interface\>" +syn match DnsmasqKeyword "^\s*\zsno-dhcpv6-interface\>" +syn match DnsmasqKeyword "^\s*\zsno-hosts\>" +syn match DnsmasqKeyword "^\s*\zsno-ident\>" +syn match DnsmasqKeyword "^\s*\zsno-negcache\>" +syn match DnsmasqKeyword "^\s*\zsno-ping\>" +syn match DnsmasqKeyword "^\s*\zsno-poll\>" +syn match DnsmasqKeyword "^\s*\zsno-resolv\>" +syn match DnsmasqKeyword "^\s*\zsno-round-robin\>" +syn match DnsmasqKeyword "^\s*\zspid-file\>" +syn match DnsmasqKeyword "^\s*\zsport-limit\>" +syn match DnsmasqKeyword "^\s*\zsport\>" +syn match DnsmasqKeyword "^\s*\zsproxy-dnssec\>" +syn match DnsmasqKeyword "^\s*\zsptr-record\>" +syn match DnsmasqKeyword "^\s*\zspxe-prompt\>" +syn match DnsmasqKeyword "^\s*\zspxe-service\>" +syn match DnsmasqKeyword "^\s*\zsquery-port\>" +syn match DnsmasqKeyword "^\s*\zsquiet-dhcp6\>" +syn match DnsmasqKeyword "^\s*\zsquiet-dhcp\>" +syn match DnsmasqKeyword "^\s*\zsquiet-ra\>" +syn match DnsmasqKeyword "^\s*\zsquiet-tftp\>" +syn match DnsmasqKeyword "^\s*\zsra-param\>" +syn match DnsmasqKeyword "^\s*\zsread-ethers\>" +syn match DnsmasqKeyword "^\s*\zsrebind-domain-ok\>" +syn match DnsmasqKeyword "^\s*\zsrebind-localhost-ok\>" +syn match DnsmasqKeyword "^\s*\zsresolv-file\>" +syn match DnsmasqKeyword "^\s*\zsrev-server\>" +syn match DnsmasqKeyword "^\s*\zsscript-arp\>" +syn match DnsmasqKeyword "^\s*\zsscript-on-renewal\>" +syn match DnsmasqKeyword "^\s*\zsselfmx\>" +syn match DnsmasqKeyword "^\s*\zsserver\>" +syn match DnsmasqKeyword "^\s*\zsservers-file\>" +syn match DnsmasqKeyword "^\s*\zsshared-network\>" +syn match DnsmasqKeyword "^\s*\zssrv-host\>" +syn match DnsmasqKeyword "^\s*\zsstop-dns-rebind\>" +syn match DnsmasqKeyword "^\s*\zsstrict-order\>" +syn match DnsmasqKeyword "^\s*\zsstrip-mac\>" +syn match DnsmasqKeyword "^\s*\zsstrip-subnet\>" +syn match DnsmasqKeyword "^\s*\zssynth-domain\>" +syn match DnsmasqKeyword "^\s*\zstag-if\>" +syn match DnsmasqKeyword "^\s*\zstest\>" +syn match DnsmasqKeyword "^\s*\zstftp-lowercase\>" +syn match DnsmasqKeyword "^\s*\zstftp-max\>" +syn match DnsmasqKeyword "^\s*\zstftp-mtu\>" +syn match DnsmasqKeyword "^\s*\zstftp-no-blocksize\>" +syn match DnsmasqKeyword "^\s*\zstftp-no-fail\>" +syn match DnsmasqKeyword "^\s*\zstftp-port-range\>" +syn match DnsmasqKeyword "^\s*\zstftp-root\>" +syn match DnsmasqKeyword "^\s*\zstftp-secure\>" +syn match DnsmasqKeyword "^\s*\zstftp-single-port\>" +syn match DnsmasqKeyword "^\s*\zstftp-unique-root\>" +syn match DnsmasqKeyword "^\s*\zstrust-anchor\>" +syn match DnsmasqKeyword "^\s*\zstxt-record\>" +syn match DnsmasqKeyword "^\s*\zsumbrella\>" +syn match DnsmasqKeyword "^\s*\zsuse-stale-cache\>" +syn match DnsmasqKeyword "^\s*\zsuser\>" +syn match DnsmasqKeyword "^\s*\zsversion\>" if b:dnsmasq_backrgound_light == 1 From 98f5171ef6ba9aa6aea7223e833115e544199bd4 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Tue, 23 Jun 2026 19:44:48 +0000 Subject: [PATCH 16/33] patch 9.2.0708: Leaks in do_autocmd in error case Problem: Leak in do_autocmd in error case (Cheng) Solution: goto err_exit in the error case and clean up, make the double ++once an actual error closes: #20606 Signed-off-by: Christian Brabandt --- src/autocmd.c | 12 ++++++++---- src/testdir/test_autocmd.vim | 33 +++++++++++++++++++++++++++++++++ src/version.c | 2 ++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/autocmd.c b/src/autocmd.c index 2e7f1efd01..98014cf037 100644 --- a/src/autocmd.c +++ b/src/autocmd.c @@ -1025,7 +1025,10 @@ do_autocmd(exarg_T *eap, char_u *arg_in, int forceit) if (STRNCMP(cmd, "++once", 6) == 0 && VIM_ISWHITE(cmd[6])) { if (once) + { semsg(_(e_duplicate_argument_str), "++once"); + goto err_exit; + } once = TRUE; cmd = skipwhite(cmd + 6); } @@ -1036,7 +1039,7 @@ do_autocmd(exarg_T *eap, char_u *arg_in, int forceit) if (nested) { semsg(_(e_duplicate_argument_str), "++nested"); - return; + goto err_exit; } nested = TRUE; cmd = skipwhite(cmd + 8); @@ -1051,12 +1054,12 @@ do_autocmd(exarg_T *eap, char_u *arg_in, int forceit) // be removed and "nested" accepted as the start of the // command. emsg(_(e_invalid_command_nested_did_you_mean_plusplus_nested)); - return; + goto err_exit; } if (nested) { semsg(_(e_duplicate_argument_str), "nested"); - return; + goto err_exit; } nested = TRUE; cmd = skipwhite(cmd + 6); @@ -1075,7 +1078,7 @@ do_autocmd(exarg_T *eap, char_u *arg_in, int forceit) cmd = expand_sfile(cmd); if (cmd == NULL) // some error - return; + goto err_exit; cmd_need_free = TRUE; } } @@ -1111,6 +1114,7 @@ do_autocmd(exarg_T *eap, char_u *arg_in, int forceit) break; } +err_exit: if (cmd_need_free) vim_free(cmd); vim_free(tofree); diff --git a/src/testdir/test_autocmd.vim b/src/testdir/test_autocmd.vim index 54116a5857..4828b3ea9e 100644 --- a/src/testdir/test_autocmd.vim +++ b/src/testdir/test_autocmd.vim @@ -3208,8 +3208,41 @@ func Test_autocmd_once() close call assert_fails('au WinNew * ++once ++once echo bad', 'E983:') + call assert_false(exists('#WinNew')) endfunc +func Test_autocmd_dup_arg() + " Duplicate ++once / ++nested, or the legacy "nested" used twice, must + " error out *and* not create the autocommand. Using an environment + " variable in the pattern also exercises the error-exit path that frees + " the expanded pattern (checked by the address/leak sanitizers). + augroup XdupTest + au! + augroup END + let $XAUTODIR = 'Xfoo' + + " New behavior: duplicate ++once now aborts, the autocmd is not added + call assert_fails('au XdupTest WinNew $XAUTODIR/* ++once ++once echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + call assert_fails('au XdupTest WinNew $XAUTODIR/* ++nested ++nested echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + call assert_fails('au XdupTest WinNew $XAUTODIR/* nested nested echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + " "nested" without "++" is rejected in Vim9 script (also frees the pattern) + call assert_fails('vim9cmd au XdupTest WinNew $XAUTODIR/* nested echo bad', 'E1078:') + call assert_false(exists('#XdupTest#WinNew')) + + augroup XdupTest + au! + augroup END + augroup! XdupTest + let $XAUTODIR = '' +endfunc + + func Test_autocmd_bufreadpre() new let b:bufreadpre = 1 diff --git a/src/version.c b/src/version.c index ff0e68e1c7..753a36d2de 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 708, /**/ 707, /**/ From 2fd83d0ddcaa2120dfc5fe5e7746c7c5e73db92a Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Tue, 23 Jun 2026 19:51:54 +0000 Subject: [PATCH 17/33] patch 9.2.0709: GTK4: a few minor issues Problem: GTK4: a few minor issues Solution: Update docs for 'mouseshape' option, remove unnecessary code, respect "v" flag in 'guioptions' (Foxe Chen) closes: #20609 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- runtime/doc/options.txt | 4 ++-- src/gui_gtk4.c | 21 ++++++++++----------- src/version.c | 2 ++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 9743dea438..7339133c6d 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1,4 +1,4 @@ -*options.txt* For Vim version 9.2. Last change: 2026 Jun 17 +*options.txt* For Vim version 9.2. Last change: 2026 Jun 23 VIM REFERENCE MANUAL by Bram Moolenaar @@ -6458,7 +6458,7 @@ A jump table for the options with a short description can be found at |Q_op|. x any X11 pointer number (see X11/cursorfont.h) The "avail" column contains a 'w' if the shape is available for Win32, - x for X11 (including GTK+ 2), g for GTK+ 3. + x for X11 (including GTK+ 2), g for GTK+ 3 and GTK 4. Any modes not specified or shapes not available use the normal mouse pointer. diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index bcadb31a56..5126f99198 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -2217,8 +2217,9 @@ motion_notify_event(GtkEventControllerMotion *controller UNUSED, // Only unhide if mouse actually moved. GTK seems to send a motion event // when switching tabs, causing the cursor to unhide. - if (p_mh && fabs(prev_mouse_x - x) > 0.05 - && fabs(prev_mouse_y - y) > 0.05) + if (p_mh && ((prev_mouse_x == -1 || prev_mouse_y == -1) + || (fabs(prev_mouse_x - x) > 0.05 + && fabs(prev_mouse_y - y) > 0.05))) gui_mch_mousehide(FALSE); prev_mouse_x = x; @@ -4362,17 +4363,9 @@ mch_set_mouse_shape(int shape) last_shape = shape; } -#else // !FEAT_MOUSESHAPE - - void -mch_set_mouse_shape(int shape UNUSED) -{ -} - #endif // FEAT_MOUSESHAPE - /* * Menus, scrollbars, dialogs, toolbar. * (merged from gui_gtk4.c) @@ -5362,10 +5355,16 @@ gui_mch_dialog( if (buttons != NULL) { - GtkWidget *but_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + GtkWidget *but_box; char **buttons_arr; // Note that array is allocated, not strings int n_buttons; + // Check 'v' flag in 'guioptions': vertical button placement. + if (vim_strchr(p_go, GO_VERTICAL) != NULL) + but_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + else + but_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_halign(but_box, GTK_ALIGN_CENTER); gtk_box_set_homogeneous(GTK_BOX(but_box), TRUE); gtk_box_append(GTK_BOX(vertbox), but_box); diff --git a/src/version.c b/src/version.c index 753a36d2de..3f52cbadff 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 709, /**/ 708, /**/ From 7908164c9d6a5ac7bda34d45681d6f298e32419d Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Tue, 23 Jun 2026 20:03:31 +0000 Subject: [PATCH 18/33] patch 9.2.0710: GTK4 GUI resize handling can be improved Problem: GTK4 GUI resize handling can be improved Solution: Remove the resize debounce, set the draw area's size request in gui_mch_set_text_area_pos() via vim_form_move_resize() (Foxe Chen). reverts: #20327 closes: #20486 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui.c | 15 ++++++ src/gui.h | 4 ++ src/gui_gtk4.c | 114 +++++++++++++++++------------------------ src/gui_gtk4_da.c | 68 ++++++++++-------------- src/gui_gtk4_f.c | 9 +--- src/proto/gui_gtk4.pro | 2 + src/version.c | 2 + 7 files changed, 99 insertions(+), 115 deletions(-) diff --git a/src/gui.c b/src/gui.c index 0cd586114a..0ca09d8eb1 100644 --- a/src/gui.c +++ b/src/gui.c @@ -1625,6 +1625,14 @@ gui_resize_shell(int pixel_width, int pixel_height) gui_position_components(pixel_width); gui_reset_scroll_region(); +#if defined(FEAT_GUI_GTK) && defined(USE_GTK4) && !defined(USE_GTK4_SNAPSHOT) + // We do not resize the draw area via the "resize" signal. This is because + // when the window is resized, the form widget is the one that is resized, + // so let that call gui_resize_shell() which will allocate the surface and + // allocate the drawing area size/position. + gui_gtk4_resize(pixel_width, pixel_height); +#endif + /* * At the "more" and ":confirm" prompt there is no redraw, put the cursor * at the last line here (why does it have to be one row too low?). @@ -1647,6 +1655,9 @@ gui_resize_shell(int pixel_width, int pixel_height) gui_update_scrollbars(TRUE); gui_update_cursor(FALSE, TRUE); +#if defined(FEAT_GUI_GTK) && defined(USE_GTK4_SNAPSHOT) + gui_gtk_calculate_bleed(pixel_width, pixel_height); +#endif #if defined(FEAT_XIM) && !defined(FEAT_GUI_GTK) xim_set_status_area(); #endif @@ -1821,6 +1832,10 @@ gui_set_shellsize( gui_position_components(width); gui_update_scrollbars(TRUE); gui_reset_scroll_region(); + +#if defined(FEAT_GUI_GTK) && defined(USE_GTK4_SNAPSHOT) + gui_gtk_calculate_bleed(width, height); +#endif } /* diff --git a/src/gui.h b/src/gui.h index 674601e0f7..78ad3a09bb 100644 --- a/src/gui.h +++ b/src/gui.h @@ -273,6 +273,10 @@ typedef struct Gui #ifdef FEAT_DIRECTX bool directx_enabled; // DirectX (DirectWrite) rendering active #endif +#if defined(FEAT_GUI_GTK) && defined(USE_GTK4_SNAPSHOT) + int bleed_right; // Number of pixels to bleed bg color right + int bleed_bot; // Number of pixels to bleed bg color down +#endif #ifdef FEAT_MENU # ifndef FEAT_GUI_GTK diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index 5126f99198..7084516d87 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -299,12 +299,9 @@ static void tabline_menu_press_event(GtkGestureClick *gesture, int n_press, doub static void mainwin_destroy_cb(GObject *object, gpointer data); static gboolean delete_event_cb(GtkWindow *window, gpointer data); static void mainwin_fullscreened_cb(GObject *obj, GParamSpec *pspec, gpointer user_data); -#ifndef USE_GTK4_SNAPSHOT static void drawarea_realize_cb(GtkWidget *widget, gpointer data); -#endif static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data); #ifndef USE_GTK4_SNAPSHOT -static void drawarea_resize_cb(GtkDrawingArea *area, int width, int height, gpointer data); static void drawarea_scale_factor_cb(GObject *object, GParamSpec *pspec, gpointer data); static cairo_surface_t *create_backing_surface(int width, int height); #endif @@ -598,13 +595,11 @@ gui_mch_init(void) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(gui.drawarea), (GtkDrawingAreaDrawFunc)draw_event, NULL, NULL); - g_signal_connect(G_OBJECT(gui.drawarea), "resize", - G_CALLBACK(drawarea_resize_cb), NULL); g_signal_connect(G_OBJECT(gui.drawarea), "notify::scale-factor", G_CALLBACK(drawarea_scale_factor_cb), NULL); +#endif g_signal_connect(G_OBJECT(gui.drawarea), "realize", G_CALLBACK(drawarea_realize_cb), NULL); -#endif g_signal_connect(G_OBJECT(gui.drawarea), "unrealize", G_CALLBACK(drawarea_unrealize_cb), NULL); @@ -887,10 +882,10 @@ gui_mch_newfont(void) { int w, h; + // Do not subtract width and height with menubar, toolbar, etc, because + // those are not part of the shell. w = gtk_widget_get_width(gui.formwin); h = gtk_widget_get_height(gui.formwin); - w -= get_menu_tool_width(); - h -= get_menu_tool_height(); gui_resize_shell(w, h); } @@ -968,7 +963,13 @@ gui_mch_get_screen_dimensions(int *screen_w, int *screen_h) gui_mch_enable_menu(int showit) { if (gui.menubar != NULL) + { gtk_widget_set_visible(gui.menubar, showit); + // Draw area might become blank after this for some reason, queue a + // redraw, same for toolbar as well. + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); + } } #endif @@ -982,6 +983,8 @@ gui_mch_show_toolbar(int showit) if (showit) vim_toolbar_set_style(VIM_TOOLBAR(gui.toolbar), toolbar_flags, tbis_flags); + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); } } #endif @@ -2339,10 +2342,10 @@ menubar_popover_closed_hook(GSignalInvocationHint *ihint UNUSED, } #endif -#ifndef USE_GTK4_SNAPSHOT static void drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) { +#ifndef USE_GTK4_SNAPSHOT int w, h; // Use formwin size since drawarea may not have its final size yet @@ -2363,10 +2366,10 @@ drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) if (gui.surface != NULL) cairo_surface_destroy(gui.surface); gui.surface = create_backing_surface(w, h); +#endif gui_mch_new_colors(); } -#endif static void drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) @@ -2384,42 +2387,8 @@ drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) } #ifndef USE_GTK4_SNAPSHOT -// Debounced resize: drawarea_resize_cb only resizes the backing surface -// (preserving old content) and (re)arms a short timeout. The actual -// gui_resize_shell() runs from drawarea_resize_apply_cb once the user has -// stopped dragging for ~100 ms, by which time no input is pending and -// update_screen() will not bail in screenclear()'s wake. -static guint drawarea_resize_timeout_id = 0; -static int drawarea_resize_pending_w = 0; -static int drawarea_resize_pending_h = 0; - - static gboolean -drawarea_resize_apply_cb(gpointer data UNUSED) -{ - int width = drawarea_resize_pending_w; - int height = drawarea_resize_pending_h; - - drawarea_resize_timeout_id = 0; - - if (width <= 0 || height <= 0) - return G_SOURCE_REMOVE; - if (updating_screen) - { - drawarea_resize_timeout_id = g_timeout_add(50, - drawarea_resize_apply_cb, NULL); - return G_SOURCE_REMOVE; - } - - gui.force_redraw = TRUE; - gui_resize_shell(width, height); - if (gui.in_use) - redraw_all_later(UPD_CLEAR); - return G_SOURCE_REMOVE; -} - - static void -drawarea_resize_cb(GtkDrawingArea *area UNUSED, int width, int height, - gpointer data UNUSED) + void +gui_gtk4_resize(int width, int height) { cairo_t *cr; cairo_surface_t *old_surface; @@ -2428,9 +2397,6 @@ drawarea_resize_cb(GtkDrawingArea *area UNUSED, int width, int height, if (width <= 0 || height <= 0) return; - drawarea_resize_pending_w = width; - drawarea_resize_pending_h = height; - // Keep the backing surface in sync with the drawing area so GTK keeps // showing the previous frame. Re-creating it preserves the old // contents. @@ -2465,13 +2431,6 @@ drawarea_resize_cb(GtkDrawingArea *area UNUSED, int width, int height, cairo_destroy(cr); } } - - // Debounce: (re)arm the apply timeout, so gui_resize_shell() only - // runs once the resize stream settles. - if (drawarea_resize_timeout_id != 0) - g_source_remove(drawarea_resize_timeout_id); - drawarea_resize_timeout_id = g_timeout_add(100, - drawarea_resize_apply_cb, NULL); } static void @@ -4977,20 +4936,41 @@ gui_mch_update_scrollbar_size(void) void gui_mch_set_text_area_pos(int x, int y, int w, int h) { + // "h" may be negative especially when draw area size is smaller than + // "gui.char_height". + if (w <= 0 || h <= 0) + return; last_text_area_w = w; last_text_area_h = h; - // Don't use vim_form_move_resize for drawarea because its - // set_size_request would prevent the window from shrinking. - // Just update position; the actual allocation is handled by - // vim_form_size_allocate which gives drawarea the formwin's full size. - vim_form_move(VIM_FORM(gui.formwin), gui.drawarea, x, y); - - // Surface sizing is owned by drawarea_resize_cb; don't recreate it - // here. Recreating on every text-area change wiped any preserved - // content whenever a sub-cell resize shifted the cell grid, and - // update_screen() may bail (char_avail()) during a drag and leave - // the fresh surface blank. + + vim_form_move_resize(VIM_FORM(gui.formwin), gui.drawarea, x, y, w, h); +} + +#ifdef USE_GTK4_SNAPSHOT +/* + * Calculate the number of pixels to bleed background color to. Should be called + * after all UI elements are positioned and resized. + */ + void +gui_gtk_calculate_bleed(int width, int height) +{ + gui.bleed_right = width - last_text_area_w; + gui.bleed_bot = height - last_text_area_h; + + if (gui.which_scrollbars[SBAR_LEFT]) + gui.bleed_right -= gui.scrollbar_width; + if (gui.which_scrollbars[SBAR_RIGHT]) + gui.bleed_right -= gui.scrollbar_width; + if (gui.which_scrollbars[SBAR_BOTTOM]) + gui.bleed_bot -= gui.scrollbar_height; + + // Not sure if this can happen, but be safe... + if (gui.bleed_right < 0) + gui.bleed_right = 0; + if (gui.bleed_bot < 0) + gui.bleed_bot = 0; } +#endif /* * ============================================================ diff --git a/src/gui_gtk4_da.c b/src/gui_gtk4_da.c index 34dad93a61..11b7463892 100644 --- a/src/gui_gtk4_da.c +++ b/src/gui_gtk4_da.c @@ -75,7 +75,7 @@ struct _VimDrawArea int n_rows; int n_cols; - int resize_count; + int bleed_right; // Used for hollow and part style cursors. For the block cursor, that is // simply rendered as a cell using vim_draw_area_add_glyphs(). May be NULL. @@ -107,7 +107,6 @@ G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET) static void draw_image_free(DrawImage *dimg); #endif static void vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot); -static void vim_draw_area_size_allocate(GtkWidget *widget, int width, int height, int baseline); static void vim_draw_area_finalize(GObject *obj) @@ -138,10 +137,10 @@ vim_draw_area_class_init(VimDrawAreaClass *class) GObjectClass *obj_class = G_OBJECT_CLASS(class); widget_class->snapshot = vim_draw_area_snapshot; - widget_class->size_allocate = vim_draw_area_size_allocate; obj_class->finalize = vim_draw_area_finalize; + gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT); } static void @@ -178,7 +177,6 @@ vim_draw_area_set_size(VimDrawArea *self, int rows, int cols) self->n_cols = cols; self->cells = g_realloc_n(self->cells, rows * cols, sizeof(DrawCell)); memset(self->cells, 0, rows * (sizeof(DrawCell) * cols)); - self->resize_count++; } static void @@ -574,14 +572,17 @@ draw_node_render(DrawNode *dnode, int row, VimDrawArea *da) if (!(dnode->dnode_flags & DRAW_NODE_NOBG)) { int width = dnode->n_cells * gui.char_width; - int bleed = gtk_widget_get_width(GTK_WIDGET(da)) - FILL_X(da->n_cols); // If this draw node touches the end of the draw area. Bleed its // background to the right if the space the draw area covers is slightly // bigger than its actual visible area (that all cells cover). This just // makes things like status bars look a bit nicer - if (END_COL(dnode) == da->n_cols - 1 && bleed > 0) - width += bleed; + // + // Don't do this for the bottom, because that will make the cursor in + // the cmdline look weird. Instead only bleed downwards when drawing the + // global background color (see vim_draw_area_snapshot()) + if (END_COL(dnode) == da->n_cols - 1) + width += gui.bleed_right; nodes[n_nodes++] = gsk_color_node_new(&dnode->bg_color, &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row), @@ -1475,8 +1476,8 @@ vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) garray_T invert_ga; gui_mch_set_bg_color(gui.back_pixel); - height = gtk_widget_get_height(widget); - width = gtk_widget_get_width(widget); + height = gtk_widget_get_height(widget) + gui.bleed_bot; + width = gtk_widget_get_width(widget) + gui.bleed_right; if (self->cells == NULL) { @@ -1485,6 +1486,23 @@ vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) return; } + // If number of pixels to bleed has changed, then dirty the nodes at the + // right edge of the draw area. + if (self->bleed_right != gui.bleed_right) + { + self->bleed_right = gui.bleed_right; + for (int r = 0; r < self->n_rows; r++) + { + DrawCell *dcell = &GET_ROW(self, r)[self->n_cols - 1]; + + if (dcell->dnode != NULL) + { + (void)draw_node_make_dirty(dcell->dnode); + draw_node_render(dcell->dnode, r, self); + } + } + } + // For inverted cells, we first build an array of bounds that represent // blocks of inverted cells. Then we apply a white color to each of those // bounds and then finish the blend. @@ -1565,36 +1583,4 @@ vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) #endif } - static void -vim_draw_area_size_allocate( - GtkWidget *widget, - int width, - int height, - int baseline UNUSED) -{ - VimDrawArea *self = VIM_DRAW_AREA(widget); - int old_count = self->resize_count; - - gui_resize_shell(width, height); - - if (old_count == self->resize_count) - { - // Number of columns or rows hasn't changed. However still re render the - // draw nodes at the right edge of the draw area, so that they can - // update their background bleed (see draw_node_render()). - for (int r = 0; r < self->n_rows; r++) - { - DrawCell *dcell = &GET_ROW(self, r)[self->n_cols - 1]; - - if (dcell->dnode != NULL) - { - (void)draw_node_make_dirty(dcell->dnode); - draw_node_render(dcell->dnode, r, self); - } - } - } - - return; -} - #endif // USE_GTK4_SNAPSHOT diff --git a/src/gui_gtk4_f.c b/src/gui_gtk4_f.c index 52941874fb..29c17aa6d5 100644 --- a/src/gui_gtk4_f.c +++ b/src/gui_gtk4_f.c @@ -209,18 +209,13 @@ vim_form_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) static gboolean vim_form_resize_idle_cb(VimForm *self) { - int w, h; - self->resize_idle_id = 0; - // Use drawarea's actual allocation, not formwin's if (gui.drawarea == NULL) goto exit; - w = gtk_widget_get_width(gui.drawarea); - h = gtk_widget_get_height(gui.drawarea); - if (w > 1 && h > 1) - gui_resize_shell(w, h); + if (self->last_width > 1 && self->last_height > 1) + gui_resize_shell(self->last_width, self->last_height); exit: g_object_unref(self); diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro index acfb13c9a5..eaf8eb2686 100644 --- a/src/proto/gui_gtk4.pro +++ b/src/proto/gui_gtk4.pro @@ -52,6 +52,7 @@ void gui_mch_draw_hollow_cursor(guicolor_T color); void gui_mch_draw_part_cursor(int w, int h, guicolor_T color); void gui_mch_flash(int msec); void gui_mch_invert_rectangle(int r, int c, int nr, int nc); +void gui_gtk4_resize(int width, int height); void gui_mch_update(void); int gui_mch_wait_for_chars(long wtime); void gui_mch_flush(void); @@ -103,6 +104,7 @@ void gui_mch_create_scrollbar(scrollbar_T *sb, int orient); void gui_mch_destroy_scrollbar(scrollbar_T *sb); void gui_mch_update_scrollbar_size(void); void gui_mch_set_text_area_pos(int x, int y, int w, int h); +void gui_gtk_calculate_bleed(int width, int height); char_u *gui_mch_browse(int saving, char_u *title, char_u *dflt, char_u *ext, char_u *initdir, char_u *filter); char_u *gui_mch_browsedir(char_u *title, char_u *initdir); int gui_mch_dialog(int type, char_u *title, char_u *message, char_u *buttons, int def_but, char_u *textfield, int ex_cmd); diff --git a/src/version.c b/src/version.c index 3f52cbadff..7433593c77 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 710, /**/ 709, /**/ From fa3680c6fa93b6bb2c96ae536f56c56750477cfc Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Tue, 23 Jun 2026 20:16:49 +0000 Subject: [PATCH 19/33] patch 9.2.0711: leak in ins_compl_infercase_gettext() in error case Problem: leak in ins_compl_infercase_gettext() in error case (Cheng) Solution: free wca before returning. closes: #20607 Signed-off-by: Christian Brabandt --- src/insexpand.c | 1 + src/version.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/insexpand.c b/src/insexpand.c index c7186b9297..3b20d2a3c7 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -699,6 +699,7 @@ ins_compl_infercase_gettext( if (ga_grow(&gap, 10) == FAIL) { ga_clear(&gap); + vim_free(wca); return (char_u *)"[failed]"; } p = (char_u *)gap.ga_data + gap.ga_len; diff --git a/src/version.c b/src/version.c index 7433593c77..c3ae1b07d9 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 711, /**/ 710, /**/ From 5c1b989b4aabf1549910752dcfb44030e64edfcc Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Tue, 23 Jun 2026 20:32:43 +0000 Subject: [PATCH 20/33] patch 9.2.0712: GTK4: dialogs not handling mnemonics correctly Problem: GTK4: dialogs not handling mnemonics correctly Solution: Allow using mnemonics without alt key (Foxe Chen). closes: #20618 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui_gtk4.c | 48 ++++++++++++++++++++++++++++++++++++++++++------ src/version.c | 2 ++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index 7084516d87..fe9381f2f6 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -5209,6 +5209,13 @@ typedef struct gboolean *done; } DialogButtonState; +typedef struct +{ + GtkWidget *win; + gboolean *done; + gboolean no_alt; +} DialogState; + static void dialog_button_clicked_cb(GtkButton *button, DialogButtonState *state) { @@ -5222,13 +5229,16 @@ dialog_key_pressed_cb( guint keyval, guint keycode, GdkModifierType state, - gboolean *done) + DialogState *dstate) { if (keyval == GDK_KEY_Escape) { - *done = TRUE; + *dstate->done = TRUE; return TRUE; } + + if (dstate->no_alt && !(state & gtk_accelerator_get_default_mod_mask())) + return gtk_widget_mnemonic_activate(dstate->win, FALSE); return FALSE; } @@ -5264,6 +5274,7 @@ gui_mch_dialog( int response = -1; gboolean done = FALSE; gboolean win_closed = FALSE; + DialogState state; utf8_title = CONVERT_TO_UTF8(title); if (utf8_title != NULL) @@ -5306,13 +5317,14 @@ gui_mch_dialog( gtk_label_set_max_width_chars(GTK_LABEL(label), 40); gtk_box_append(GTK_BOX(message_box), label); - // Close the dialog when the key is pressed. the GTK3 GUI also allows - // mnemonics without key, but that behaviour comes from GTK+ 1.2 (from - // 1999!), so most users probably don't care... + // Close the dialog when the key is pressed. Also allow using + // mnemonics without key (if there is no text field). key_controller = gtk_event_controller_key_new(); g_signal_connect(key_controller, "key-pressed", - G_CALLBACK(dialog_key_pressed_cb), &done); + G_CALLBACK(dialog_key_pressed_cb), &state); gtk_widget_add_controller(GTK_WIDGET(win), key_controller); + state.done = &done; + state.win = GTK_WIDGET(win); if (textfield != NULL) { @@ -5331,6 +5343,30 @@ gui_mch_dialog( // (which is set as the default widget). gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); gtk_box_append(GTK_BOX(vertbox), entry); + state.no_alt = FALSE; + } + else + { + GListModel *controllers; + int len; + + // Set all shortcut controllers in the window to not require a modifier + // for mnemonics. + controllers = gtk_widget_observe_controllers(GTK_WIDGET(win)); + len = g_list_model_get_n_items(controllers); + for (int i = 0; i < len; i++) + { + GtkEventController *controller; + + controller = g_list_model_get_item(controllers, i); + if (GTK_IS_SHORTCUT_CONTROLLER(controller)) + gtk_shortcut_controller_set_mnemonics_modifiers( + GTK_SHORTCUT_CONTROLLER(controller), 0); + } + g_object_unref(controllers); + + gtk_window_set_mnemonics_visible(win, TRUE); + state.no_alt = TRUE; } if (buttons != NULL) diff --git a/src/version.c b/src/version.c index c3ae1b07d9..b03ecdf307 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 712, /**/ 711, /**/ From 7948a2c3d8b6971663a6a9a34a35653aa93db09e Mon Sep 17 00:00:00 2001 From: Hirohito Higashi Date: Tue, 23 Jun 2026 21:02:46 +0000 Subject: [PATCH 21/33] patch 9.2.0713: completion: ruler not updated correctly when the popup menu is visible Problem: While the insert-mode completion popup menu is visible, the ruler - and the ruler shown in a status line when 'laststatus' is set - shows the column where the completion started instead of the real cursor column. With a status line the ruler can also stay at the column from before the completion until the next key is pressed. Solution: Position the popup menu at the completion start column without moving the cursor there, so the ruler keeps reflecting the real cursor column. Also mark the status line for redraw before the menu is shown, so its ruler is updated for the real cursor column while the menu is visible. closes: #20626 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- src/cmdexpand.c | 2 +- src/insexpand.c | 23 ++++++++----------- src/popupmenu.c | 18 +++++++-------- src/proto/popupmenu.pro | 2 +- .../dumps/Test_autocompletedelay_1.dump | 2 +- .../dumps/Test_autocompletedelay_4.dump | 2 +- .../Test_autocompletedelay_longest_2.dump | 2 +- .../Test_autocompletedelay_longest_4.dump | 2 +- .../Test_autocompletedelay_preinsert_2.dump | 2 +- .../dumps/Test_fuzzy_autocompletedelay_1.dump | 2 +- .../dumps/Test_fuzzy_autocompletedelay_2.dump | 2 +- ...fo_popupwin_clears_cmdline_on_hide_01.dump | 2 +- .../Test_popup_and_previewwindow_pbuffer.dump | 2 +- .../Test_popup_and_previewwindow_pedit.dump | 2 +- src/testdir/dumps/Test_pum_highlights_09.dump | 2 +- src/testdir/dumps/Test_pum_matchins_11.dump | 2 +- .../dumps/Test_pum_statusline_ruler_1.dump | 8 +++++++ .../dumps/Test_pum_with_preview_win.dump | 2 +- .../Test_pum_with_special_characters_13.dump | 2 +- .../dumps/Test_pumopt_opacity_text_attrs.dump | 2 +- ...est_pumopt_opacity_textprop_undercurl.dump | 2 +- .../dumps/Test_pumopt_opacity_wide_bg.dump | 2 +- .../Test_pumopt_opacity_wide_bg_shifted.dump | 2 +- .../dumps/Test_shortmess_complmsg_2.dump | 2 +- src/testdir/dumps/Test_winhighlight_8.dump | 2 +- src/testdir/dumps/Test_winhighlight_9.dump | 2 +- src/testdir/test_ins_complete.vim | 18 +++++++++++++++ src/version.c | 2 ++ 28 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 src/testdir/dumps/Test_pum_statusline_ruler_1.dump diff --git a/src/cmdexpand.c b/src/cmdexpand.c index 496ab7b883..961ced5773 100644 --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -458,7 +458,7 @@ cmdline_pum_display(void) { if (p_po > 0 && p_po < 100 && !pum_redraw_in_same_position()) pum_call_update_screen(); - pum_display(compl_match_array, compl_match_arraysize, compl_selected); + pum_display(compl_match_array, compl_match_arraysize, compl_selected, -1); } /* diff --git a/src/insexpand.c b/src/insexpand.c index 3b20d2a3c7..f5ad940081 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -1889,22 +1889,19 @@ ins_compl_show_pum(void) // part of the screen would be updated. We do need to redraw here. dollar_vcol = -1; - // Compute the screen column of the start of the completed text. - // Use the cursor to get all wrapping and other settings right. + // Position the menu at the completion start without moving the cursor + // there, so the ruler keeps showing the real cursor column. col = curwin->w_cursor.col; curwin->w_cursor.col = compl_col; - compl_selected_item = cur; - pum_display(compl_match_array, compl_match_arraysize, cur); + validate_cursor_col(); + int pum_wcol = curwin->w_wcol; curwin->w_cursor.col = col; - -#ifdef FEAT_CONCEAL - // The cursor was temporarily moved to "compl_col" above to position the - // menu, so the screen update left w_wcol conceal-corrected for that column - // rather than for the real cursor. Redraw the cursor line so the caret is - // positioned correctly when the cursor line has concealed text. - if (curwin->w_p_cole > 0 && conceal_cursor_line(curwin)) - redrawWinline(curwin, curwin->w_cursor.lnum); -#endif + validate_cursor_col(); + compl_selected_item = cur; + // Flag the status line so the ruler is redrawn for the real cursor column + // when the menu update redraws the screen. + curwin->w_redr_status = true; + pum_display(compl_match_array, compl_match_arraysize, cur, pum_wcol); // After adding leader, set the current match to shown match. if (compl_started && compl_curr_match != compl_shown_match) diff --git a/src/popupmenu.c b/src/popupmenu.c index bed2495b87..08aa442865 100644 --- a/src/popupmenu.c +++ b/src/popupmenu.c @@ -290,8 +290,10 @@ pum_compute_horizontal_placement(int cursor_col) pum_display( pumitem_T *array, int size, - int selected) // index of initially selected item, -1 if + int selected, // index of initially selected item, -1 if // out of range + int pum_wcol) // screen column to align the menu to, or -1 + // to use the cursor column { int cursor_col; int above_row; @@ -326,7 +328,7 @@ pum_display( pum_win_row = curwin->w_wrow + W_WINROW(curwin); pum_win_height = curwin->w_height; pum_win_col = curwin->w_wincol; - pum_win_wcol = curwin->w_wcol; + pum_win_wcol = pum_wcol >= 0 ? pum_wcol : curwin->w_wcol; pum_win_width = curwin->w_width; #if defined(FEAT_QUICKFIX) @@ -359,8 +361,9 @@ pum_display( cursor_col = cmdline_compl_startcol(); else { - // w_wcol includes virtual text "above" - int wcol = curwin->w_wcol % curwin->w_width; + int wcol = pum_wcol >= 0 ? pum_wcol : curwin->w_wcol; + // w_wcol includes virtual text "above". + wcol %= curwin->w_width; #ifdef FEAT_CONCEAL // w_wcol does not account for text concealed before the cursor; // shift by the offset win_line() recorded for the cursor line so the @@ -1671,16 +1674,11 @@ pum_may_redraw(void) } else { - int wcol = curwin->w_wcol; - // Window layout changed, recompute the position. // Use the remembered w_wcol value, the cursor may have moved when a // completion was inserted, but we want the menu in the same position. pum_undisplay(); - curwin->w_wcol = pum_win_wcol; - curwin->w_valid |= VALID_WCOL; - pum_display(array, len, selected); - curwin->w_wcol = wcol; + pum_display(array, len, selected, pum_win_wcol); } } diff --git a/src/proto/popupmenu.pro b/src/proto/popupmenu.pro index 4afd678125..f7a00511cf 100644 --- a/src/proto/popupmenu.pro +++ b/src/proto/popupmenu.pro @@ -2,7 +2,7 @@ void pum_set_border(int enable); void pum_set_shadow(int enable); void pum_set_margin(int enable); -void pum_display(pumitem_T *array, int size, int selected); +void pum_display(pumitem_T *array, int size, int selected, int pum_wcol); void pum_call_update_screen(void); int pum_under_menu(int row, int col, int only_redrawing); void pum_opacity_changed(void); diff --git a/src/testdir/dumps/Test_autocompletedelay_1.dump b/src/testdir/dumps/Test_autocompletedelay_1.dump index 51128fc2f3..c792e09ea9 100644 --- a/src/testdir/dumps/Test_autocompletedelay_1.dump +++ b/src/testdir/dumps/Test_autocompletedelay_1.dump @@ -7,4 +7,4 @@ |f+0#0000001#ffd7ff255|o@1| @11| +0#4040ff13#ffffff0@59 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|T|o|p| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|2| @10|A|l@1| diff --git a/src/testdir/dumps/Test_autocompletedelay_4.dump b/src/testdir/dumps/Test_autocompletedelay_4.dump index 53199f9d60..31d58c55c1 100644 --- a/src/testdir/dumps/Test_autocompletedelay_4.dump +++ b/src/testdir/dumps/Test_autocompletedelay_4.dump @@ -7,4 +7,4 @@ |f+0#0000001#ffd7ff255|o@1| @11| +0#4040ff13#ffffff0@59 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|3| @10|A|l@1| diff --git a/src/testdir/dumps/Test_autocompletedelay_longest_2.dump b/src/testdir/dumps/Test_autocompletedelay_longest_2.dump index 19a83bfa78..8d7eabd605 100644 --- a/src/testdir/dumps/Test_autocompletedelay_longest_2.dump +++ b/src/testdir/dumps/Test_autocompletedelay_longest_2.dump @@ -7,4 +7,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|T|o|p| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|6| @10|A|l@1| diff --git a/src/testdir/dumps/Test_autocompletedelay_longest_4.dump b/src/testdir/dumps/Test_autocompletedelay_longest_4.dump index 09a8514da0..95a4857bd2 100644 --- a/src/testdir/dumps/Test_autocompletedelay_longest_4.dump +++ b/src/testdir/dumps/Test_autocompletedelay_longest_4.dump @@ -7,4 +7,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|3| @10|A|l@1| diff --git a/src/testdir/dumps/Test_autocompletedelay_preinsert_2.dump b/src/testdir/dumps/Test_autocompletedelay_preinsert_2.dump index 17252dfa3e..6b6520925e 100644 --- a/src/testdir/dumps/Test_autocompletedelay_preinsert_2.dump +++ b/src/testdir/dumps/Test_autocompletedelay_preinsert_2.dump @@ -7,4 +7,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|3| @10|A|l@1| diff --git a/src/testdir/dumps/Test_fuzzy_autocompletedelay_1.dump b/src/testdir/dumps/Test_fuzzy_autocompletedelay_1.dump index ed15dc9627..b82983f94b 100644 --- a/src/testdir/dumps/Test_fuzzy_autocompletedelay_1.dump +++ b/src/testdir/dumps/Test_fuzzy_autocompletedelay_1.dump @@ -7,4 +7,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|T|o|p| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|3| @10|A|l@1| diff --git a/src/testdir/dumps/Test_fuzzy_autocompletedelay_2.dump b/src/testdir/dumps/Test_fuzzy_autocompletedelay_2.dump index 84083f6634..550f1e154c 100644 --- a/src/testdir/dumps/Test_fuzzy_autocompletedelay_2.dump +++ b/src/testdir/dumps/Test_fuzzy_autocompletedelay_2.dump @@ -7,4 +7,4 @@ |v+0#0000001#ffd7ff255|i|m| @11| +0#4040ff13#ffffff0@59 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|2| @10|A|l@1| diff --git a/src/testdir/dumps/Test_info_popupwin_clears_cmdline_on_hide_01.dump b/src/testdir/dumps/Test_info_popupwin_clears_cmdline_on_hide_01.dump index 27947cd33f..b098b70f6d 100644 --- a/src/testdir/dumps/Test_info_popupwin_clears_cmdline_on_hide_01.dump +++ b/src/testdir/dumps/Test_info_popupwin_clears_cmdline_on_hide_01.dump @@ -12,4 +12,4 @@ |f+0#0000001#ffd7ff255|o|u|r| @10| +0#0000000#ffffff0@59 |f+0#0000001#e0e0e08|i|v|e| @11|o|n|e| @2| +0#0000000#ffffff0@52 |f|i|v|e> @10| +0#0000001#e0e0e08|t|w|o| @2| +0#0000000#ffffff0@52 -|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@2| +0#0000001#e0e0e08|t|h|r|e@1| | +0#0000000#ffffff0@34|1|6|,|1| @9|B|o|t| +|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@2| +0#0000001#e0e0e08|t|h|r|e@1| | +0#0000000#ffffff0@34|1|6|,|5| @9|B|o|t| diff --git a/src/testdir/dumps/Test_popup_and_previewwindow_pbuffer.dump b/src/testdir/dumps/Test_popup_and_previewwindow_pbuffer.dump index 0eab2a62c6..e01908c6b1 100644 --- a/src/testdir/dumps/Test_popup_and_previewwindow_pbuffer.dump +++ b/src/testdir/dumps/Test_popup_and_previewwindow_pbuffer.dump @@ -16,5 +16,5 @@ |a|b|0> @71 |~+0#4040ff13&| @73 |~| @73 -|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1@1|,|1| @10|B|o|t +|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1@1|,|4| @10|B|o|t |-+2&&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |1|0| +0#0000000&@26 diff --git a/src/testdir/dumps/Test_popup_and_previewwindow_pedit.dump b/src/testdir/dumps/Test_popup_and_previewwindow_pedit.dump index b235c7d511..fe27c9d6c1 100644 --- a/src/testdir/dumps/Test_popup_and_previewwindow_pedit.dump +++ b/src/testdir/dumps/Test_popup_and_previewwindow_pedit.dump @@ -16,5 +16,5 @@ |a+0#0000001#ffd7ff255|b|6| @11| +0#0000000#a8a8a8255| +0&#ffffff0@58 |a|b|0> @71 |~+0#4040ff13&| @73 -|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1@1|,|1| @10|B|o|t +|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1@1|,|4| @10|B|o|t |-+2&&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |1|0| +0#0000000&@26 diff --git a/src/testdir/dumps/Test_pum_highlights_09.dump b/src/testdir/dumps/Test_pum_highlights_09.dump index 4e7d08b793..552f3361a2 100644 --- a/src/testdir/dumps/Test_pum_highlights_09.dump +++ b/src/testdir/dumps/Test_pum_highlights_09.dump @@ -17,4 +17,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|2| @10|A|l@1| diff --git a/src/testdir/dumps/Test_pum_matchins_11.dump b/src/testdir/dumps/Test_pum_matchins_11.dump index a44a6ee566..d11ba8c048 100644 --- a/src/testdir/dumps/Test_pum_matchins_11.dump +++ b/src/testdir/dumps/Test_pum_matchins_11.dump @@ -16,5 +16,5 @@ |~| @73 |~| @73 |~| @73 -|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1|,|1| @11|A|l@1 +|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1|,|4| @11|A|l@1 |-+2&&@1| |O|m|n|i| |c|o|m|p|l|e|t|i|o|n| |(|^|O|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |3| +0#0000000&@34 diff --git a/src/testdir/dumps/Test_pum_statusline_ruler_1.dump b/src/testdir/dumps/Test_pum_statusline_ruler_1.dump new file mode 100644 index 0000000000..43de9b69f4 --- /dev/null +++ b/src/testdir/dumps/Test_pum_statusline_ruler_1.dump @@ -0,0 +1,8 @@ +|a+0&#ffffff0@1| |a@2| |a@1> @30 +|~+0#4040ff13&| @4| +0#0000001#e0e0e08|a@1| @12| +0#4040ff13#ffffff0@17 +|~| @4| +0#0000001#ffd7ff255|a@2| @11| +0#4040ff13#ffffff0@17 +|~| @38 +|~| @38 +|~| @38 +|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @8|1|,|1|0| @10|A|l@1 +|-+2&&@1| |m+0#00e0003&|a|t|c|h| |1| |o|f| |2| +0#0000000&@24 diff --git a/src/testdir/dumps/Test_pum_with_preview_win.dump b/src/testdir/dumps/Test_pum_with_preview_win.dump index ad0df78146..d2487bfdb6 100644 --- a/src/testdir/dumps/Test_pum_with_preview_win.dump +++ b/src/testdir/dumps/Test_pum_with_preview_win.dump @@ -8,5 +8,5 @@ |t+0#0000001#ffd7ff255|h|r|e@1| @9| +0#4040ff13#ffffff0@59 |~| @73 |~| @73 -|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1|,|1| @11|A|l@1 +|[+3#0000000&|N|o| |N|a|m|e|]| |[|+|]| @43|1|,|4| @11|A|l@1 |-+2&&@1| |O|m|n|i| |c|o|m|p|l|e|t|i|o|n| |(|^|O|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |3| +0#0000000&@34 diff --git a/src/testdir/dumps/Test_pum_with_special_characters_13.dump b/src/testdir/dumps/Test_pum_with_special_characters_13.dump index 313d500783..3ed2c9573d 100644 --- a/src/testdir/dumps/Test_pum_with_special_characters_13.dump +++ b/src/testdir/dumps/Test_pum_with_special_characters_13.dump @@ -9,4 +9,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|5| @10|A|l@1| diff --git a/src/testdir/dumps/Test_pumopt_opacity_text_attrs.dump b/src/testdir/dumps/Test_pumopt_opacity_text_attrs.dump index 62e7df726b..1d708e641b 100644 --- a/src/testdir/dumps/Test_pumopt_opacity_text_attrs.dump +++ b/src/testdir/dumps/Test_pumopt_opacity_text_attrs.dump @@ -17,4 +17,4 @@ |ほ*&|げ|ほ|げ|ほ|げ|漢*4#e000e06&|字|テ|ス|ト|あ*0#0000000&|い|う|え|お|カ|タ|カ|ナ| +&@34 |ほ*&|げ|ほ|げ|ほ|げ|漢*4#e000e06&|字|テ|ス|ト|あ*0#0000000&|い|う|え|お|カ|タ|カ|ナ| +&@34 |ほ*&|げ|ほ|げ|ほ|げ|漢*4#e000e06&|字|テ|ス|ト|あ*0#0000000&|い|う|え|お|カ|タ|カ|ナ| +&@34 -|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1| @10|T|o|p| +|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|7|-|5| @8|T|o|p| diff --git a/src/testdir/dumps/Test_pumopt_opacity_textprop_undercurl.dump b/src/testdir/dumps/Test_pumopt_opacity_textprop_undercurl.dump index a1242fde84..2558f5bf1a 100644 --- a/src/testdir/dumps/Test_pumopt_opacity_textprop_undercurl.dump +++ b/src/testdir/dumps/Test_pumopt_opacity_textprop_undercurl.dump @@ -17,4 +17,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1|3| @9|A|l@1| diff --git a/src/testdir/dumps/Test_pumopt_opacity_wide_bg.dump b/src/testdir/dumps/Test_pumopt_opacity_wide_bg.dump index 6b9bb9935a..23c67a84cf 100644 --- a/src/testdir/dumps/Test_pumopt_opacity_wide_bg.dump +++ b/src/testdir/dumps/Test_pumopt_opacity_wide_bg.dump @@ -17,4 +17,4 @@ |ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@34 |ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@34 |ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@34 -|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1| @10|T|o|p| +|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|7|-|5| @8|T|o|p| diff --git a/src/testdir/dumps/Test_pumopt_opacity_wide_bg_shifted.dump b/src/testdir/dumps/Test_pumopt_opacity_wide_bg_shifted.dump index f67556307b..4aee9cd6aa 100644 --- a/src/testdir/dumps/Test_pumopt_opacity_wide_bg_shifted.dump +++ b/src/testdir/dumps/Test_pumopt_opacity_wide_bg_shifted.dump @@ -17,4 +17,4 @@ |a|ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@33 |ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@34 |a|ほ*&|げ|ほ|げ|ほ|げ|漢|字|テ|ス|ト|あ|い|う|え|お|カ|タ|カ|ナ| +&@33 -|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|1| @10|T|o|p| +|-+2&&@1| |I|N|S|E|R|T| |-@1| +0&&@44|1|,|7|-|5| @8|T|o|p| diff --git a/src/testdir/dumps/Test_shortmess_complmsg_2.dump b/src/testdir/dumps/Test_shortmess_complmsg_2.dump index 66c421b59a..99d03a4ec7 100644 --- a/src/testdir/dumps/Test_shortmess_complmsg_2.dump +++ b/src/testdir/dumps/Test_shortmess_complmsg_2.dump @@ -9,4 +9,4 @@ |~| @73 |~| @73 |~| @73 -|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|A|l@1| +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|6| @10|A|l@1| diff --git a/src/testdir/dumps/Test_winhighlight_8.dump b/src/testdir/dumps/Test_winhighlight_8.dump index 69568fc95c..c5ff052503 100644 --- a/src/testdir/dumps/Test_winhighlight_8.dump +++ b/src/testdir/dumps/Test_winhighlight_8.dump @@ -4,5 +4,5 @@ |w+0#0000001#ffd7ff255|a+0#ffffff16#e000002|l@1| @10| +0#0000000#a8a8a8255| +0&#ffffff0@20||+1&&|S+0&&|i|x| @33 |w+0#0000001#ffd7ff255|h+0#ffffff16#e000002|i|l|e| @9| +0#0000000#a8a8a8255| +0#4040ff13#ffffff0@20||+1#0000000&|~+0#4040ff13&| @35 |w+0#0000001#ffd7ff255|i+0#ffffff16#e000002|n|c|m|d| @8| +0#0000000#a8a8a8255| +0#4040ff13#ffffff0@20||+1#0000000&|~+0#4040ff13&| @35 -|w+0#0000001#ffd7ff255|i+0#ffffff16#e000002|n|d|o| @9| +0#0000000#a8a8a8255| +3&#ffffff0@2|1|,|1| @11|A|l@1| |[+1&&|N|o| |N|a|m|e|]| |[|+|]| @5|1|,|1| @11|A|l@1 +|w+0#0000001#ffd7ff255|i+0#ffffff16#e000002|n|d|o| @9| +0#0000000#a8a8a8255| +3&#ffffff0@2|1|,|6| @11|A|l@1| |[+1&&|N|o| |N|a|m|e|]| |[|+|]| @5|1|,|1| @11|A|l@1 |-+2&&@1| |C|o|m@1|a|n|d|-|l|i|n|e| |c|o|m|p|l|e|t|i|o|n| |(|^|V|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |1|5| +0#0000000&@25 diff --git a/src/testdir/dumps/Test_winhighlight_9.dump b/src/testdir/dumps/Test_winhighlight_9.dump index 174531af71..67877d0889 100644 --- a/src/testdir/dumps/Test_winhighlight_9.dump +++ b/src/testdir/dumps/Test_winhighlight_9.dump @@ -4,5 +4,5 @@ |S|i|x| @33| +0#0000001#ffd7ff255|w|a|l@1| @10| +0#0000000#a8a8a8255| +0&#ffffff0@20 |~+0#4040ff13&| @35| +0#0000001#ffd7ff255|w|h|i|l|e| @9| +0#0000000#a8a8a8255| +0#4040ff13#ffffff0@20 |~| @35| +0#0000001#ffd7ff255|w|i|n|c|m|d| @8| +0#0000000#a8a8a8255| +0#4040ff13#ffffff0@20 -|[+1#0000000&|N|o| |N|a|m|e|]| |[|+|]| @5|1|,|5| @11|A|l@1| +0#0000001#ffd7ff255|w|i|n|d|o| @9| +0#0000000#a8a8a8255| +3&#ffffff0@2|1|,|1| @11|A|l@1 +|[+1#0000000&|N|o| |N|a|m|e|]| |[|+|]| @5|1|,|5| @11|A|l@1| +0#0000001#ffd7ff255|w|i|n|d|o| @9| +0#0000000#a8a8a8255| +3&#ffffff0@2|1|,|6| @11|A|l@1 |-+2&&@1| |C|o|m@1|a|n|d|-|l|i|n|e| |c|o|m|p|l|e|t|i|o|n| |(|^|V|^|N|^|P|)| |m+0#00e0003&|a|t|c|h| |1| |o|f| |1|5| +0#0000000&@25 diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index cb901d609f..4df2efb061 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -1025,6 +1025,24 @@ func Test_pum_with_preview_win() call StopVimInTerminal(buf) endfunc +func Test_pum_statusline_ruler() + CheckScreendump + + " With a status line, the ruler must follow the real cursor column after a + " completion inserts text, not stay at the completion start column. + let lines =<< trim END + call setline(1, 'aa aaa ') + set laststatus=2 ruler + END + call writefile(lines, 'Xstlruler', 'D') + let buf = RunVimInTerminal('-S Xstlruler', #{rows: 8, cols: 40}) + call term_sendkeys(buf, "A\") + call VerifyScreenDump(buf, 'Test_pum_statusline_ruler_1', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + func Test_scrollbar_on_wide_char() CheckScreendump diff --git a/src/version.c b/src/version.c index b03ecdf307..02d7bc410e 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 713, /**/ 712, /**/ From 19ab872a905e0f6f2a9df3aa008459f286dc4e5c Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Wed, 24 Jun 2026 17:29:22 +0000 Subject: [PATCH 22/33] patch 9.2.0714: Coverity warns for NULL deref Problem: Coverity warns for NULL dereference in f_remote_startserver() Solution: Return early if the server name is null. related: #20624 Signed-off-by: Christian Brabandt --- src/clientserver.c | 2 ++ src/version.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/clientserver.c b/src/clientserver.c index 5b0d4a2df9..60849fe1e1 100644 --- a/src/clientserver.c +++ b/src/clientserver.c @@ -1176,6 +1176,8 @@ f_remote_startserver(typval_T *argvars UNUSED, typval_T *rettv UNUSED) } char_u *server = tv_get_string_chk(&argvars[0]); + if (server == NULL) + return; # ifdef MSWIN if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) serverSetName(server); diff --git a/src/version.c b/src/version.c index 02d7bc410e..5a42a1ef14 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 714, /**/ 713, /**/ From f67e912ffefb6a9f0ddb2f7dc123e34a552d1f11 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Wed, 24 Jun 2026 17:30:22 +0000 Subject: [PATCH 23/33] patch 9.2.0715: Coverity warns about copy/paste error in hl_blend_attr() Problem: Coverity warns about copy/paste error in hl_blend_attr() Solution: Use foreground color instead, regenerate dump closes: #20624 Signed-off-by: Christian Brabandt --- src/highlight.c | 4 ++-- src/testdir/dumps/Test_popupwin_opacity_hl_80.dump | 4 ++-- src/version.c | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/highlight.c b/src/highlight.c index 325e5d65f0..717a8f5fb6 100644 --- a/src/highlight.c +++ b/src/highlight.c @@ -3581,7 +3581,7 @@ hl_blend_attr(int char_attr, int popup_attr, int blend, int blend_fg UNUSED) under_fg_rgb = char_aep->ae_u.cterm.fg_rgb; #endif new_en.ae_u.cterm.fg_color = blend_cterm_colors( - popup_aep->ae_u.cterm.bg_color, popup_bg_rgb, + popup_aep->ae_u.cterm.fg_color, popup_bg_rgb, under_fg, under_fg_rgb, fallback_fg_rgb, blend); } // Approximate cterm bg by blending with the underlying bg @@ -3768,7 +3768,7 @@ hl_pum_blend_attr(int char_attr, int popup_attr, int blend UNUSED) popup_bg_rgb = popup_aep->ae_u.cterm.bg_rgb; #endif new_en.ae_u.cterm.fg_color = blend_cterm_colors( - popup_aep->ae_u.cterm.bg_color, popup_bg_rgb, + popup_aep->ae_u.cterm.fg_color, popup_bg_rgb, under_fg, under_fg_rgb, fallback_fg_rgb, blend); } // Approximate cterm bg by blending with the underlying bg diff --git a/src/testdir/dumps/Test_popupwin_opacity_hl_80.dump b/src/testdir/dumps/Test_popupwin_opacity_hl_80.dump index c57365eaae..78d3115450 100644 --- a/src/testdir/dumps/Test_popupwin_opacity_hl_80.dump +++ b/src/testdir/dumps/Test_popupwin_opacity_hl_80.dump @@ -1,7 +1,7 @@ >1+0&#ffffff0| @73 |2| @73 -|3| @7|f+0#ff404010#87d7ff255|o@1| +0#5fafd7255&@1|b+0#0000001&|a|r| +0#0000000#ffffff0@57 -|4| @7|b+0#0000001#87d7ff255|a|z| +0#5fafd7255&@4| +0#0000000#ffffff0@57 +|3| @7|f+0#ff404010#87d7ff255|o@1| +0#d75f5f255&@1|b+0#0000001&|a|r| +0#0000000#ffffff0@57 +|4| @7|b+0#0000001#87d7ff255|a|z| +0#0000000&@4| +0&#ffffff0@57 |5| @73 |6| @73 |7| @73 diff --git a/src/version.c b/src/version.c index 5a42a1ef14..de6662811d 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 715, /**/ 714, /**/ From 758543dcb7527e4d4bfe3f51196c9134632b3a7a Mon Sep 17 00:00:00 2001 From: "Wu, Zhenyu" Date: Wed, 24 Jun 2026 17:34:25 +0000 Subject: [PATCH 24/33] patch 9.2.0716: filetype: not all supertux files are recognized Problem: filetype: not all supertux files are recognized Solution: Detect more supertux related files as scheme filetype (Wu Zhenyu) levels: *.stwm: supertux world map https://github.com/SuperTux/supertux/wiki/Worldmap-Format *.stl: supertux level https://github.com/SuperTux/supertux/wiki/Level-Format *.stxt: supertux scrolling texts https://github.com/SuperTux/supertux/wiki/File_formats#scrolling-texts images: *.sprite: supertux sprite https://github.com/SuperTux/supertux/wiki/Sprite *.strf: supertux tileset https://github.com/SuperTux/supertux/wiki/Tileset *.satc: supertux autotiles configuration *.stcd: supertux converter data font: *.stf: supetux font particles: *.stcp: supertux custom particle music: *.music: supertux music config: ~/.local/share/supertux2/config: supertux config https://github.com/SuperTux/supertux/wiki/S-Expression#supertux-config-file *.stsg: supertux save game info: info: https://github.com/SuperTux/supertux/wiki/File_formats#level-subsets related: #16287 closes: #20615 Signed-off-by: Wu, Zhenyu Signed-off-by: Christian Brabandt --- runtime/filetype.vim | 2 +- src/testdir/test_filetype.vim | 2 +- src/version.c | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime/filetype.vim b/runtime/filetype.vim index 680b62b37a..efcea55e30 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -1062,7 +1062,7 @@ au BufNewFile,BufRead .zshrc,.zshenv,.zlogin,.zlogout,.zcompdump,.zsh_history se au BufNewFile,BufRead *.zsh,*.zsh-theme,*.zunit setf zsh " Scheme, Supertux configuration, Lips.js history, Guile init file ("racket" patterns are now separate, see above) -au BufNewFile,BufRead *.scm,*.ss,*.sld,*.stsg,*/supertux2/config,.lips_repl_history,.guile setf scheme +au BufNewFile,BufRead *.scm,*.ss,*.sld,*.stwm,*.stl,*.stxt,*.sprite,*.strf,*.satc,*.stcd,*.stf,*.stcp,*.music,*.stsg,*/supertux2/config,supertux2/*/info,.lips_repl_history,.guile setf scheme " SiSU au BufNewFile,BufRead *.sst.meta,*.-sst.meta,*._sst.meta setf sisu diff --git a/src/testdir/test_filetype.vim b/src/testdir/test_filetype.vim index 61a72fd1f3..7924789e28 100644 --- a/src/testdir/test_filetype.vim +++ b/src/testdir/test_filetype.vim @@ -729,7 +729,7 @@ def s:GetFilenameChecks(): dict> sass: ['file.sass'], sbt: ['file.sbt'], scala: ['file.scala', 'file.mill'], - scheme: ['file.scm', 'file.ss', 'file.sld', 'file.stsg', 'any/local/share/supertux2/config', '.lips_repl_history', '.guile'], + scheme: ['file.scm', 'file.ss', 'file.sld', 'file.stwm', 'file.stl', 'file.stxt', 'file.sprite', 'file.strf', 'file.satc', 'file.stcd', 'file.stf', 'file.stcp', 'file.music', 'file.stsg', 'any/local/share/supertux2/config', 'supertux2/levels/world1/info', '.lips_repl_history', '.guile'], scilab: ['file.sci', 'file.sce'], screen: ['.screenrc', 'screenrc'], scss: ['file.scss'], diff --git a/src/version.c b/src/version.c index de6662811d..aebc461077 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 716, /**/ 715, /**/ From 05df981c35e066dd45ebe12af26fb4e662a932ef Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 24 Jun 2026 17:45:48 +0000 Subject: [PATCH 25/33] patch 9.2.0717: tests: strange indent in Test_autocmd_dup_arg() Problem: tests: strange indent in Test_autocmd_dup_arg() (after v9.2.0708) Solution: Indent using the settings in the modeline. closes: #20619 Signed-off-by: zeertzjq Signed-off-by: Christian Brabandt --- src/testdir/test_autocmd.vim | 56 ++++++++++++++++++------------------ src/version.c | 2 ++ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/testdir/test_autocmd.vim b/src/testdir/test_autocmd.vim index 4828b3ea9e..b2a3dea149 100644 --- a/src/testdir/test_autocmd.vim +++ b/src/testdir/test_autocmd.vim @@ -3212,34 +3212,34 @@ func Test_autocmd_once() endfunc func Test_autocmd_dup_arg() - " Duplicate ++once / ++nested, or the legacy "nested" used twice, must - " error out *and* not create the autocommand. Using an environment - " variable in the pattern also exercises the error-exit path that frees - " the expanded pattern (checked by the address/leak sanitizers). - augroup XdupTest - au! - augroup END - let $XAUTODIR = 'Xfoo' - - " New behavior: duplicate ++once now aborts, the autocmd is not added - call assert_fails('au XdupTest WinNew $XAUTODIR/* ++once ++once echo bad', 'E983:') - call assert_false(exists('#XdupTest#WinNew')) - - call assert_fails('au XdupTest WinNew $XAUTODIR/* ++nested ++nested echo bad', 'E983:') - call assert_false(exists('#XdupTest#WinNew')) - - call assert_fails('au XdupTest WinNew $XAUTODIR/* nested nested echo bad', 'E983:') - call assert_false(exists('#XdupTest#WinNew')) - - " "nested" without "++" is rejected in Vim9 script (also frees the pattern) - call assert_fails('vim9cmd au XdupTest WinNew $XAUTODIR/* nested echo bad', 'E1078:') - call assert_false(exists('#XdupTest#WinNew')) - - augroup XdupTest - au! - augroup END - augroup! XdupTest - let $XAUTODIR = '' + " Duplicate ++once / ++nested, or the legacy "nested" used twice, must + " error out *and* not create the autocommand. Using an environment + " variable in the pattern also exercises the error-exit path that frees + " the expanded pattern (checked by the address/leak sanitizers). + augroup XdupTest + au! + augroup END + let $XAUTODIR = 'Xfoo' + + " New behavior: duplicate ++once now aborts, the autocmd is not added + call assert_fails('au XdupTest WinNew $XAUTODIR/* ++once ++once echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + call assert_fails('au XdupTest WinNew $XAUTODIR/* ++nested ++nested echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + call assert_fails('au XdupTest WinNew $XAUTODIR/* nested nested echo bad', 'E983:') + call assert_false(exists('#XdupTest#WinNew')) + + " "nested" without "++" is rejected in Vim9 script (also frees the pattern) + call assert_fails('vim9cmd au XdupTest WinNew $XAUTODIR/* nested echo bad', 'E1078:') + call assert_false(exists('#XdupTest#WinNew')) + + augroup XdupTest + au! + augroup END + augroup! XdupTest + let $XAUTODIR = '' endfunc diff --git a/src/version.c b/src/version.c index aebc461077..72f154f3e6 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 717, /**/ 716, /**/ From f2954c821ebf007503bc29cd25d01974f691432c Mon Sep 17 00:00:00 2001 From: Dmytro Meleshko Date: Wed, 24 Jun 2026 17:51:22 +0000 Subject: [PATCH 26/33] patch 9.2.0718: :syn sync without an argument also lists syntax cluster Problem: :syn sync without an argument also lists every defined cluster Solution: Fix control flow in syn_cmd_list() so that only the syncing items are printed when this function gets called by :syn sync. (dmitmel) closes: #20614 Signed-off-by: Dmytro Meleshko Signed-off-by: Christian Brabandt --- src/po/vim.pot | 22 +++++++++++----------- src/syntax.c | 32 ++++++++++++++++++-------------- src/testdir/test_syntax.vim | 19 +++++++++++++++++++ src/version.c | 2 ++ 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/po/vim.pot b/src/po/vim.pot index bbbf9f0cc2..131e52457f 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-06-22 19:36+0000\n" +"POT-Creation-Date: 2026-06-24 17:52+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -3109,26 +3109,26 @@ msgstr "" msgid "syncing on C-style comments" msgstr "" -msgid "no syncing" +msgid "" +"\n" +"--- Syntax sync items ---" msgstr "" -msgid "syncing starts at the first line" +msgid "" +"\n" +"syncing on items" msgstr "" -msgid "syncing starts " +msgid "no syncing" msgstr "" -msgid " lines before top line" +msgid "syncing starts at the first line" msgstr "" -msgid "" -"\n" -"--- Syntax sync items ---" +msgid "syncing starts " msgstr "" -msgid "" -"\n" -"syncing on items" +msgid " lines before top line" msgstr "" msgid "" diff --git a/src/syntax.c b/src/syntax.c index 4333c31839..6f22d9ece6 100644 --- a/src/syntax.c +++ b/src/syntax.c @@ -3834,9 +3834,22 @@ syn_cmd_list( msg_puts(_("syncing on C-style comments")); syn_lines_msg(); syn_match_msg(); - return; } - else if (!(curwin->w_s->b_syn_sync_flags & SF_MATCH)) + else if (curwin->w_s->b_syn_sync_flags & SF_MATCH) + { + msg_puts_title(_("\n--- Syntax sync items ---")); + if (curwin->w_s->b_syn_sync_minlines > 0 + || curwin->w_s->b_syn_sync_maxlines > 0 + || curwin->w_s->b_syn_sync_linebreaks > 0) + { + msg_puts(_("\nsyncing on items")); + syn_lines_msg(); + syn_match_msg(); + } + for (id = 1; id <= highlight_num_groups() && !got_int; ++id) + syn_list_one(id, syncing, FALSE); + } + else { if (curwin->w_s->b_syn_sync_minlines == 0) msg_puts(_("no syncing")); @@ -3852,20 +3865,11 @@ syn_cmd_list( } syn_match_msg(); } - return; - } - msg_puts_title(_("\n--- Syntax sync items ---")); - if (curwin->w_s->b_syn_sync_minlines > 0 - || curwin->w_s->b_syn_sync_maxlines > 0 - || curwin->w_s->b_syn_sync_linebreaks > 0) - { - msg_puts(_("\nsyncing on items")); - syn_lines_msg(); - syn_match_msg(); } + return; } - else - msg_puts_title(_("\n--- Syntax items ---")); + + msg_puts_title(_("\n--- Syntax items ---")); if (ends_excmd2(eap->cmd, arg)) { /* diff --git a/src/testdir/test_syntax.vim b/src/testdir/test_syntax.vim index 170e2e3b93..d4a81dd656 100644 --- a/src/testdir/test_syntax.vim +++ b/src/testdir/test_syntax.vim @@ -419,6 +419,25 @@ func Test_syn_sync() call assert_match('SyncHere', execute('syntax sync')) syn sync clear call assert_notmatch('SyncHere', execute('syntax sync')) + + syn sync minlines=10 + syntax cluster xmlStuff contains=xmlGroup1,xmlGroup2 + syntax region xmlComment start=// keepend contains=@Spell + syntax sync match xmlSync1 grouphere xmlComment // + syntax cluster xmlAll contains=ALL + let out = execute('syntax sync') + call assert_match('xmlSync1', out) + call assert_match('xmlSync2', out) + call assert_match('grouphere xmlComment', out) + call assert_match('groupthere NONE', out) + call assert_notmatch('xmlStuff', out) + call assert_notmatch('xmlAll', out) + call assert_notmatch('cluster', out) + call assert_notmatch('keepend', out) + " should output 4 lines: 1 header + 3 syn sync lines + call assert_equal(4, len(split(out, '\n'))) + syn clear endfunc diff --git a/src/version.c b/src/version.c index 72f154f3e6..7fd7b6b801 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 718, /**/ 717, /**/ From 037a19e1c1f737abc560ffc1d2edf2b535194816 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 18:19:53 +0000 Subject: [PATCH 27/33] patch 9.2.0719: GTK4: default menu is lacking Problem: GTK4: default menu is lacking: accelerator text is not shown, mnemonics and 'winaltkeys' do not work Solution: Replace the GMenuModel-based menus with a custom widget set (VimMenuBar, VimMenuBarItem, VimMenu, VimMenuItem) in a new gui_gtk4_menu.c, modelled on the GTK3 menu bar: show accelerator text, support mnemonics and 'winaltkeys', add keyboard navigation, instant tooltips, and the popup and F10 menus, and implement the previously stubbed menu functions (Foxe Chen). closes: #20593 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- Filelist | 2 + runtime/doc/gui_x11.txt | 13 +- runtime/doc/tags | 1 + src/Makefile | 14 +- src/gui_gtk4.c | 770 +++++++++++------------------ src/gui_gtk4_da.c | 2 + src/gui_gtk4_menu.c | 1038 +++++++++++++++++++++++++++++++++++++++ src/gui_gtk4_menu.h | 61 +++ src/gui_gtk4_tb.c | 2 + src/menu.c | 4 - src/structs.h | 2 + src/version.c | 2 + 12 files changed, 1412 insertions(+), 499 deletions(-) create mode 100644 src/gui_gtk4_menu.c create mode 100644 src/gui_gtk4_menu.h diff --git a/Filelist b/Filelist index 019fa1aeea..c073e1ad6d 100644 --- a/Filelist +++ b/Filelist @@ -518,6 +518,8 @@ SRC_UNIX = \ src/gui_gtk4_da.h \ src/gui_gtk4_tb.c \ src/gui_gtk4_tb.h \ + src/gui_gtk4_menu.c \ + src/gui_gtk4_menu.h \ src/gui_gtk_res.xml \ src/gui_motif.c \ src/gui_xmdlg.c \ diff --git a/runtime/doc/gui_x11.txt b/runtime/doc/gui_x11.txt index 2e558d8a0a..8a084505e8 100644 --- a/runtime/doc/gui_x11.txt +++ b/runtime/doc/gui_x11.txt @@ -1,4 +1,4 @@ -*gui_x11.txt* For Vim version 9.2. Last change: 2026 Jun 13 +*gui_x11.txt* For Vim version 9.2. Last change: 2026 Jun 24 VIM REFERENCE MANUAL by Bram Moolenaar @@ -782,5 +782,16 @@ Most newer applications will provide their current selection via PRIMARY ("*) and use CLIPBOARD ("+) for cut/copy/paste operations. You thus have access to both by choosing to use either of the "* or "+ registers. + *gtk4-menu-navigation* +In the GTK 4 GUI, you may also navigate the menu items with these keyboard +mappings: + key meaning ~ + Go to next item + Go to previous item + Go to parent submenu + Go to current item's submenu + Go to next menu bar item + Go to previous menu bar item + vim:tw=78:sw=4:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/tags b/runtime/doc/tags index 11ae058bbc..3abd3e035e 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -8260,6 +8260,7 @@ gtk-css gui_x11.txt /*gtk-css* gtk-tooltip-colors gui_x11.txt /*gtk-tooltip-colors* gtk3-slow gui_x11.txt /*gtk3-slow* gtk4-hwaccel gui_x11.txt /*gtk4-hwaccel* +gtk4-menu-navigation gui_x11.txt /*gtk4-menu-navigation* gtk4-slow gui_x11.txt /*gtk4-slow* gu change.txt /*gu* gugu change.txt /*gugu* diff --git a/src/Makefile b/src/Makefile index 89e023c9d0..e0f7057b83 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1244,6 +1244,7 @@ GTK4_SRC = gui.c gui_gtk4.c gui_gtk4_f.c \ gui_gtk4_da.c \ gui_beval.o \ gui_gtk4_tb.c \ + gui_gtk4_menu.c \ $(GRESOURCE_SRC) GTK4_OBJ = objects/gui.o objects/gui_gtk4.o \ objects/gui_gtk4_f.o \ @@ -1251,6 +1252,7 @@ GTK4_OBJ = objects/gui.o objects/gui_gtk4.o \ objects/gui_gtk4_da.o \ objects/gui_beval.o \ objects/gui_gtk4_tb.o \ + objects/gui_gtk4_menu.o \ $(GRESOURCE_OBJ) GTK4_DEFS = -DFEAT_GUI_GTK $(NARROW_PROTO) GTK4_IPATH = $(GUI_INC_LOC) @@ -1320,7 +1322,7 @@ HAIKUGUI_TESTTARGET = gui HAIKUGUI_BUNDLE = # All GUI files -ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c gui_gtk4_cb.c gui_gtk4_da.c gui_gtk4_tb.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc +ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c gui_gtk4_cb.c gui_gtk4_da.c gui_gtk4_tb.c gui_gtk4_menu.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc ALL_GUI_PRO = proto/gui.pro proto/gui_gtk.pro proto/gui_gtk4.pro proto/gui_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro proto/gui_w32.pro proto/gui_photon.pro # }}} @@ -3421,6 +3423,9 @@ objects/gui_gtk4_da.o: gui_gtk4_da.c objects/gui_gtk4_tb.o: gui_gtk4_tb.c $(CCC) -o $@ gui_gtk4_tb.c +objects/gui_gtk4_menu.o: gui_gtk4_menu.c + $(CCC) -o $@ gui_gtk4_menu.c + objects/gui_haiku.o: gui_haiku.cc $(CCC) -o $@ gui_haiku.cc @@ -4518,7 +4523,7 @@ objects/gui_gtk4.o: auto/osdef.h gui_gtk4.c vim.h protodef.h auto/config.h featu structs.h regexp.h gui.h libvterm/include/vterm.h \ libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \ globals.h errors.h gui_gtk4_f.h auto/gui_gtk_gresources.h \ - gui_gtk4_cb.h gui_gtk4_da.h gui_gtk4_tb.h + gui_gtk4_cb.h gui_gtk4_da.h gui_gtk4_tb.h gui_gtk4_menu.h objects/gui_gtk4_f.o: auto/osdef.h gui_gtk4_f.c vim.h protodef.h auto/config.h feature.h \ os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \ beval.h structs.h regexp.h gui.h \ @@ -4539,6 +4544,11 @@ objects/gui_gtk4_tb.o: auto/osdef.h gui_gtk4_tb.c vim.h protodef.h auto/config.h beval.h structs.h regexp.h gui.h \ libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \ ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_tb.h +objects/gui_gtk4_menu.o: auto/osdef.h gui_gtk4_menu.c vim.h protodef.h auto/config.h feature.h \ + os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \ + beval.h structs.h regexp.h gui.h \ + libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \ + ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_menu.h objects/gui_gtk_f.o: auto/osdef.h gui_gtk_f.c vim.h protodef.h auto/config.h feature.h \ os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \ beval.h structs.h regexp.h gui.h \ diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index fe9381f2f6..b5f1f93b40 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -36,6 +36,9 @@ #ifdef FEAT_TOOLBAR # include "gui_gtk4_tb.h" #endif +#ifdef FEAT_MENU +# include "gui_gtk4_menu.h" +#endif /* * Geometry string parser, replacing XParseGeometry to remove X11 dependency. @@ -126,9 +129,6 @@ static int last_shape = 0; #define DEFAULT_FONT "Monospace 10" -// Menu action group for GMenu-based menus -static GSimpleActionGroup *menu_action_group = NULL; - // Cursor blinking state static enum { BLINK_NONE, @@ -283,9 +283,6 @@ static void enter_notify_event(GtkEventControllerMotion *controller, double x, d static gboolean scroll_event(GtkEventControllerScroll *controller, double dx, double dy, gpointer data); static void focus_in_event(GtkEventControllerFocus *controller, gpointer data); static void focus_out_event(GtkEventControllerFocus *controller, gpointer data); -#ifdef FEAT_MENU -static gboolean menubar_popover_closed_hook(GSignalInvocationHint *ihint, guint n_param_values, const GValue *param_values, gpointer data); -#endif #ifdef FEAT_DND static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data); #endif @@ -293,7 +290,7 @@ static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, do static void tabline_enter_cb(GtkEventController *controller, double x, double y, void *udata); static void on_select_tab(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data); static void on_tab_reordered(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data); -static GMenu *create_tabline_popup_menu(GActionGroup **agroup_store); +static VimMenu *create_tabline_popup_menu(void); static void tabline_menu_press_event(GtkGestureClick *gesture, int n_press, double x, double y, GtkWidget *popover); #endif static void mainwin_destroy_cb(GObject *object, gpointer data); @@ -481,30 +478,10 @@ gui_mch_init(void) gtk_window_set_child(GTK_WINDOW(gui.mainwin), vbox); #ifdef FEAT_MENU - { - GMenu *gmenu = g_menu_new(); - gui.menubar = gtk_popover_menu_bar_new_from_model( - G_MENU_MODEL(gmenu)); - g_object_set_data_full(G_OBJECT(gui.menubar), "vim-gmenu", - gmenu, g_object_unref); - gtk_widget_set_name(gui.menubar, "vim-menubar"); - gtk_widget_set_visible(gui.menubar, FALSE); - gtk_box_append(GTK_BOX(vbox), gui.menubar); - } - // Return keyboard focus to the drawing area when a menubar popover - // closes (issue #20274). GtkPopoverMenuBar owns its popovers - // privately, so attach via an emission hook on GtkPopover::closed - // and filter for popovers under our menubar inside the callback. - { - GTypeClass *cls = g_type_class_ref(GTK_TYPE_POPOVER); - guint sig_id = g_signal_lookup("closed", GTK_TYPE_POPOVER); - - if (sig_id != 0) - g_signal_add_emission_hook(sig_id, 0, - menubar_popover_closed_hook, NULL, NULL); - if (cls != NULL) - g_type_class_unref(cls); - } + gui.menubar = vim_menu_bar_new(); + gtk_widget_set_name(gui.menubar, "vim-menubar"); + gtk_widget_set_visible(gui.menubar, FALSE); + gtk_box_append(GTK_BOX(vbox), gui.menubar); #endif #ifdef FEAT_TOOLBAR @@ -544,28 +521,25 @@ gui_mch_init(void) // Create right click popup menu for tabline { GtkGesture *click; - GActionGroup *agroup; - GMenu *menu; - GtkWidget *popover; + VimMenu *menu; click = gtk_gesture_click_new(); - menu = create_tabline_popup_menu(&agroup); - popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(menu)); - g_object_unref(menu); + menu = create_tabline_popup_menu(); - gtk_widget_set_parent(popover, gui.tabline); - g_object_set_data(G_OBJECT(gui.tabline), "menu", popover); - gtk_widget_insert_action_group(gui.tabline, "tabline", agroup); - g_object_unref(agroup); + gtk_widget_set_parent(GTK_WIDGET(menu), gui.tabline); + g_object_set_data(G_OBJECT(gui.tabline), "menu", menu); - gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); - gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM); + gtk_popover_set_has_arrow(GTK_POPOVER(menu), FALSE); + gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM); + // Make popover start at top left corner + gtk_widget_set_halign(GTK_WIDGET(menu), GTK_ALIGN_START); // Listen for anny mouse button gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0); g_signal_connect_object(click, "pressed", - G_CALLBACK(tabline_menu_press_event), popover, G_CONNECT_DEFAULT); + G_CALLBACK(tabline_menu_press_event), + menu, G_CONNECT_DEFAULT); gtk_widget_add_controller(gui.tabline, GTK_EVENT_CONTROLLER(click)); } #endif @@ -812,6 +786,19 @@ gui_mch_exit(int rc UNUSED) // Make sure to destroy popover used for balloon eval, or we will get a // warning from GTK that the draw area still has children left. gui_mch_destroy_beval_area(balloonEval); +#endif +#ifdef FEAT_MENU + // Make sure to unparent any popover menus + { + vimmenu_T *menu; + + FOR_ALL_MENUS(menu) + { + if ((menu->name[0] == ']' || menu_is_popup(menu->name)) + && menu->submenu_id != NULL) + gtk_widget_unparent(menu->submenu_id); + } + } #endif gtk_window_destroy(GTK_WINDOW(gui.mainwin)); } @@ -1939,6 +1926,19 @@ key_press_event(GtkEventControllerKey *controller UNUSED, state |= GDK_SHIFT_MASK; } +#ifdef FEAT_MENU + // If there is a menu and 'wak' is "yes", or 'wak' is "menu" and the key + // is a menu shortcut, we ignore everything with the ALT modifier. + if ((state & GDK_ALT_MASK) + && gui.menu_is_active + && (*p_wak == 'y' + || (*p_wak == 'm' + && len == 1 + && gui_is_menu_shortcut(string[0])))) + // Tell GTK we have not handled the key (so it can handle it). + return FALSE; +#endif + // Check for special keys if (len == 0 || len == 1) { @@ -2227,6 +2227,10 @@ motion_notify_event(GtkEventControllerMotion *controller UNUSED, prev_mouse_x = x; prev_mouse_y = y; + + // Make sure keyboard input goes to the drawing area. Fixes issues with menu + // still being focused. + gtk_widget_grab_focus(gui.drawarea); } static void @@ -2237,8 +2241,7 @@ enter_notify_event(GtkEventControllerMotion *controller UNUSED, prev_mouse_y = y; // Make sure keyboard input goes to the drawing area. - if (!gtk_widget_has_focus(gui.drawarea)) - gtk_widget_grab_focus(gui.drawarea); + gtk_widget_grab_focus(gui.drawarea); } static gboolean @@ -2300,48 +2303,6 @@ focus_out_event(GtkEventControllerFocus *controller UNUSED, } } -#ifdef FEAT_MENU - static gboolean -grab_drawarea_focus_idle(gpointer data UNUSED) -{ - if (gui.drawarea != NULL && !gtk_widget_has_focus(gui.drawarea)) - gtk_widget_grab_focus(gui.drawarea); - return G_SOURCE_REMOVE; -} - - static gboolean -menubar_popover_closed_hook(GSignalInvocationHint *ihint UNUSED, - guint n_param_values, const GValue *param_values, - gpointer data UNUSED) -{ - GObject *obj; - GtkWidget *popover; - GtkWidget *parent; - - if (n_param_values < 1 || gui.menubar == NULL || gui.drawarea == NULL) - return TRUE; - obj = g_value_get_object(¶m_values[0]); - if (!GTK_IS_POPOVER(obj)) - return TRUE; - popover = GTK_WIDGET(obj); - - // Only react to popovers that descend from the menubar. - for (parent = gtk_widget_get_parent(popover); - parent != NULL; - parent = gtk_widget_get_parent(parent)) - { - if (parent != gui.menubar) - continue; - // Defer the grab to the next main loop iteration; calling it - // synchronously while GTK is still completing the popover close - // has no effect (issue #20274). - g_idle_add(grab_drawarea_focus_idle, NULL); - break; - } - return TRUE; // keep the emission hook installed -} -#endif - static void drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) { @@ -2818,6 +2779,7 @@ gui_mch_enable_scrollbar(scrollbar_T *sb, int flag) gtk_widget_set_visible(sb->id, flag); } +#if defined(FEAT_MENU) /* * ============================================================ * Menu stubs @@ -2827,56 +2789,22 @@ gui_mch_enable_scrollbar(scrollbar_T *sb, int flag) void gui_mch_menu_grey(vimmenu_T *menu, int grey) { - if (menu->id == NULL || menu_action_group == NULL) - return; - - // For toolbar items, use gtk_widget_set_sensitive - if (menu->parent != NULL && menu_is_toolbar(menu->parent->name)) - { - if (menu->id != (GtkWidget *)1) - gtk_widget_set_sensitive(menu->id, !grey); + if (menu->id == NULL) return; - } - - // For menu items, enable/disable the GSimpleAction - if (menu->label != NULL) - { - GAction *action = g_action_map_lookup_action( - G_ACTION_MAP(menu_action_group), - (const char *)menu->label); - if (action != NULL) - g_simple_action_set_enabled(G_SIMPLE_ACTION(action), !grey); - } + gtk_widget_set_sensitive(menu->id, !grey); + gui_mch_update(); } -#if defined(FEAT_MENU) /* * Make menu item hidden or not hidden. */ void gui_mch_menu_hidden(vimmenu_T *menu, int hidden) { - // GMenu-based menu items have no real widget, only the (GtkWidget *)1 - // marker; they cannot be toggled via the widget API. - if (menu->id == NULL || menu->id == (GtkWidget *)1) + if (menu->id == NULL) return; - - if (hidden) - { - if (gtk_widget_get_visible(menu->id)) - { - gtk_widget_set_visible(menu->id, FALSE); - gui_mch_update(); - } - } - else - { - if (!gtk_widget_get_visible(menu->id)) - { - gtk_widget_set_visible(menu->id, TRUE); - gui_mch_update(); - } - } + gtk_widget_set_visible(menu->id, !hidden); + gui_mch_update(); } void @@ -3053,52 +2981,34 @@ on_tab_reordered( * Handle selecting an item in the tab line popup menu. */ static void -tabline_menu_action_cb( - GSimpleAction *action UNUSED, - GVariant *parameter UNUSED, - void *udata) +tabline_menu_event_cb(VimMenuItem *item, VimMenuItemEvent event, void *udata) { - send_tabline_menu_event(tabpage_hover, GPOINTER_TO_INT(udata)); + if (event == VIM_MENU_ITEM_CLICKED) + send_tabline_menu_event(tabpage_hover, GPOINTER_TO_INT(udata)); } static void -add_tabline_menu_item( - GMenu *gmenu, - GActionMap *amap, - const char *name, - const char *action, - int resp) +add_tabline_menu_item(VimMenu *menu, const char *name, int resp) { - GSimpleAction *act = g_simple_action_new(action, NULL); - char detailed[32]; + VimMenuItem *item = VIM_MENU_ITEM(vim_menu_item_new(name, + tabline_menu_event_cb, GINT_TO_POINTER(resp))); - g_signal_connect(act, "activate", G_CALLBACK(tabline_menu_action_cb), - GINT_TO_POINTER(resp)); - g_action_map_add_action(amap, G_ACTION(act)); - g_object_unref(act); - - vim_snprintf(detailed, sizeof(detailed), "tabline.%s", action); - g_menu_append(gmenu, name, detailed); + vim_menu_insert_item(menu, item, -1); } /* * Create a menu for the tab line. */ - static GMenu * -create_tabline_popup_menu(GActionGroup **agroup_store) + static VimMenu * +create_tabline_popup_menu(void) { - GMenu *gmenu = g_menu_new(); - GSimpleActionGroup *agroup = g_simple_action_group_new(); + VimMenu *menu = VIM_MENU(vim_menu_new()); - add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup), - _("Close Tab"), "close-tab", TABLINE_MENU_CLOSE); - add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup), - _("New Tab"), "new-tab", TABLINE_MENU_NEW); - add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup), - _("Open Tab..."), "open-tab", TABLINE_MENU_OPEN); + add_tabline_menu_item(menu, _("Close Tab"), TABLINE_MENU_CLOSE); + add_tabline_menu_item(menu, _("New Tab"), TABLINE_MENU_NEW); + add_tabline_menu_item(menu, _("Open Tab..."), TABLINE_MENU_OPEN); - *agroup_store = G_ACTION_GROUP(agroup); - return gmenu; + return menu; } static void @@ -3826,43 +3736,92 @@ gui_get_x11_windis(Window *win UNUSED, Display **dis UNUSED) } #if defined(FEAT_MENU) - void -gui_gtk_set_mnemonics(int enable UNUSED) +/* + * Translate Vim's mnemonic tagging to GTK+ style and convert to UTF-8 + * if necessary. The caller must vim_free() the returned string. + * + * Input Output + * _ __ + * && & + * & _ stripped if use_mnemonic == FALSE + * end of menu label text + */ + static char_u * +translate_mnemonic_tag(char_u *name, int use_mnemonic) { - // TODO: implement? -} + char_u *buf; + char_u *psrc; + char_u *pdest; + int n_underscores = 0; - static void -popupmenu_closed_cb(GtkPopover *popover, gpointer data UNUSED) -{ - gtk_widget_unparent(GTK_WIDGET(popover)); - if (gui.drawarea != NULL) - gtk_widget_queue_draw(gui.drawarea); -} + name = CONVERT_TO_UTF8(name); + if (name == NULL) + return NULL; -typedef struct { - GtkPopover *popover; - vimmenu_T *menu; -} popup_item_data_T; + for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc) + if (*psrc == '_') + ++n_underscores; - static void -popup_item_clicked_cb(GtkButton *button UNUSED, gpointer data) + buf = alloc(psrc - name + n_underscores + 1); + if (buf != NULL) + { + pdest = buf; + for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc) + { + if (*psrc == '_') + { + *pdest++ = '_'; + *pdest++ = '_'; + } + else if (*psrc != '&') + { + *pdest++ = *psrc; + } + else if (*(psrc + 1) == '&') + { + *pdest++ = *psrc++; + } + else if (use_mnemonic) + { + *pdest++ = '_'; + } + } + *pdest = NUL; + } + + CONVERT_TO_UTF8_FREE(name); + return buf; +} + +/* + * Enable or disable accelerators for the toplevel menus. + */ + void +gui_gtk_set_mnemonics(int enable) { - popup_item_data_T *d = data; + vimmenu_T *menu; + char_u *name; - if (d->popover != NULL) - gtk_popover_popdown(d->popover); - if (d->menu != NULL) + FOR_ALL_MENUS(menu) { - gui_menu_cb(d->menu); - gui_mch_flush(); + if (menu->id == NULL) + continue; + + name = translate_mnemonic_tag(menu->name, enable); + // Don't think the check if necessary but still do it anyways + if (VIM_IS_MENU_BAR_ITEM(menu->id)) + vim_menu_bar_item_set_text(VIM_MENU_BAR_ITEM(menu->id), + (const char *)name); + vim_free(name); } } static void -popup_item_data_free(gpointer data, GClosure *closure UNUSED) +popupmenu_closed_cb(GtkWidget *popover, void *udata UNUSED) { - g_free(data); + gtk_widget_unparent(popover); + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); } /* @@ -3872,93 +3831,24 @@ popup_item_data_free(gpointer data, GClosure *closure UNUSED) gui_gtk_popup_at(vimmenu_T *menu, int x, int y) { GtkWidget *popover; - GtkWidget *box; - GtkWidget *parent; GdkRectangle rect; - vimmenu_T *child; - int mode; - int natural_width = 0; - if (menu == NULL || menu->children == NULL) + if (menu == NULL || menu->submenu_id == NULL) return; - // Attach the popover to drawarea's parent rather than to drawarea itself. - // GtkDrawingArea is a leaf widget whose snapshot does not iterate children, - // and parenting a popover to it has been observed to leave the drawing area - // blank while the popover is open. - parent = gtk_widget_get_parent(gui.drawarea); - if (parent == NULL) - parent = gui.drawarea; - - // Build the popover by hand instead of using gtk_popover_menu_new_from_model. - // GtkPopoverMenu relies on the "menu." action-group lookup walking up - // the parent chain, which has been observed to silently fail on some - // compositors when the popover is parented via gtk_widget_set_parent. Wiring - // each menu item to a plain "clicked" signal sidesteps that entirely. - popover = gtk_popover_new(); - gtk_widget_set_parent(popover, parent); - gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); - gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM); - gtk_widget_add_css_class(popover, "menu"); - - box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_popover_set_child(GTK_POPOVER(popover), box); - - mode = get_menu_mode_flag(); - - for (child = menu->children; child != NULL; child = child->next) - { - GtkWidget *item; - char_u *label; - popup_item_data_T *cb_data; - - if (menu_is_separator(child->name)) - { - item = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); - gtk_box_append(GTK_BOX(box), item); - continue; - } - - label = CONVERT_TO_UTF8(child->dname); - item = gtk_button_new_with_mnemonic( - label != NULL ? (const char *)label : ""); - CONVERT_TO_UTF8_FREE(label); - - gtk_widget_add_css_class(item, "flat"); - gtk_widget_add_css_class(item, "model"); - gtk_button_set_has_frame(GTK_BUTTON(item), FALSE); - gtk_widget_set_halign(item, GTK_ALIGN_FILL); - { - GtkWidget *btn_label = gtk_button_get_child(GTK_BUTTON(item)); - if (GTK_IS_LABEL(btn_label)) - gtk_label_set_xalign(GTK_LABEL(btn_label), 0.0); - } - - if (!(child->modes & child->enabled & mode)) - gtk_widget_set_sensitive(item, FALSE); - - cb_data = g_new0(popup_item_data_T, 1); - cb_data->popover = GTK_POPOVER(popover); - cb_data->menu = child; - g_signal_connect_data(item, "clicked", - G_CALLBACK(popup_item_clicked_cb), - cb_data, popup_item_data_free, 0); - - gtk_box_append(GTK_BOX(box), item); - } + popover = vim_menu_copy(VIM_MENU(menu->submenu_id)); + gtk_widget_set_parent(popover, gui.drawarea); rect.x = x; rect.y = y; - // GtkPopover with GTK_POS_BOTTOM centres horizontally on the pointing-to - // rectangle. Use the box's natural width so the popover's left edge ends - // up at the cursor (down-and-to-the-right of the pointer). - gtk_widget_measure(box, GTK_ORIENTATION_HORIZONTAL, -1, - NULL, &natural_width, NULL, NULL); - rect.width = natural_width > 0 ? natural_width : 1; - rect.height = 1; + rect.width = rect.height = 1; + + // Make sure popover aligns down-and-to-the-right of the pointer. + gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM); + gtk_widget_set_halign(popover, GTK_ALIGN_START); gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect); - g_signal_connect(popover, "closed", + g_signal_connect(GTK_POPOVER(popover), "closed", G_CALLBACK(popupmenu_closed_cb), NULL); gtk_popover_popup(GTK_POPOVER(popover)); } @@ -3977,23 +3867,8 @@ gui_make_popup(char_u *path_name, int mouse_pos) gui_mch_getmouse(&x, &y); else { - // Find the cursor position relative to parent of drawarea - GtkWidget *parent = gtk_widget_get_parent(gui.drawarea); - graphene_point_t point; - if (parent == NULL) - parent = gui.drawarea; - - if (!gtk_widget_compute_point(gui.drawarea, parent, - &GRAPHENE_POINT_INIT(0, 0), &point)) - x = y = 0; - else - { - x = point.x; - y = point.y; - } - - x += FILL_X(curwin->w_wincol + curwin->w_wcol + 1) + 1; - y += FILL_Y(W_WINROW(curwin) + curwin->w_wrow + 1) + 1; + x = FILL_X(curwin->w_wincol + curwin->w_wcol + 1) + 1; + y = FILL_Y(W_WINROW(curwin) + curwin->w_wrow + 1) + 1; } gui_gtk_popup_at(menu, x, y); @@ -4339,7 +4214,6 @@ static int last_text_area_h = 0; * ============================================================ * Menu functions * ============================================================ - * TODO: Implement using GMenu + GtkPopoverMenuBar */ /* @@ -4427,109 +4301,107 @@ create_toolbar_icon(vimmenu_T *menu) return image; } -/* - * GTK4 Menu system using GMenu + GSimpleActionGroup + GtkPopoverMenuBar. - * - * Each menu/submenu has a GMenu stored in menu->submenu_id (cast to - * GtkWidget* to fit the struct field type). - * Actions are added to a GSimpleActionGroup attached to gui.mainwin. - */ - -static int menu_action_id = 0; - static void -menu_action_cb(GSimpleAction *action UNUSED, GVariant *parameter UNUSED, - gpointer data) +menu_button_clicked_cb( + VimMenuItem *item, + VimMenuItemEvent event, + vimmenu_T *menu) { - // Force-close any open popover menus in the menubar. - // GTK4 marks them as not-visible but Vim's custom main loop - // may not process the rendering update, so we flush explicitly. - if (gui.menubar != NULL) + if (event == VIM_MENU_ITEM_CLICKED) + gui_menu_cb(menu); + else if (event == VIM_MENU_ITEM_SELECTED) { - GtkWidget *item; + // Show tooltip instantly in cmdline message. + char_u *tooltip; + static gboolean did_msg = FALSE; - for (item = gtk_widget_get_first_child(gui.menubar); - item != NULL; - item = gtk_widget_get_next_sibling(item)) - { - GtkWidget *child; + if (State & MODE_CMDLINE) + return; - for (child = gtk_widget_get_first_child(item); - child != NULL; - child = gtk_widget_get_next_sibling(child)) - { - if (GTK_IS_POPOVER(child)) - gtk_popover_popdown(GTK_POPOVER(child)); - } + tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]); + if (tooltip != NULL && utf_valid_string(tooltip, NULL)) + { + msg((char *)tooltip); + did_msg = TRUE; + setcursor(); + out_flush_cursor(TRUE, FALSE); } + else if (did_msg) + { + msg(""); + did_msg = FALSE; + setcursor(); + out_flush_cursor(TRUE, FALSE); + } + CONVERT_TO_UTF8_FREE(tooltip); } - - gui_menu_cb((vimmenu_T *)data); - gui_mch_flush(); -} - - static char * -make_action_name(vimmenu_T *menu) -{ - // Create a unique action name from the menu pointer - static char buf[64]; - vim_snprintf(buf, sizeof(buf), "menu%d", menu_action_id++); - return buf; } void -gui_mch_add_menu(vimmenu_T *menu, int idx UNUSED) +gui_mch_add_menu(vimmenu_T *menu, int idx) { - GMenu *submenu; + vimmenu_T *parent; + GtkWidget *parent_widget; + gboolean use_mnemonic; + char_u *text; if (menu->name[0] == ']' || menu_is_popup(menu->name)) { - // Popup menus - just create a GMenu, don't add to menubar - submenu = g_menu_new(); - menu->submenu_id = (GtkWidget *)(gpointer)submenu; + // Attach the popover to drawarea's parent rather than to drawarea + // itself. GtkDrawingArea is a leaf widget whose snapshot does not + // iterate children, and parenting a popover to it has been observed to + // leave the drawing area blank while the popover is open. + menu->submenu_id = g_object_ref_sink(vim_menu_new()); + gtk_widget_set_parent(menu->submenu_id, gui.drawarea); return; } - if (menu->parent != NULL && menu->parent->submenu_id == NULL) - return; - if (!menu_is_menubar(menu->name)) - return; + parent = menu->parent; - // Create a submenu for this menu - submenu = g_menu_new(); - menu->submenu_id = (GtkWidget *)(gpointer)submenu; + if ((parent != NULL && parent->submenu_id == NULL) + || !menu_is_menubar(menu->name)) + return; - // Add to parent menu or menubar's model - { - GMenu *parent_menu; - char_u *label; + menu->submenu_id = g_object_ref_sink(vim_menu_new()); - label = CONVERT_TO_UTF8(menu->dname); + use_mnemonic = parent != NULL || p_wak[0] != 'n'; + text = translate_mnemonic_tag(menu->name, use_mnemonic); - if (menu->parent != NULL) - parent_menu = (GMenu *)(gpointer)menu->parent->submenu_id; - else - parent_menu = (GMenu *)(gpointer)g_object_get_data( - G_OBJECT(gui.menubar), "vim-gmenu"); + if (parent != NULL) + { + parent_widget = parent->submenu_id; + menu->id = g_object_ref_sink(vim_menu_item_new( + (const char *)text, NULL, NULL)); - if (parent_menu != NULL) - g_menu_append_submenu(parent_menu, (const char *)label, - G_MENU_MODEL(submenu)); + vim_menu_item_set_submenu(VIM_MENU_ITEM(menu->id), + VIM_MENU(menu->submenu_id)); + vim_menu_insert_item(VIM_MENU(parent_widget), + VIM_MENU_ITEM(menu->id), idx); + } + else + { + parent_widget = gui.menubar; + menu->id = g_object_ref_sink(vim_menu_bar_item_new( + (const char *)text, VIM_MENU(menu->submenu_id))); - CONVERT_TO_UTF8_FREE(label); + vim_menu_bar_insert_item(VIM_MENU_BAR(parent_widget), + VIM_MENU_BAR_ITEM(menu->id), idx); } + + vim_free(text); } void gui_mch_add_menu_item(vimmenu_T *menu, int idx) { - vimmenu_T *parent = menu->parent; + vimmenu_T *parent = menu->parent; #ifdef FEAT_TOOLBAR if (parent != NULL && menu_is_toolbar(parent->name)) { if (menu_is_separator(menu->name)) { + // TODO menu->id = vim_toolbar_insert_separator(VIM_TOOLBAR(gui.toolbar), idx); } @@ -4565,51 +4437,36 @@ gui_mch_add_menu_item(vimmenu_T *menu, int idx) if (parent == NULL || parent->submenu_id == NULL) return; + if (menu_is_separator(menu->name)) + { + menu->id = g_object_ref_sink(vim_menu_insert_separator( + VIM_MENU(parent->submenu_id), idx)); + } + else { - GMenu *parent_menu = (GMenu *)(gpointer)parent->submenu_id; + char_u *text; + char_u *accel_text = NULL; + gboolean use_mnemonic; - if (menu_is_separator(menu->name)) - { - // GMenu doesn't have real separators; use a section - GMenu *section = g_menu_new(); - g_menu_insert_section(parent_menu, idx, NULL, - G_MENU_MODEL(section)); - g_object_unref(section); - menu->id = NULL; - } - else - { - char *action_name; - char detailed[80]; - char_u *label; - GSimpleAction *action; - - // Create a unique action - action_name = make_action_name(menu); - action = g_simple_action_new(action_name, NULL); - g_signal_connect(action, "activate", - G_CALLBACK(menu_action_cb), menu); - - if (menu_action_group == NULL) - { - menu_action_group = g_simple_action_group_new(); - gtk_widget_insert_action_group(gui.mainwin, "menu", - G_ACTION_GROUP(menu_action_group)); - } - g_action_map_add_action(G_ACTION_MAP(menu_action_group), - G_ACTION(action)); - g_object_unref(action); - - label = CONVERT_TO_UTF8(menu->dname); - vim_snprintf(detailed, sizeof(detailed), "menu.%s", action_name); - g_menu_insert(parent_menu, idx, (const char *)label, detailed); - CONVERT_TO_UTF8_FREE(label); - - menu->id = (GtkWidget *)1; // non-NULL marker - // Store action name for later use (grey/enable) - menu->label = (GtkWidget *)vim_strsave( - (char_u *)action_name); - } + use_mnemonic = p_wak[0] != 'n'; + text = translate_mnemonic_tag(menu->name, use_mnemonic); + + if (menu->actext != NULL && menu->actext[0] != NUL) + accel_text = CONVERT_TO_UTF8(menu->actext); + + // Add our own reference to the widget + menu->id = g_object_ref_sink(vim_menu_item_new((const char *)text, + (VimMenuItemFunc)menu_button_clicked_cb, menu)); + + if (accel_text != NULL) + vim_menu_item_set_accel(VIM_MENU_ITEM(menu->id), + (const char *)accel_text); + + vim_menu_insert_item(VIM_MENU(parent->submenu_id), + VIM_MENU_ITEM(menu->id), idx); + + vim_free(text); + CONVERT_TO_UTF8_FREE(accel_text); } } @@ -4624,7 +4481,7 @@ gui_mch_menu_set_tip(vimmenu_T *menu) { char_u *tooltip; - if (menu->id == NULL || menu->parent == NULL || gui.toolbar == NULL) + if (menu->id == NULL) return; tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]); @@ -4633,104 +4490,29 @@ gui_mch_menu_set_tip(vimmenu_T *menu) CONVERT_TO_UTF8_FREE(tooltip); } -/* - * Return TRUE if "menu" has a corresponding entry in its parent's GMenu. - * Popup menus, toolbar children and orphaned submenus do not. - */ - static int -menu_has_gmenu_slot(vimmenu_T *menu) -{ - if (menu == NULL || menu->name == NULL) - return FALSE; - if (menu->name[0] == ']' || menu_is_popup(menu->name)) - return FALSE; - if (menu->parent != NULL) - { - if (menu_is_toolbar(menu->parent->name)) - return FALSE; - if (menu->parent->submenu_id == NULL) - return FALSE; - return TRUE; - } - return menu_is_menubar(menu->name); -} - -/* - * Find the parent GMenu containing the entry for "menu" and the position of - * that entry. Returns TRUE on success. - */ - static int -get_gmenu_pos_in_parent(vimmenu_T *menu, GMenu **parent_out, int *pos_out) -{ - GMenu *parent_gmenu; - vimmenu_T *first_sibling; - vimmenu_T *sib; - int pos = 0; - - if (!menu_has_gmenu_slot(menu)) - return FALSE; - - if (menu->parent != NULL) - { - parent_gmenu = (GMenu *)(gpointer)menu->parent->submenu_id; - first_sibling = menu->parent->children; - } - else - { - if (gui.menubar == NULL) - return FALSE; - parent_gmenu = (GMenu *)(gpointer)g_object_get_data( - G_OBJECT(gui.menubar), "vim-gmenu"); - first_sibling = root_menu; - } - if (parent_gmenu == NULL) - return FALSE; - - for (sib = first_sibling; sib != NULL && sib != menu; sib = sib->next) - if (menu_has_gmenu_slot(sib)) - pos++; - if (sib != menu) - return FALSE; - - *parent_out = parent_gmenu; - *pos_out = pos; - return TRUE; -} - void gui_mch_destroy_menu(vimmenu_T *menu) { - GMenu *parent_gmenu = NULL; - int pos = 0; - // For toolbar buttons and separators, remove from the toolbar box. - if (menu->id != NULL && menu->id != (GtkWidget *)1) + if (menu->parent != NULL && menu_is_toolbar(menu->parent->name)) { vim_toolbar_remove(VIM_TOOLBAR(gui.toolbar), menu->id); menu->id = NULL; return; } - menu->id = NULL; - // Remove the entry from the parent GMenu so the visible menu updates. - if (get_gmenu_pos_in_parent(menu, &parent_gmenu, &pos)) - g_menu_remove(parent_gmenu, pos); - - // Remove the GAction created for this item and free its name. - if (menu->label != NULL) - { - if (menu_action_group != NULL) - g_action_map_remove_action(G_ACTION_MAP(menu_action_group), - (const char *)menu->label); - VIM_CLEAR(menu->label); - } + // For popup menus, unparent the menu as well + if (menu->name[0] == ']' || menu_is_popup(menu->name)) + gtk_widget_unparent(menu->submenu_id); + else if (menu->parent == NULL) + // Remove from menubar + vim_menu_bar_remove(VIM_MENU_BAR(gui.menubar), menu->id); + else + // Remove from parent menu + vim_menu_remove(VIM_MENU(menu->parent->submenu_id), menu->id); - // Release our reference on the submenu GMenu (if any). - if (menu->submenu_id != NULL) - { - g_object_unref(menu->submenu_id); - menu->submenu_id = NULL; - } + g_clear_object(&menu->submenu_id); + g_clear_object(&menu->id); } void @@ -4745,28 +4527,32 @@ gui_mch_show_popupmenu(vimmenu_T *menu) static void show_menubar_popover(void) { - GMenu *gmenu; - GtkWidget *popover; + GtkWidget *menu; GdkRectangle rect; if (gui.menubar == NULL || gui.drawarea == NULL) return; - gmenu = (GMenu *)g_object_get_data(G_OBJECT(gui.menubar), "vim-gmenu"); - if (gmenu == NULL || g_menu_model_get_n_items(G_MENU_MODEL(gmenu)) == 0) + + if (gtk_widget_is_visible(gui.menubar)) + { + // If menubar is visible, then just show first menu in menubar, like how + // GTK traditionally seems to do it? + vim_menu_bar_show(VIM_MENU_BAR(gui.menubar), NULL); return; + } - popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(gmenu)); - gtk_widget_set_parent(popover, gui.drawarea); - gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); - gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM); + // Copy and convert the menubar into a menu popover + menu = vim_menu_bar_to_menu(VIM_MENU_BAR(gui.menubar)); + + gtk_widget_set_parent(menu, gui.drawarea); + gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM); rect.x = 0; rect.y = 0; rect.width = 1; rect.height = 1; - gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect); - g_signal_connect(popover, "closed", - G_CALLBACK(popupmenu_closed_cb), NULL); - gtk_popover_popup(GTK_POPOVER(popover)); + gtk_popover_set_pointing_to(GTK_POPOVER(menu), &rect); + g_signal_connect(menu, "closed", G_CALLBACK(popupmenu_closed_cb), NULL); + gtk_popover_popup(GTK_POPOVER(menu)); } /* diff --git a/src/gui_gtk4_da.c b/src/gui_gtk4_da.c index 11b7463892..8db224ea67 100644 --- a/src/gui_gtk4_da.c +++ b/src/gui_gtk4_da.c @@ -140,7 +140,9 @@ vim_draw_area_class_init(VimDrawAreaClass *class) obj_class->finalize = vim_draw_area_finalize; + // Add a layout manager so it can handle child popovers gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT); + } static void diff --git a/src/gui_gtk4_menu.c b/src/gui_gtk4_menu.c new file mode 100644 index 0000000000..174f2f772b --- /dev/null +++ b/src/gui_gtk4_menu.c @@ -0,0 +1,1038 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +#include "vim.h" + +#ifdef FEAT_MENU + +#include +#include "gui_gtk4_menu.h" + +// Note that this may return NULL for popup menus +#define GET_MENU_BAR(m) VIM_MENU_BAR(gtk_widget_get_ancestor( \ + GTK_WIDGET(m), VIM_TYPE_MENU_BAR)) + +/* + * Similar as GtkButton but set CSS name to "item" to emulate GtkPopoverMenuBar + * styling. Always has a submenu. + */ +struct _VimMenuBarItem +{ + GtkButton parent; + + GtkWidget *menu; +}; + +G_DEFINE_TYPE(VimMenuBarItem, vim_menu_bar_item, GTK_TYPE_BUTTON) + + static void +vim_menu_bar_item_dispose(GObject *object) +{ + VimMenuBarItem *self = VIM_MENU_BAR_ITEM(object); + + g_clear_pointer((GtkWidget **)&self->menu, gtk_widget_unparent); + + G_OBJECT_CLASS(vim_menu_bar_item_parent_class)->dispose(object); +} + + static void +vim_menu_bar_item_class_init(VimMenuBarItemClass *class) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); + GObjectClass *obj_class = G_OBJECT_CLASS(class); + + obj_class->dispose = vim_menu_bar_item_dispose; + + gtk_widget_class_set_css_name(widget_class, "item"); +} + + static void +vim_menu_bar_item_init(VimMenuBarItem *self) +{ + // Enable mnemonics + gtk_button_set_use_underline(GTK_BUTTON(self), TRUE); +} + + GtkWidget * +vim_menu_bar_item_new(const char *text, VimMenu *menu) +{ + VimMenuBarItem *item = g_object_new(VIM_TYPE_MENU_BAR_ITEM, NULL); + + gtk_button_set_label(GTK_BUTTON(item), text); + + item->menu = GTK_WIDGET(menu); + gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM); + // Make popover start at top left corner + gtk_widget_set_halign(GTK_WIDGET(menu), GTK_ALIGN_START); + gtk_widget_set_parent(GTK_WIDGET(menu), GTK_WIDGET(item)); + + return GTK_WIDGET(item); +} + + void +vim_menu_bar_item_set_text(VimMenuBarItem *self, const char *text) +{ + gtk_button_set_label(GTK_BUTTON(self), text); +} + +/* + * Similar to GtkPopoverMenuBar + */ +struct _VimMenuBar +{ + GtkWidget parent; + + GList *items; + + // Currently visible item that has submenu popped up, else NULL + GtkWidget *active_item; +}; + +G_DEFINE_TYPE(VimMenuBar, vim_menu_bar, GTK_TYPE_WIDGET) + + static void +vim_menu_bar_dispose(GObject *object) +{ + VimMenuBar *self = VIM_MENU_BAR(object); + + g_clear_list(&self->items, (GDestroyNotify)gtk_widget_unparent); + + G_OBJECT_CLASS(vim_menu_bar_parent_class)->dispose(object); +} + + static void +vim_menu_bar_class_init(VimMenuBarClass *class) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); + GObjectClass *obj_class = G_OBJECT_CLASS(class); + + obj_class->dispose = vim_menu_bar_dispose; + + gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BOX_LAYOUT); + gtk_widget_class_set_css_name(widget_class, "menubar"); +} + + static void +vim_menu_bar_init(VimMenuBar *self) +{ + GtkLayoutManager *lm = gtk_widget_get_layout_manager(GTK_WIDGET(self)); + + gtk_orientable_set_orientation(GTK_ORIENTABLE(lm), + GTK_ORIENTATION_HORIZONTAL); + gtk_box_layout_set_spacing(GTK_BOX_LAYOUT(lm), 0); +} + + GtkWidget * +vim_menu_bar_new(void) +{ + return g_object_new(VIM_TYPE_MENU_BAR, NULL); +} + +/* + * Create a VimMenu widget with the menus of the menu bar as its submenus. Note + * that it is a deep copy. + */ + GtkWidget * +vim_menu_bar_to_menu(VimMenuBar *self) +{ + GtkWidget *menu = vim_menu_new(); + int i = 0; + + for (GList *l = self->items; l != NULL; l = l->next, i++) + { + VimMenuBarItem *baritem = l->data; + GtkWidget *item; + + item = vim_menu_item_new( + gtk_button_get_label(GTK_BUTTON(baritem)), NULL, NULL); + + vim_menu_item_set_submenu(VIM_MENU_ITEM(item), + VIM_MENU(vim_menu_copy(VIM_MENU(baritem->menu)))); + + vim_menu_insert_item(VIM_MENU(menu), VIM_MENU_ITEM(item), i); + } + return menu; +} + +/* + * Set the currently active menu of the menubar to "item". If NULL, then close + * any submenus. + */ + static void +vim_menu_bar_set_active_item( + VimMenuBar *self, + VimMenuBarItem *item, + gboolean force) +{ + // Do nothing if currently active item is "item", or if there is not + // currently active item. User must click a menu item first for menus to + // automatically appear on hover. This is unless "force" is TRUE. + // + // Only make item selected if there is no active item (no submenu open), or + // if the item was set as the active item.. + if ((!force && self->active_item == NULL) + || self->active_item == GTK_WIDGET(item)) + { + if (self->active_item == NULL) + gtk_widget_set_state_flags(GTK_WIDGET(item), + GTK_STATE_FLAG_SELECTED, FALSE); + return; + } + + if (self->active_item != NULL) + { + // Call this before popdown, since "closed" signal may be emitted + // immediately. + gtk_widget_unset_state_flags(self->active_item, + GTK_STATE_FLAG_SELECTED); + gtk_popover_popdown(GTK_POPOVER( + VIM_MENU_BAR_ITEM(self->active_item)->menu) + ); + } + + self->active_item = GTK_WIDGET(item); + if (item != NULL) + { + gtk_popover_popup(GTK_POPOVER(item->menu)); + gtk_widget_set_state_flags(GTK_WIDGET(item), + GTK_STATE_FLAG_SELECTED, FALSE); + } +} + + static void +vim_menu_bar_item_enter_cb( + GtkEventController *controller, + double x UNUSED, + double y UNUSED, + VimMenuBar *menubar) +{ + VimMenuBarItem *self; + + self = VIM_MENU_BAR_ITEM(gtk_event_controller_get_widget(controller)); + vim_menu_bar_set_active_item(menubar, self, FALSE); +} + + static void +vim_menu_bar_item_leave_cb( + GtkEventController *controller, + VimMenuBar *menubar) +{ + VimMenuBarItem *self; + + self = VIM_MENU_BAR_ITEM(gtk_event_controller_get_widget(controller)); + + // If the item is the currently active item, then don't deselect it. + if (menubar->active_item != GTK_WIDGET(self)) + gtk_widget_unset_state_flags(GTK_WIDGET(self), + GTK_STATE_FLAG_SELECTED); +} + + static void +vim_menu_bar_item_clicked_cb(VimMenuBarItem *self, VimMenuBar *menubar) +{ + vim_menu_bar_set_active_item(menubar, self, TRUE); +} + + static void +vim_menu_bar_item_menu_closed_cb(VimMenu *menu UNUSED, VimMenuBar *menubar) +{ + if (menubar->active_item != NULL) + gtk_widget_unset_state_flags(GTK_WIDGET(menubar->active_item), + GTK_STATE_FLAG_SELECTED); + vim_menu_bar_set_active_item(menubar, NULL, TRUE); + // Make sure to focus drawarea + gtk_widget_grab_focus(gui.drawarea); +} + +/* + * Insert the menu item at the given index in the menu bar. + */ + void +vim_menu_bar_insert_item(VimMenuBar *self, VimMenuBarItem *item, int idx) +{ + GtkEventController *controller; + GList *next_sibling; + + next_sibling = g_list_nth(self->items, idx); + gtk_widget_insert_before(GTK_WIDGET(item), GTK_WIDGET(self), + next_sibling == NULL ? NULL : next_sibling->data); + + self->items = g_list_insert(self->items, item, idx); + + controller = gtk_event_controller_motion_new(); + g_signal_connect_object(controller, "enter", + G_CALLBACK(vim_menu_bar_item_enter_cb), self, G_CONNECT_DEFAULT); + g_signal_connect_object(controller, "leave", + G_CALLBACK(vim_menu_bar_item_leave_cb), self, G_CONNECT_DEFAULT); + gtk_widget_add_controller(GTK_WIDGET(item), controller); + + g_signal_connect_object(item, "clicked", + G_CALLBACK(vim_menu_bar_item_clicked_cb), self, G_CONNECT_DEFAULT); + + g_signal_connect_object(item->menu, "closed", + G_CALLBACK(vim_menu_bar_item_menu_closed_cb), + self, G_CONNECT_DEFAULT); +} + +/* + * Remove the menu item or separator from the menu bar + */ + void +vim_menu_bar_remove(VimMenuBar *self, GtkWidget *item) +{ + self->items = g_list_remove(self->items, item); + gtk_widget_unparent(item); +} + +/* + * Show the given menu in the menubar. If "item" is NULL, then show first menu. + */ + void +vim_menu_bar_show(VimMenuBar *self, VimMenuBarItem *item) +{ + if (item == NULL) + item = g_list_nth_data(self->items, 0); + + vim_menu_bar_set_active_item(self, item, TRUE); +} + +/* + * If "dir" is negative, then move to the item previous of currently the active + * item. If "dir" is positive, then move to the next item. Return the resulting + * item or NULL if there are no suitable ones. + */ + static GtkWidget * +vim_menu_bar_move_active_item(VimMenuBar *self, int dir) +{ + GtkWidget *(*func)(GtkWidget *); + GtkWidget *(*null_func)(GtkWidget *); + GtkWidget *widget = self->active_item; + + if (widget == NULL) + return gtk_widget_get_first_child(GTK_WIDGET(self)); + + if (dir > 0) + { + func = gtk_widget_get_next_sibling; + null_func = gtk_widget_get_first_child; + } + else + { + func = gtk_widget_get_prev_sibling; + null_func = gtk_widget_get_last_child; + } + + while (TRUE) + { + widget = func(widget); + if (widget == NULL) + { + if (null_func == NULL) + break; + widget = null_func(GTK_WIDGET(self)); + null_func = NULL; + if (widget == NULL) + break; + } + break; + } + return widget; +} + +/* + * Menu button that can be used to perform actions, or if there is a submenu, + * toggle the state of the submenu popover. CSS name is "modelbutton" to make it + * styled like GtkPopoverMenu + */ +struct _VimMenuItem +{ + GtkButton parent; + + GtkWidget *label; // Displays text for button. + GtkWidget *aux_widget; // Either an icon or a label showing the accelerator + // text. + + GtkWidget *submenu; // Submenu popover if any (VimMenu) + + // Callback called when clicked or selected, we store this so that copying a + // menu item works properly. + VimMenuItemFunc func; + void *func_udata; +}; + +G_DEFINE_TYPE(VimMenuItem, vim_menu_item, GTK_TYPE_BUTTON) + + static void +vim_menu_item_dispose(GObject *object) +{ + VimMenuItem *self = VIM_MENU_ITEM(object); + + g_clear_pointer(&self->label, gtk_widget_unparent); + g_clear_pointer(&self->aux_widget, gtk_widget_unparent); + g_clear_pointer((GtkWidget **)&self->submenu, gtk_widget_unparent); + + G_OBJECT_CLASS(vim_menu_item_parent_class)->dispose(object); +} + + static void +vim_menu_item_class_init(VimMenuItemClass *class) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); + GObjectClass *obj_class = G_OBJECT_CLASS(class); + + obj_class->dispose = vim_menu_item_dispose; + + gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BOX_LAYOUT); + gtk_widget_class_set_css_name(widget_class, "modelbutton"); +} + + static void +vim_menu_item_init(VimMenuItem *self) +{ + GtkLayoutManager *lm = gtk_widget_get_layout_manager(GTK_WIDGET(self)); + + gtk_orientable_set_orientation(GTK_ORIENTABLE(lm), + GTK_ORIENTATION_HORIZONTAL); + gtk_box_layout_set_spacing(GTK_BOX_LAYOUT(lm), 0); +} + +/* + * Create a new menu item with the given text to display. "func" may be NULL if + * not needed. + */ + GtkWidget * +vim_menu_item_new(const char *text, VimMenuItemFunc func, void *udata) +{ + VimMenuItem *item = g_object_new(VIM_TYPE_MENU_ITEM, NULL); + + item->func = func; + item->func_udata = udata; + item->label = gtk_label_new_with_mnemonic(text); + + // Make sure label is on the right and pushes everything to the left + gtk_widget_set_halign(item->label, GTK_ALIGN_START); + gtk_widget_set_hexpand(item->label, TRUE); + gtk_widget_set_parent(item->label, GTK_WIDGET(item)); + + return GTK_WIDGET(item); +} + +/* + * Update displayed text for menu item + */ + void +vim_menu_item_set_text(VimMenuItem *self, const char *text) +{ + gtk_label_set_text_with_mnemonic(GTK_LABEL(self->label), text); +} + + static void +vim_menu_item_set_aux_widget(VimMenuItem *self, GtkWidget *aux) +{ + self->aux_widget = aux; + gtk_widget_set_halign(self->aux_widget, GTK_ALIGN_END); + gtk_widget_set_hexpand(self->aux_widget, FALSE); + gtk_widget_set_margin_start(self->aux_widget, 50); +} + +/* + * Set the accelerator text for the menu item. + */ + void +vim_menu_item_set_accel(VimMenuItem *self, const char *accel_text) +{ + assert(self->aux_widget == NULL); + + vim_menu_item_set_aux_widget(self, gtk_label_new(accel_text)); + gtk_widget_insert_after(self->aux_widget, GTK_WIDGET(self), self->label); +} + +/* + * Set the submenu popover for the menu item + */ + void +vim_menu_item_set_submenu(VimMenuItem *self, VimMenu *submenu) +{ + GtkWidget *icon; + + assert(self->submenu == NULL); + assert(self->aux_widget == NULL); + + // Add arrow icon pointing to right + icon = gtk_image_new_from_icon_name("pan-end-symbolic"); + vim_menu_item_set_aux_widget(self, icon); + gtk_widget_insert_after(self->aux_widget, GTK_WIDGET(self), self->label); + + gtk_popover_set_position(GTK_POPOVER(submenu), GTK_POS_RIGHT); + // Make top of popover be aligned with button. + gtk_widget_set_valign(GTK_WIDGET(submenu), GTK_ALIGN_START); + + self->submenu = GTK_WIDGET(submenu); + gtk_widget_set_parent(GTK_WIDGET(submenu), GTK_WIDGET(self)); +} + +/* + * Create a deep copy of the menu item + */ + static GtkWidget * +vim_menu_item_copy(VimMenuItem *self) +{ + GtkWidget *copy; + + copy = vim_menu_item_new( + gtk_label_get_text(GTK_LABEL(self->label)), + self->func, self->func_udata); + + if (self->submenu != NULL) + vim_menu_item_set_submenu(VIM_MENU_ITEM(copy), + VIM_MENU(vim_menu_copy(VIM_MENU(self->submenu)))); + else if (self->aux_widget != NULL) + vim_menu_item_set_accel(VIM_MENU_ITEM(copy), + gtk_label_get_text(GTK_LABEL(self->aux_widget))); + return copy; +} + +/* + * Similar to GtkPopoverMenu, except uses GtkWidgets directly like GTK3, instead + * of abstracting it into GMenuModel. + */ +struct _VimMenu +{ + GtkPopover parent; + + GtkWidget *box; + GtkWidget *scr; + + GList *items; + + // Currently active item showing submenu popover, or being hovered on, or + // NULL. Note that item may have submenu but not have it open, when + // navigating via keyboard. + GtkWidget *active_item; + + // Used when mouse is hovering over an item and user is navigating with + // keyboard. When the scrolled window scrolls down or up, this causes a + // mouse enter event, causing the active item to go to the item that the + // mouse is hovered on, instead of the next item (from keyboard navigation). + gboolean ignore_hover; + double prev_x; + double prev_y; +}; + +G_DEFINE_TYPE(VimMenu, vim_menu, GTK_TYPE_POPOVER) + + static void +vim_menu_dispose(GObject *object) +{ + VimMenu *self = VIM_MENU(object); + + g_clear_list(&self->items, (GDestroyNotify)gtk_widget_unparent); + g_clear_pointer(&self->scr, gtk_widget_unparent); + + G_OBJECT_CLASS(vim_menu_parent_class)->dispose(object); +} + + static void +vim_menu_class_init(VimMenuClass *class) +{ + GObjectClass *obj_class = G_OBJECT_CLASS(class); + + obj_class->dispose = vim_menu_dispose; +} + + static gboolean +vim_menu_select_active_item(VimMenu *self, gboolean open) +{ + VimMenuItem *item; + + gtk_widget_set_state_flags(self->active_item, + GTK_STATE_FLAG_SELECTED, FALSE); + + // Make sure to focus item, so that scrolled window knows what to do. + gtk_widget_grab_focus(GTK_WIDGET(self->active_item)); + + item = VIM_MENU_ITEM(self->active_item); + if (item->func != NULL) + item->func(item, VIM_MENU_ITEM_SELECTED, item->func_udata); + + if (open && VIM_MENU_ITEM(self->active_item)->submenu != NULL) + { + GtkWidget *submenu = VIM_MENU_ITEM(self->active_item)->submenu; + gtk_popover_popup(GTK_POPOVER(submenu)); + return TRUE; + } + return FALSE; +} + +/* + * Set the active item of the menu to "item". If "item" is NULL, then close any + * submenus. If "open" is FALSE, then don't open the submenu if any. + */ + static void +vim_menu_set_active_item(VimMenu *self, VimMenuItem *item, gboolean open) +{ + if (self->active_item == GTK_WIDGET(item)) + return; + + if (self->active_item != NULL) + { + if (VIM_MENU_ITEM(self->active_item)->submenu != NULL) + gtk_popover_popdown(GTK_POPOVER( + VIM_MENU_ITEM(self->active_item)->submenu + )); + gtk_widget_unset_state_flags(GTK_WIDGET(self->active_item), + GTK_STATE_FLAG_SELECTED); + } + + self->active_item = GTK_WIDGET(item); + if (item != NULL) + (void)vim_menu_select_active_item(self, open); +} + + static void +vim_menu_closed_cb(VimMenu *self, void *udata UNUSED) +{ + vim_menu_set_active_item(self, NULL, FALSE); +} + +/* + * If "dir" is negative, then move to the item previous of currently the active + * item. If "dir" is positive, then move to the next item. Return the resulting + * item or NULL if there are no suitable ones. + */ + static GtkWidget * +vim_menu_move_active_item(VimMenu *self, int dir) +{ + GtkWidget *(*func)(GtkWidget *); + GtkWidget *(*null_func)(GtkWidget *); + GtkWidget *widget = self->active_item; + + // If there is no currently active item, then just use the first one + if (widget == NULL) + { + widget = gtk_widget_get_first_child(self->box); + while (widget != NULL && !VIM_IS_MENU_ITEM(widget)) + widget = gtk_widget_get_next_sibling(widget); + return widget; + } + + // Could also just use GList functions, but this seems simpler (no + // difference anyways). + if (dir > 0) + { + func = gtk_widget_get_next_sibling; + null_func = gtk_widget_get_first_child; + } + else + { + func = gtk_widget_get_prev_sibling; + null_func = gtk_widget_get_last_child; + } + + while (TRUE) + { + widget = func(widget); + if (widget == NULL) + { + if (null_func == NULL) + break; + widget = null_func(self->box); + null_func = NULL; + if (widget == NULL) + break; + } + if (VIM_IS_MENU_ITEM(widget)) + break; + } + return widget; +} + + static void +vim_menu_reset_parent_prelight(VimMenu *self) +{ + GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(self)); + VimMenu *parent_menu; + + // gtk_widget_get_ancestor assumes the widget itself is also an ancestor, so + // we must get parent of menu first. + parent_menu = VIM_MENU(gtk_widget_get_ancestor(parent, VIM_TYPE_MENU)); + + if (parent_menu == NULL) + // TRUE for popup menus + return; + + if (parent_menu->active_item != NULL) + gtk_widget_unset_state_flags(GTK_WIDGET(parent_menu->active_item), + GTK_STATE_FLAG_PRELIGHT); +} + +/* + * Close all submenus in the menubar given a menu widget + */ + static void +vim_menu_close_all(VimMenu *self) +{ + VimMenuBar *menubar = GET_MENU_BAR(self); + + // Must check if NULL, because popup menus don't have a parent. + if (menubar != NULL) + vim_menu_bar_set_active_item(menubar, NULL, TRUE); + else + gtk_popover_popdown(GTK_POPOVER(self)); + + // Grab focus after popup menus without a menubar are closed + gtk_widget_grab_focus(gui.drawarea); +} + + static gboolean +vim_menu_key_pressed_cb( + GtkEventController *controller UNUSED, + guint keyval, + guint keycode UNUSED, + GdkModifierType state, + VimMenu *self) +{ + GtkWidget *widget; + + switch (keyval) + { + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_ISO_Left_Tab: + // Go to the previous or next item if any + widget = vim_menu_move_active_item(self, + (state & GDK_SHIFT_MASK) + || keyval == GDK_KEY_Up ? -1 : 1); + vim_menu_set_active_item(self, VIM_MENU_ITEM(widget), FALSE); + self->ignore_hover = TRUE; + return TRUE; + case GDK_KEY_Left: + // Pressing control switches menu bar item. + if (state & GDK_CONTROL_MASK) + { + VimMenuBar *menubar = GET_MENU_BAR(self); + + if (menubar != NULL) + { + widget = vim_menu_bar_move_active_item(menubar, -1); + vim_menu_bar_set_active_item(menubar, + VIM_MENU_BAR_ITEM(widget), TRUE); + } + return TRUE; + } + // Go to parent menu (if any). We can do this by just closing the + // popover. + gtk_popover_popdown(GTK_POPOVER(self)); + // For some reason when pointer is hovered over draw area, the + // active item in the parent menu will stay prelighted even when the + // active item is moved. + vim_menu_reset_parent_prelight(self); + return TRUE; + case GDK_KEY_Right: + if (state & GDK_CONTROL_MASK) + { + VimMenuBar *menubar = GET_MENU_BAR(self); + + if (menubar != NULL) + { + widget = vim_menu_bar_move_active_item(menubar, 1); + vim_menu_bar_set_active_item(menubar, + VIM_MENU_BAR_ITEM(widget), TRUE); + } + return TRUE; + } + // Open submenu if active item has one + if (self->active_item != NULL + && vim_menu_select_active_item(self, TRUE)) + { + // Select first item in opened submenu + VimMenu *submenu = VIM_MENU( + VIM_MENU_ITEM(self->active_item)->submenu + ); + + vim_menu_set_active_item(submenu, + VIM_MENU_ITEM( + gtk_widget_get_first_child(submenu->box)), + FALSE); + } + self->ignore_hover = TRUE; + return TRUE; + case GDK_KEY_Escape: + // Close all popover menus + vim_menu_close_all(self); + return TRUE; + case GDK_KEY_ISO_Enter: + case GDK_KEY_3270_Enter: + case GDK_KEY_KP_Enter: + case GDK_KEY_Return: + if (self->active_item != NULL) + g_signal_emit_by_name(self->active_item, "clicked"); + return TRUE; + default: + break; + } + return FALSE; +} + + + static void +vim_menu_motion_cb( + GtkEventController *controller UNUSED, + double x, + double y, + VimMenu *self) +{ + if (self->prev_x == -1 || self->prev_y == -1 || + (fabs(self->prev_x - x) > 0.05 && fabs(self->prev_y - y) > 0.05)) + self->ignore_hover = FALSE; + self->prev_x = x; + self->prev_y = y; +} + + static void +vim_menu_focus_cb(GtkEventController *controller UNUSED, VimMenu *self) +{ + gtk_popover_set_mnemonics_visible(GTK_POPOVER(self), TRUE); +} + + static void +vim_menu_init(VimMenu *self) +{ + GtkEventController *controller; + GtkWidget *stack; + GtkWidget *parent_box; + GListModel *controllers; + + gtk_popover_set_has_arrow(GTK_POPOVER(self), FALSE); + gtk_popover_set_autohide(GTK_POPOVER(self), TRUE); + + // Do not make child popovers close parent popovers when they are closed. + gtk_popover_set_cascade_popdown(GTK_POPOVER(self), FALSE); + + stack = gtk_stack_new(); + + // "stack" and "parent_box" have no use other than to make the css structure + // of the popup menu be exactly like GtkPopoverMenu. This is so that GTK + // themes style VimMenu exactly like GtkPopoverMenu. + parent_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_stack_add_child(GTK_STACK(stack), parent_box); + gtk_stack_set_visible_child(GTK_STACK(stack), parent_box); + + self->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(self->box, TRUE); + gtk_widget_set_vexpand(self->box, TRUE); + gtk_box_append(GTK_BOX(parent_box), self->box); + + self->scr = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->scr), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_propagate_natural_width( + GTK_SCROLLED_WINDOW(self->scr), TRUE); + gtk_scrolled_window_set_propagate_natural_height( + GTK_SCROLLED_WINDOW(self->scr), TRUE); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scr), stack); + gtk_popover_set_child(GTK_POPOVER(self), self->scr); + + gtk_widget_add_css_class(GTK_WIDGET(self), "menu"); + + // Add key controller for basic movement + controller = gtk_event_controller_key_new(); + // Make sure we get the key presses first and handle them if possible + gtk_event_controller_set_propagation_phase(controller, + GTK_PHASE_CAPTURE); + g_signal_connect_object(controller, "key-pressed", + G_CALLBACK(vim_menu_key_pressed_cb), + self, G_CONNECT_DEFAULT); + gtk_widget_add_controller(GTK_WIDGET(self), controller); + + // Show mnemonic underline always + controller = gtk_event_controller_focus_new(); + g_signal_connect_object(controller, "enter", + G_CALLBACK(vim_menu_focus_cb), self, G_CONNECT_DEFAULT); + gtk_widget_add_controller(GTK_WIDGET(self), controller); + + controller = gtk_event_controller_motion_new(); + g_signal_connect_object(controller, "motion", + G_CALLBACK(vim_menu_motion_cb), self, G_CONNECT_DEFAULT); + gtk_widget_add_controller(GTK_WIDGET(self), controller); + + g_signal_connect(self, "closed", G_CALLBACK(vim_menu_closed_cb), NULL); + + // Set all shortcut controllers in the window to not require a modifier for + // mnemonics. + controllers = gtk_widget_observe_controllers(GTK_WIDGET(self)); + for (int i = 0; i < g_list_model_get_n_items(controllers); i++) + { + controller = g_list_model_get_item(controllers, i); + if (GTK_IS_SHORTCUT_CONTROLLER(controller)) + gtk_shortcut_controller_set_mnemonics_modifiers( + GTK_SHORTCUT_CONTROLLER(controller), 0); + } + g_object_unref(controllers); + + self->prev_x = self->prev_y = -1; +} + + GtkWidget * +vim_menu_new(void) +{ + return g_object_new(VIM_TYPE_MENU, NULL); +} + + static GtkWidget * +vim_menu_insert(VimMenu *self, GtkWidget *item, int idx) +{ + if (idx > 0) + { + GList *prev = g_list_nth(self->items, idx - 1); + if (prev == NULL) + gtk_box_append(GTK_BOX(self->box), item); + else + gtk_box_insert_child_after(GTK_BOX(self->box), item, + prev->data); + } + else if (idx == 0) + gtk_box_prepend(GTK_BOX(self->box), item); + else + gtk_box_append(GTK_BOX(self->box), item); + + self->items = g_list_insert(self->items, item, idx); + return item; +} + + static void +vim_menu_item_clicked_cb(VimMenuItem *self, VimMenu *menu) +{ + // Only close all menus if item is a regular button (no submenu). If item + // has a submenu, then just toggle it on and off. + if (self->submenu != NULL) + { + if (gtk_widget_is_visible(self->submenu)) + gtk_popover_popdown(GTK_POPOVER(self->submenu)); + else + gtk_popover_popup(GTK_POPOVER(self->submenu)); + return; + } + + // Since we set the "cascade-popdown" property to FALSE, we must popdown the + // toplevel menu/popover, so that all submenus are closed. + vim_menu_close_all(menu); + if (self->func != NULL) + self->func(self, VIM_MENU_ITEM_CLICKED, self->func_udata); +} + + static void +vim_menu_item_enter_cb( + GtkEventController *controller, + double x UNUSED, + double y UNUSED, + VimMenu *menu) +{ + VimMenuItem *self; + + if (menu->ignore_hover || !gtk_event_controller_motion_contains_pointer( + GTK_EVENT_CONTROLLER_MOTION(controller))) + return; + + self = VIM_MENU_ITEM(gtk_event_controller_get_widget(controller)); + vim_menu_set_active_item(menu, self, TRUE); +} + + static void +vim_menu_item_leave_cb(GtkEventController *controller, VimMenu *menu) +{ + VimMenuItem *self; + + if (gtk_event_controller_motion_contains_pointer( + GTK_EVENT_CONTROLLER_MOTION(controller))) + return; + + self = VIM_MENU_ITEM(gtk_event_controller_get_widget(controller)); + if (menu->active_item == GTK_WIDGET(self)) + vim_menu_set_active_item(menu, NULL, FALSE); +} + +/* + * Insert the menu item at the given index in the menu. If "idx" is negative, + * then append the menu item. + */ + void +vim_menu_insert_item(VimMenu *self, VimMenuItem *item, int idx) +{ + GtkEventController *controller; + + vim_menu_insert(self, GTK_WIDGET(item), idx); + + controller = gtk_event_controller_motion_new(); + g_signal_connect_object(controller, "enter", + G_CALLBACK(vim_menu_item_enter_cb), self, G_CONNECT_DEFAULT); + g_signal_connect_object(controller, "leave", + G_CALLBACK(vim_menu_item_leave_cb), self, G_CONNECT_DEFAULT); + gtk_widget_add_controller(GTK_WIDGET(item), controller); + + g_signal_connect_object(item, "clicked", + G_CALLBACK(vim_menu_item_clicked_cb), + self, G_CONNECT_DEFAULT); +} + +/* + * Insert a separator at the given position and return it. + */ + GtkWidget * +vim_menu_insert_separator(VimMenu *self, int idx) +{ + return vim_menu_insert(self, + gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), idx); +} + +/* + * Remove the menu item or separator from the menu + */ + void +vim_menu_remove(VimMenu *self, GtkWidget *item) +{ + self->items = g_list_remove(self->items, item); + gtk_box_remove(GTK_BOX(self->box), item); +} + +/* + * Create a deep copy of the menu + */ + GtkWidget * +vim_menu_copy(VimMenu *self) +{ + GtkWidget *copy = vim_menu_new(); + int i = 0; + + for (GList *l = self->items; l != NULL; l = l->next, i++) + { + VimMenuItem *item; + GtkWidget *item_copy; + + if (!VIM_IS_MENU_ITEM(l->data)) + { + assert(GTK_IS_SEPARATOR(l->data)); + vim_menu_insert_separator(VIM_MENU(copy), i); + continue; + } + + item = l->data; + item_copy = vim_menu_item_copy(item); + + vim_menu_insert_item(VIM_MENU(copy), VIM_MENU_ITEM(item_copy), i); + } + return copy; +} + +#endif // FEAT_MENU diff --git a/src/gui_gtk4_menu.h b/src/gui_gtk4_menu.h new file mode 100644 index 0000000000..f6965e6dc0 --- /dev/null +++ b/src/gui_gtk4_menu.h @@ -0,0 +1,61 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +#ifndef GUI_GTK4_MENU_H +#define GUI_GTK4_MENU_H + +#include "vim.h" + +#ifdef FEAT_MENU + +# include + +# define VIM_TYPE_MENU_BAR_ITEM (vim_menu_bar_item_get_type()) +G_DECLARE_FINAL_TYPE(VimMenuBarItem, vim_menu_bar_item, VIM, MENU_BAR_ITEM, GtkButton) + +# define VIM_TYPE_MENU_BAR (vim_menu_bar_get_type()) +G_DECLARE_FINAL_TYPE(VimMenuBar, vim_menu_bar, VIM, MENU_BAR, GtkWidget) + +# define VIM_TYPE_MENU_ITEM (vim_menu_item_get_type()) +G_DECLARE_FINAL_TYPE(VimMenuItem, vim_menu_item, VIM, MENU_ITEM, GtkButton) + +# define VIM_TYPE_MENU (vim_menu_get_type()) +G_DECLARE_FINAL_TYPE(VimMenu, vim_menu, VIM, MENU, GtkPopover) + +typedef enum +{ + VIM_MENU_ITEM_CLICKED, + VIM_MENU_ITEM_SELECTED +} VimMenuItemEvent; + +typedef void (*VimMenuItemFunc)(VimMenuItem *item, VimMenuItemEvent event, void *udata); + +GtkWidget *vim_menu_bar_item_new(const char *text, VimMenu *menu); +void vim_menu_bar_item_set_text(VimMenuBarItem *self, const char *text); + +GtkWidget *vim_menu_bar_new(void); +GtkWidget *vim_menu_bar_to_menu(VimMenuBar *self); +void vim_menu_bar_insert_item(VimMenuBar *self, VimMenuBarItem *item, int idx); +void vim_menu_bar_remove(VimMenuBar *self, GtkWidget *item); +void vim_menu_bar_show(VimMenuBar *self, VimMenuBarItem *item); + +GtkWidget *vim_menu_item_new(const char *text, VimMenuItemFunc func, void *udata); +void vim_menu_item_set_text(VimMenuItem *self, const char *text); +void vim_menu_item_set_accel(VimMenuItem *self, const char *accel_text); +void vim_menu_item_set_submenu(VimMenuItem *self, VimMenu *submenu); + +GtkWidget *vim_menu_new(void); +void vim_menu_insert_item(VimMenu *self, VimMenuItem *item, int idx); +GtkWidget *vim_menu_insert_separator(VimMenu *self, int idx); +void vim_menu_remove(VimMenu *self, GtkWidget *item); +GtkWidget *vim_menu_copy(VimMenu *self); + +#endif + +#endif diff --git a/src/gui_gtk4_tb.c b/src/gui_gtk4_tb.c index 79682cfb13..1971a4728e 100644 --- a/src/gui_gtk4_tb.c +++ b/src/gui_gtk4_tb.c @@ -92,6 +92,8 @@ vim_toolbar_init(VimToolbar *self) self->overflow_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); gtk_popover_set_child(GTK_POPOVER(popover), self->overflow_box); gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); + // Make sure popover aligns to top right of button + gtk_widget_set_halign(popover, GTK_ALIGN_END); gtk_menu_button_set_popover(GTK_MENU_BUTTON(self->overflow_btn), popover); } diff --git a/src/menu.c b/src/menu.c index 5fd83d80b5..861d787112 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1074,10 +1074,6 @@ free_menu(vimmenu_T **menup) // Also may rebuild a tearoff'ed menu if (gui.in_use) gui_mch_destroy_menu(menu); -# ifdef USE_GTK4 - // GTK4 uses "menu->label" for action name - vim_free((char_u *)menu->label); -# endif #endif // Don't change *menup until after calling gui_mch_destroy_menu(). The diff --git a/src/structs.h b/src/structs.h index 70010567d2..b112c6d182 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4733,7 +4733,9 @@ struct VimMenu # if defined(GTK_CHECK_VERSION) && !GTK_CHECK_VERSION(3,4,0) GtkWidget *tearoff_handle; # endif +# ifndef USE_GTK4 GtkWidget *label; // Used by "set wak=" code. +# endif # endif # ifdef FEAT_GUI_MOTIF int sensitive; // turn button on/off diff --git a/src/version.c b/src/version.c index 7fd7b6b801..8d517c22f8 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 719, /**/ 718, /**/ From 03a8a13e93d6c2a96c74bb634289de79ee7e97d3 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 18:29:23 +0000 Subject: [PATCH 28/33] patch 9.2.0720: GTK4: no support for browsefilter Problem: GTK4: no support for browsefilter Solution: Add browsefilter support (Foxe Chen) closes: #20612 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui_gtk.c | 43 ++++++++++++++++++++----------------- src/gui_gtk4.c | 58 +++++++++++++++++++++++++++++++++++++++++++++----- src/version.c | 2 ++ 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/gui_gtk.c b/src/gui_gtk.c index 84eae6d41b..13eaf70a73 100644 --- a/src/gui_gtk.c +++ b/src/gui_gtk.c @@ -1270,32 +1270,35 @@ gui_mch_browse(int saving, gfilter = gtk_file_filter_new(); patt = alloc(STRLEN(filter)); - while (p != NULL && *p != NUL) + if (patt != NULL) { - if (*p == '\n' || *p == ';' || *p == '\t') + while (p != NULL && *p != NUL) { - STRNCPY(patt, filter, i); - patt[i] = '\0'; - if (*p == '\t') - gtk_file_filter_set_name(gfilter, (gchar *)patt); - else + if (*p == '\n' || *p == ';' || *p == '\t') { - gtk_file_filter_add_pattern(gfilter, (gchar *)patt); - if (*p == '\n') + STRNCPY(patt, filter, i); + patt[i] = '\0'; + if (*p == '\t') + gtk_file_filter_set_name(gfilter, (gchar *)patt); + else { - gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fc), - gfilter); - if (*(p + 1) != NUL) - gfilter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(gfilter, (gchar *)patt); + if (*p == '\n') + { + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fc), + gfilter); + if (*(p + 1) != NUL) + gfilter = gtk_file_filter_new(); + } } + filter = ++p; + i = 0; + } + else + { + p++; + i++; } - filter = ++p; - i = 0; - } - else - { - p++; - i++; } } vim_free(patt); diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index b5f1f93b40..a193233a90 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -4805,12 +4805,13 @@ gui_mch_browse(int saving, char_u *dflt, char_u *ext UNUSED, char_u *initdir, - char_u *filter UNUSED) + char_u *filter) { - GtkFileDialog *dlg; - FileDialogData fdd; - char_u dirbuf[MAXPATHL]; - char_u *result = NULL; + GtkFileDialog *dlg; + FileDialogData fdd; + char_u dirbuf[MAXPATHL]; + char_u *result = NULL; + GListStore *filters; title = CONVERT_TO_UTF8(title); @@ -4833,6 +4834,53 @@ gui_mch_browse(int saving, g_object_unref(dir); } + // Add file filters + filters = g_list_store_new(GTK_TYPE_FILE_FILTER); + if (filter != NULL && *filter != NUL) + { + int i = 0; + char_u *patt; + char_u *p = filter; + GtkFileFilter *gfilter; + + gfilter = gtk_file_filter_new(); + patt = alloc(STRLEN(filter)); + if (patt != NULL) + { + while (p != NULL && *p != NUL) + { + if (*p == '\n' || *p == ';' || *p == '\t') + { + STRNCPY(patt, filter, i); + patt[i] = '\0'; + if (*p == '\t') + gtk_file_filter_set_name(gfilter, (gchar *)patt); + else + { + gtk_file_filter_add_pattern(gfilter, (gchar *)patt); + if (*p == '\n') + { + g_list_store_append(filters, gfilter); + g_object_unref(gfilter); + if (*(p + 1) != NUL) + gfilter = gtk_file_filter_new(); + } + } + filter = ++p; + i = 0; + } + else + { + p++; + i++; + } + } + } + vim_free(patt); + } + gtk_file_dialog_set_filters(dlg, G_LIST_MODEL(filters)); + g_object_unref(filters); + if (saving && dflt != NULL && *dflt != NUL) gtk_file_dialog_set_initial_name(dlg, (const char *)dflt); diff --git a/src/version.c b/src/version.c index 8d517c22f8..77fdc87d88 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 720, /**/ 719, /**/ From 37d9805675abc6c75fb44fabd510df5313f486b0 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 18:40:50 +0000 Subject: [PATCH 29/33] patch 9.2.0721: serverlist() returns strings separated by \n Problem: serverlist() returns strings separated by \n (Christian J. Robinson) Solution: Return a list of server names when given the option dict argument (Foxe Chen). fixes: #20582 closes: #20601 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- runtime/doc/builtin.txt | 18 ++++++-- runtime/doc/version9.txt | 1 + src/clientserver.c | 74 +++++++++++++++++++++++-------- src/evalfunc.c | 4 +- src/if_xcmdsrv.c | 19 ++++---- src/os_mswin.c | 18 ++++---- src/proto/if_xcmdsrv.pro | 2 +- src/proto/os_mswin.pro | 2 +- src/proto/socketserver.pro | 2 +- src/socketserver.c | 22 ++++----- src/testdir/test_clientserver.vim | 37 ++++++++++++++++ src/testdir/test_vim9_builtin.vim | 11 +++++ src/version.c | 2 + 13 files changed, 153 insertions(+), 59 deletions(-) diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index d362273f4e..47d320a73d 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1,4 +1,4 @@ -*builtin.txt* For Vim version 9.2. Last change: 2026 Jun 17 +*builtin.txt* For Vim version 9.2. Last change: 2026 Jun 24 VIM REFERENCE MANUAL by Bram Moolenaar @@ -9865,15 +9865,25 @@ server2client({clientid}, {string}) *server2client()* Return type: |Number| -serverlist() *serverlist()* +serverlist([{dict}]) *serverlist()* Return a list of available server names, one per line. When there are no servers or the information is not available - an empty string is returned. See also |clientserver|. + an empty string is returned. {only available when compiled with the |+clientserver| feature} + + If {dict} is given, then it is a |Dictionary| supporting the + following options: + key type meaning ~ + list |Boolean| Return a list of strings, + where each string is a server + name. + + See also |clientserver|. + Example: > :echo serverlist() < - Return type: |String| + Return type: |String| or list setbufline({buf}, {lnum}, {text}) *setbufline()* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index a515589de5..ecde258764 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -52688,6 +52688,7 @@ Changed ~ - During |complete()|-triggered completion, CTRL-N and CTRL-P are now subject to insert-mode mappings. - It is possible to clear the alternate file register |quote#|. +- |serverlist()| can return a list of all available server names. *added-9.3* diff --git a/src/clientserver.c b/src/clientserver.c index 60849fe1e1..13f3fafcb2 100644 --- a/src/clientserver.c +++ b/src/clientserver.c @@ -598,24 +598,33 @@ cmdsrv_main( } else if (STRICMP(argv[i], "--serverlist") == 0) { + list_T *list = NULL; + garray_T ga; + # ifdef MSWIN if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) // Win32 always works? - res = serverGetVimNames(); + list = serverGetVimNames(); # endif # ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) -# ifdef MSWIN - res = vim_strsave((char_u *)""); -# else - res = socketserver_list(); -# endif + list = socketserver_list(); # endif # ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11 && xterm_dpy != NULL) - res = serverGetVimNames(xterm_dpy); + list = serverGetVimNames(xterm_dpy); # endif + + ga_init2(&ga, 1, 80); + if (list != NULL) + { + list_join(&ga, list, (char_u *)"\n", TRUE, FALSE, 0); + ga_append(&ga, NUL); + list_free(list); + } + res = ga.ga_data; + if (did_emsg) mch_errmsg("\n"); } @@ -1250,32 +1259,59 @@ f_server2client(typval_T *argvars UNUSED, typval_T *rettv) void f_serverlist(typval_T *argvars UNUSED, typval_T *rettv) { - char_u *r = NULL; + list_T *list = NULL; + bool use_list = false; + + if (check_for_opt_dict_arg(argvars, 0) == FAIL) + return; + + if (argvars[0].v_type != VAR_UNKNOWN) + { + dict_T *d = argvars[0].vval.v_dict; + + use_list = dict_get_bool(d, "list", false); + } # ifdef FEAT_CLIENTSERVER # ifdef MSWIN if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) - r = serverGetVimNames(); + list = serverGetVimNames(); # endif # ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) -# ifdef MSWIN - r = vim_strsave((char_u *)""); -# else - r = socketserver_list(); -# endif + list = socketserver_list(); # endif # ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11) { - make_connection(); - if (X_DISPLAY != NULL) - r = serverGetVimNames(X_DISPLAY); + make_connection(); + if (X_DISPLAY != NULL) + list = serverGetVimNames(X_DISPLAY); } # endif # endif - rettv->v_type = VAR_STRING; - rettv->vval.v_string = r; + if (use_list && list != NULL) + { + list->lv_refcount++; + rettv->v_type = VAR_LIST; + rettv->vval.v_list = list; + } + else + { + garray_T ga; + + ga_init2(&ga, 1, 80); + + if (list != NULL) + { + list_join(&ga, list, (char_u *)"\n", TRUE, FALSE, 0); + ga_append(&ga, NUL); + list_free(list); + } + + rettv->v_type = VAR_STRING; + rettv->vval.v_string = (char_u *)ga.ga_data; + } } #endif diff --git a/src/evalfunc.c b/src/evalfunc.c index 45002c5bf8..3aeda7e11b 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -2815,8 +2815,8 @@ static const funcentry_T global_functions[] = ret_list_number, f_searchpos}, {"server2client", 2, 2, FEARG_1, arg2_string, ret_number_bool, f_server2client}, - {"serverlist", 0, 0, 0, NULL, - ret_string, f_serverlist}, + {"serverlist", 0, 1, 0, arg1_dict_any, + ret_any, f_serverlist}, {"setbufline", 3, 3, FEARG_3, arg3_setbufline, ret_number_bool, f_setbufline}, {"setbufvar", 3, 3, FEARG_3, arg3_buffer_string_any, diff --git a/src/if_xcmdsrv.c b/src/if_xcmdsrv.c index 43e1e34070..22423a2edf 100644 --- a/src/if_xcmdsrv.c +++ b/src/if_xcmdsrv.c @@ -629,9 +629,9 @@ ServerWait( * Fetch a list of all the Vim instance names currently registered for the * display. * - * Returns a newline separated list in allocated memory or NULL. + * Returns a list of strings or NULL on failure. */ - char_u * + list_T * serverGetVimNames(Display *dpy) { char_u *regProp; @@ -639,7 +639,7 @@ serverGetVimNames(Display *dpy) char_u *p; long_u numItems; int_u w; - garray_T ga; + list_T *list; if (registryProperty == None) { @@ -647,6 +647,10 @@ serverGetVimNames(Display *dpy) return NULL; } + list = list_alloc(); + if (list == NULL) + return NULL; + /* * Read the registry property. */ @@ -656,7 +660,6 @@ serverGetVimNames(Display *dpy) /* * Scan all of the names out of the property. */ - ga_init2(&ga, 1, 100); for (p = regProp; (long_u)(p - regProp) < numItems; p++) { entry = p; @@ -667,18 +670,14 @@ serverGetVimNames(Display *dpy) w = None; sscanf((char *)entry, "%x", &w); if (WindowValid(dpy, (Window)w)) - { - ga_concat(&ga, p + 1); - GA_CONCAT_LITERAL(&ga, "\n"); - } + list_append_string(list, p + 1, -1); while (*p != 0) p++; } } if (regProp != empty_prop) XFree(regProp); - ga_append(&ga, NUL); - return ga.ga_data; + return list; } ///////////////////////////////////////////////////////////// diff --git a/src/os_mswin.c b/src/os_mswin.c index edaa03d4f7..6831171cf7 100644 --- a/src/os_mswin.c +++ b/src/os_mswin.c @@ -2256,16 +2256,14 @@ enumWindowsGetServer(HWND hwnd, LPARAM lparam) static BOOL CALLBACK enumWindowsGetNames(HWND hwnd, LPARAM lparam) { - garray_T *ga = (garray_T *)lparam; + list_T *list = (list_T *)lparam; char server[MAX_PATH]; // Get the title of the window if (getVimServerName(hwnd, server, sizeof(server)) == 0) return TRUE; - // Add the name to the list - ga_concat(ga, (char_u *)server); - GA_CONCAT_LITERAL(ga, "\n"); + list_append_string(list, (char_u *)server, -1); return TRUE; } @@ -2371,17 +2369,17 @@ serverSetName(char_u *name) } } - char_u * + list_T * serverGetVimNames(void) { - garray_T ga; + list_T *list = list_alloc(); - ga_init2(&ga, 1, 100); + if (list == NULL) + return NULL; - enum_windows(enumWindowsGetNames, (LPARAM)(&ga)); - ga_append(&ga, NUL); + enum_windows(enumWindowsGetNames, (LPARAM)list); - return ga.ga_data; + return list; } int diff --git a/src/proto/if_xcmdsrv.pro b/src/proto/if_xcmdsrv.pro index 74245b75a2..d8ce00259b 100644 --- a/src/proto/if_xcmdsrv.pro +++ b/src/proto/if_xcmdsrv.pro @@ -2,7 +2,7 @@ int serverRegisterName(Display *dpy, char_u *name); void serverChangeRegisteredWindow(Display *dpy, Window newwin); int serverSendToVim(Display *dpy, char_u *name, char_u *cmd, char_u **result, Window *server, Bool asExpr, int timeout, Bool localLoop, int silent); -char_u *serverGetVimNames(Display *dpy); +list_T *serverGetVimNames(Display *dpy); Window serverStrToWin(char_u *str); int serverSendReply(char_u *name, char_u *str); int serverReadReply(Display *dpy, Window win, char_u **str, int localLoop, int timeout); diff --git a/src/proto/os_mswin.pro b/src/proto/os_mswin.pro index bcb789f518..32b4aff009 100644 --- a/src/proto/os_mswin.pro +++ b/src/proto/os_mswin.pro @@ -48,7 +48,7 @@ char_u *mch_resolve_path(char_u *fname, int reparse_point); void win32_set_foreground(void); void serverInitMessaging(void); void serverSetName(char_u *name); -char_u *serverGetVimNames(void); +list_T *serverGetVimNames(void); int serverSendReply(char_u *name, char_u *reply); int serverSendToVim(char_u *name, char_u *cmd, char_u **result, void *ptarget, int asExpr, int timeout, int silent); void serverForeground(char_u *name); diff --git a/src/proto/socketserver.pro b/src/proto/socketserver.pro index 69d292c3ab..c1cb3d9a10 100644 --- a/src/proto/socketserver.pro +++ b/src/proto/socketserver.pro @@ -1,7 +1,7 @@ /* socketserver.c */ int socketserver_start(char_u *name, bool quiet); void socketserver_stop(void); -char_u *socketserver_list(void); +list_T *socketserver_list(void); int set_ref_in_socketserver_channel(int copyID); void socketserver_parse_messages(void); int socketserver_send(char_u *name, char_u *str, char_u **result, bool is_expr, int timeout, bool silent, channel_T **ch); diff --git a/src/socketserver.c b/src/socketserver.c index 48c00c3580..d109a21750 100644 --- a/src/socketserver.c +++ b/src/socketserver.c @@ -225,16 +225,21 @@ socketserver_cleanup(void) /* * List available sockets that can be connected to, only in common directories * that Vim knows about. Vim instances with custom socket paths will not be - * detected. Returns a newline separated string on success and NULL on failure. + * detected. Returns a list of strings (with reference count not set) on success + * and NULL on failure. */ - char_u * + list_T * socketserver_list(void) { + list_T *list = list_alloc(); + + if (list == NULL) + return NULL; + # ifdef MSWIN // Only support addresses on Windows - return vim_strsave((char_u *)""); + return list; # else - garray_T str; string_T buf; string_T path; DIR *dirp; @@ -255,8 +260,6 @@ socketserver_list(void) buf.length = 0; path.length = 0; - ga_init2(&str, 1, 100); - for (size_t i = 0 ; i < ARRAY_LENGTH(known_dirs); i++) { const char_u *dir = known_dirs[i]; @@ -285,9 +288,8 @@ socketserver_list(void) buf.length = vim_snprintf_safelen((char *)buf.string, MAXPATHL, "%s/%s", path.string, dp->d_name); - ga_concat_len(&str, (char_u *)dp->d_name, + list_append_string(list, (char_u *)dp->d_name, buf.length - (path.length + 1)); - ga_append(&str, '\n'); } closedir(dirp); @@ -298,9 +300,7 @@ socketserver_list(void) vim_free(path.string); vim_free(buf.string); - ga_append(&str, NUL); - - return str.ga_data; + return list; # endif } diff --git a/src/testdir/test_clientserver.vim b/src/testdir/test_clientserver.vim index 754aa77305..18877a9d8b 100644 --- a/src/testdir/test_clientserver.vim +++ b/src/testdir/test_clientserver.vim @@ -540,6 +540,43 @@ func Test_clientserver_env_method() endtry endfunc +" Test if serverlist() can return a list of strings +func Test_clientserver_serverlist_list() + CheckNotGui + + let g:test_is_flaky = 1 + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + " Don't use channel:2000, because previous tests use that and it may take a + " while for the channel to fully close. + let actual = cmd .. ' --servername XVIMTEST' + + let job = job_start(actual, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_match('XVIMTEST', serverlist())}) + + call assert_equal('list', typename(serverlist(#{list: v:true}))) + call assert_true(serverlist(#{list: v:true})->index('XVIMTEST') != -1) + + if has('win32') || has('gui_running') + call job_stop(job, 'kill') + else + call system(actual .. " --remote-expr 'execute(\"qa!\")'") + endif + try + call WaitForAssert({-> assert_equal("dead", job_status(job))}) + finally + if job_status(job) != 'dead' + call assert_report('Server did not exit') + call job_stop(job, 'kill') + endif + endtry +endfunc + " Uncomment this line to get a debugging log " call ch_logfile('channellog', 'w') diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim index 3f95ee8b25..9b5abba5ce 100644 --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -3600,6 +3600,17 @@ def Test_remote_startserver() v9.CheckSourceDefAndScriptFailure(['remote_startserver({})'], ['E1013: Argument 1: type mismatch, expected string but got dict', 'E1174: String required for argument 1']) enddef +def Test_remote_serverlist() + CheckFeature clientserver + + v9.CheckSourceDefAndScriptFailure(['serverlist("")'], ['E1013: Argument 1: type mismatch, expected dict but got string', 'E1206: Dictionary required for argument 1']) + v9.CheckSourceScriptFailure(['vim9script', 'serverlist({list: ""})'], 'E1135: Using a String as a Bool: ""') + var l: any = serverlist() + assert_equal(v:t_string, type(l)) + l = serverlist({'list': true}) + assert_equal(v:t_list, type(l)) +enddef + def Test_remove_literal_list() var l: list = [1, 2, 3, 4] assert_equal([1, 2], remove(l, 0, 1)) diff --git a/src/version.c b/src/version.c index 77fdc87d88..05a8e6cd7a 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 721, /**/ 720, /**/ From 9ed2c957fee9508c158bbc92b1404e93a2854197 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 18:57:35 +0000 Subject: [PATCH 30/33] patch 9.2.0722: GTK4: find/replace dialog can be improved Problem: GTK4: find/replace dialog can be improved Solution: Store the action buttons in the dialog struct, update their sensitivity when the search field changes, close the dialog on Escape and set the dialog's default widget (Foxe Chen) closes: #20613 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui_gtk4.c | 102 +++++++++++++++++++++++++++++++++++++++++++++---- src/version.c | 2 + 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index a193233a90..06efcfec15 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -5297,6 +5297,9 @@ typedef struct GtkWidget *mcase; // Match case check GtkWidget *up; // Direction up radio GtkWidget *down; // Direction down radio + GtkWidget *find; // 'Find Next' action button + GtkWidget *replace; // 'Replace With' action button + GtkWidget *all; // 'Replace All' action button } SharedFindReplace; static SharedFindReplace find_widgets = {0}; @@ -5347,6 +5350,61 @@ dialog_destroyed_cb(GtkWidget *widget UNUSED, gpointer data) *(GtkWidget **)data = NULL; } + static void +entry_changed_cb(GtkWidget *entry, GtkWidget *dialog) +{ + const gchar *entry_text; + gboolean nonempty; + + entry_text = gtk_editable_get_text(GTK_EDITABLE(entry)); + + if (!entry_text) + return; // Shouldn't happen + + nonempty = (entry_text[0] != '\0'); + + if (dialog == find_widgets.dialog) + gtk_widget_set_sensitive(find_widgets.find, nonempty); + + if (dialog == repl_widgets.dialog) + { + gtk_widget_set_sensitive(repl_widgets.find, nonempty); + gtk_widget_set_sensitive(repl_widgets.replace, nonempty); + gtk_widget_set_sensitive(repl_widgets.all, nonempty); + } +} + + static gboolean +find_key_pressed_cb( + GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + SharedFindReplace *frdp) +{ + // If the user is holding one of the key modifiers we will just bail out, + // thus preserving the possibility of normal focus traversal. + if (state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) + return FALSE; + + // the Escape key synthesizes a cancellation action + if (keyval == GDK_KEY_Escape) + { + // Destroy rather than hide the dialog: reusing a hidden toplevel loses + // window decorations on Wayland + gtk_window_destroy(GTK_WINDOW(frdp->dialog)); + return TRUE; + } + + return FALSE; +} + + static void +entry_activate_cb(GtkWidget *widget UNUSED, void *udata) +{ + gtk_widget_grab_focus(GTK_WIDGET(udata)); +} + static void find_replace_dialog_create(char_u *arg, int do_replace) { @@ -5356,6 +5414,7 @@ find_replace_dialog_create(char_u *arg, int do_replace) int mcase = !p_ic; GtkWidget *vertbox, *grid, *hbox, *tmp, *btn; gboolean sensitive; + GtkEventController *key_controller; frdp = do_replace ? &repl_widgets : &find_widgets; entry_text = get_find_dialog_text(arg, &wword, &mcase); @@ -5368,7 +5427,7 @@ find_replace_dialog_create(char_u *arg, int do_replace) } // If the dialog already exists, just raise it. - if (frdp->dialog) + if (frdp->dialog != NULL) { if (entry_text != NULL) { @@ -5380,7 +5439,15 @@ find_replace_dialog_create(char_u *arg, int do_replace) (gboolean)mcase); } gtk_window_present(GTK_WINDOW(frdp->dialog)); + + // For :promptfind dialog, always give keyboard focus to 'what' entry. + // For :promptrepl dialog, give it to 'with' entry if 'what' has a + // non-empty entry; otherwise, to 'what' entry. gtk_widget_grab_focus(frdp->what); + if (do_replace && g_utf8_strlen( + gtk_editable_get_text(GTK_EDITABLE(frdp->what)), -1) > 0) + gtk_widget_grab_focus(frdp->with); + vim_free(entry_text); return; } @@ -5392,7 +5459,7 @@ find_replace_dialog_create(char_u *arg, int do_replace) gtk_window_set_destroy_with_parent(GTK_WINDOW(frdp->dialog), TRUE); gtk_window_set_title(GTK_WINDOW(frdp->dialog), do_replace ? _("VIM - Search and Replace...") - : _("VIM - Search...")); + : _("VIM - Search...")); gtk_window_set_resizable(GTK_WINDOW(frdp->dialog), FALSE); g_signal_connect(frdp->dialog, "destroy", @@ -5433,7 +5500,17 @@ find_replace_dialog_create(char_u *arg, int do_replace) frdp->with = gtk_entry_new(); gtk_widget_set_hexpand(frdp->with, TRUE); gtk_grid_attach(GTK_GRID(grid), frdp->with, 1, 1, 1, 1); + gtk_entry_set_activates_default(GTK_ENTRY(frdp->with), TRUE); + + // Make the entry activation only change the input focus onto the + // with item. + gtk_entry_set_activates_default(GTK_ENTRY(frdp->what), FALSE); + g_signal_connect(G_OBJECT(frdp->what), "activate", + G_CALLBACK(entry_activate_cb), frdp->with); } + else + // Make the entry activation do the search. + gtk_entry_set_activates_default(GTK_ENTRY(frdp->what), TRUE); // Checkboxes hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); @@ -5472,6 +5549,18 @@ find_replace_dialog_create(char_u *arg, int do_replace) btn = gtk_button_new_with_label(_("Find Next")); gtk_widget_set_sensitive(btn, sensitive); + frdp->find = btn; + + key_controller = gtk_event_controller_key_new(); + g_signal_connect(key_controller, "key-pressed", + G_CALLBACK(find_key_pressed_cb), frdp); + gtk_widget_add_controller(GTK_WIDGET(frdp->dialog), key_controller); + + g_signal_connect(G_OBJECT(frdp->what), "changed", + G_CALLBACK(entry_changed_cb), frdp->dialog); + + // Make it so that when entry is activated, this button will be activated. + gtk_window_set_default_widget(GTK_WINDOW(frdp->dialog), btn); g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT)); gtk_box_append(GTK_BOX(hbox), btn); @@ -5479,13 +5568,17 @@ find_replace_dialog_create(char_u *arg, int do_replace) if (do_replace) { btn = gtk_button_new_with_label(_("Replace")); + gtk_widget_set_sensitive(btn, sensitive); g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), GINT_TO_POINTER(FRD_REPLACE)); + frdp->replace = btn; gtk_box_append(GTK_BOX(hbox), btn); btn = gtk_button_new_with_label(_("Replace All")); + gtk_widget_set_sensitive(btn, sensitive); g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), GINT_TO_POINTER(FRD_REPLACEALL)); + frdp->all = btn; gtk_box_append(GTK_BOX(hbox), btn); } @@ -5494,11 +5587,6 @@ find_replace_dialog_create(char_u *arg, int do_replace) G_CALLBACK(gtk_window_destroy), frdp->dialog); gtk_box_append(GTK_BOX(hbox), btn); - // Connect Enter key in entry to Find Next - g_signal_connect_swapped(frdp->what, "activate", - G_CALLBACK(find_replace_cb), - GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT)); - gtk_window_present(GTK_WINDOW(frdp->dialog)); gtk_widget_grab_focus(frdp->what); if (do_replace && entry_text != NULL && entry_text[0] != NUL) diff --git a/src/version.c b/src/version.c index 05a8e6cd7a..d9613a8fb9 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 722, /**/ 721, /**/ From 5fdb44c438747d419a403110f3c2dd792f539497 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 19:35:59 +0000 Subject: [PATCH 31/33] patch 9.2.0723: term_start() does not support "noclose" Problem: term_start() does not support "noclose" Solution: Add support for "noclose" for the "term_finish" option of term_start() (Foxe Chen) closes: #20620 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- runtime/doc/tags | 1 + runtime/doc/terminal.txt | 8 +++++--- runtime/doc/version9.txt | 1 + src/job.c | 3 ++- src/testdir/test_terminal.vim | 16 ++++++++++++++++ src/version.c | 2 ++ 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/runtime/doc/tags b/runtime/doc/tags index 3abd3e035e..1e06d460b4 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -11012,6 +11012,7 @@ tempfile change.txt /*tempfile* template autocmd.txt /*template* tempname() builtin.txt /*tempname()* term++close terminal.txt /*term++close* +term++noclose terminal.txt /*term++noclose* term++open terminal.txt /*term++open* term++shell terminal.txt /*term++shell* term-dependent-settings term.txt /*term-dependent-settings* diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt index 01fd88ca66..99d9f3a4af 100644 --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -1,4 +1,4 @@ -*terminal.txt* For Vim version 9.2. Last change: 2026 Apr 06 +*terminal.txt* For Vim version 9.2. Last change: 2026 Jun 24 VIM REFERENCE MANUAL by Bram Moolenaar @@ -232,7 +232,7 @@ Command syntax ~ keys in the terminal window. For MS-Windows see the ++eof argument below. - *term++close* *term++open* + *term++close* *term++noclose* *term++open* Supported [options] are: ++close The terminal window will close automatically when the job terminates. @@ -1001,9 +1001,11 @@ term_start({cmd} [, {options}]) *term_start()* terminal window, see |term_setkill()| "term_finish" What to do when the job is finished: "close": close any windows + "noclose": window will not be opened "open": open window if needed Note that "open" can be interruptive. - See |term++close| and |term++open|. + See |term++close|, |term++noclose| and + |term++open|. "term_opencmd" command to use for opening the window when "open" is used for "term_finish"; must have "%d" where the buffer number diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index ecde258764..310e9e695c 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -52657,6 +52657,7 @@ Other ~ not drop leading spaces |stl-%0{|. - Generated Session and View files are written in Vim9 script, see |:mksession|, |:mkview| and |:mkvimrc| +- The "term_finish" option of term_start() supports a "noclose" value. Platform specific ~ ----------------- diff --git a/src/job.c b/src/job.c index 937d55f6be..c7c546aa38 100644 --- a/src/job.c +++ b/src/job.c @@ -386,7 +386,8 @@ get_job_options(typval_T *tv, jobopt_T *opt, int supported, int supported2) if (!(supported2 & JO2_TERM_FINISH)) break; val = tv_get_string(item); - if (STRCMP(val, "open") != 0 && STRCMP(val, "close") != 0) + if (STRCMP(val, "open") != 0 && STRCMP(val, "close") != 0 + && STRCMP(val, "noclose") != 0) { semsg(_(e_invalid_value_for_argument_str_str), "term_finish", val); diff --git a/src/testdir/test_terminal.vim b/src/testdir/test_terminal.vim index 98e82fcb4c..096904e111 100644 --- a/src/testdir/test_terminal.vim +++ b/src/testdir/test_terminal.vim @@ -774,6 +774,22 @@ func Test_terminal_finish_open_close() call assert_equal('opened the buffer in a window', g:result) unlet g:result bwipe + + " Test "noclose" for term_start() + let cmd = Get_cat_123_cmd() + + let buf = term_start(cmd, { + \ 'term_finish': 'noclose', + \ 'hidden': v:true + \ }) + + call WaitForAssert({-> assert_equal('finished', term_getstatus(buf))}) + + let info = getbufinfo(buf)[0] + call assert_equal(1, info.hidden) + call assert_equal(1, info.listed) + call assert_equal(1, info.loaded) + call WaitForAssert({-> assert_equal(['123'], getbufline(buf, 1, 1))}) endfunc func Test_terminal_cwd() diff --git a/src/version.c b/src/version.c index d9613a8fb9..43deacfd7a 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 723, /**/ 722, /**/ From 1ccf3388dc950aab43330035d5d0daeb80536b8f Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Wed, 24 Jun 2026 19:40:51 +0000 Subject: [PATCH 32/33] patch 9.2.0724: Use-after-free when freeing exit_cb job on exit Problem: Use-after-free when freeing exit_cb job on exit Solution: Return when def_functions has been freed already (Foxe Chen) closes: #20621 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/testdir/test_vim9_func.vim | 24 ++++++++++++++++++++++++ src/version.c | 2 ++ src/vim9compile.c | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/testdir/test_vim9_func.vim b/src/testdir/test_vim9_func.vim index a2faec225d..3774609c80 100644 --- a/src/testdir/test_vim9_func.vim +++ b/src/testdir/test_vim9_func.vim @@ -2,6 +2,7 @@ import './util/vim9.vim' as v9 source util/screendump.vim +source util/shared.vim func Test_def_basic() def SomeFunc(): string @@ -5045,4 +5046,27 @@ def Test_void_method_chain() v9.CheckScriptFailure(lines, 'E1186: Expression does not result in a value: bufload(') enddef +def Test_term_wait_in_job_exit_cb() + CheckUnix + CheckFeature terminal + + var cmd = g:GetVimCommand() + + var lines =<< eval trim END + var buf: number = term_start(["{cmd}", "+q"], {{}}) + + var job: job = term_getjob(buf) + + job_setoptions(job, {{ + exit_cb: (_, _) => {{ + term_wait(buf) + }} + }}) + END + + # This shouldn't cause an ASAN error immediately, but will result in a use + # after free when Vim exits. + v9.CheckDefSuccess(lines) +enddef + " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker diff --git a/src/version.c b/src/version.c index 43deacfd7a..010537fa1e 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 724, /**/ 723, /**/ diff --git a/src/vim9compile.c b/src/vim9compile.c index 181df2a6fb..3b1b9e9ef8 100644 --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -5238,7 +5238,7 @@ delete_def_function_contents(dfunc_T *dfunc, int mark_deleted) void unlink_def_function(ufunc_T *ufunc) { - if (ufunc->uf_dfunc_idx <= 0) + if (ufunc->uf_dfunc_idx <= 0 || def_functions.ga_data == NULL) return; dfunc_T *dfunc = ((dfunc_T *)def_functions.ga_data) From d22ff1c955ff87e8273210eae125aab0e85b6c30 Mon Sep 17 00:00:00 2001 From: Hirohito Higashi Date: Mon, 22 Jun 2026 13:00:36 +0900 Subject: [PATCH 33/33] patch 9.2.0725: [security]: Stack out-of-bounds write in spell_soundfold_sal() Problem: [security]: A crafted spell file with non-collapsing SAL rules can make soundfold() write one byte past the end of the MAXWLEN result buffer. This is the same class of out-of-bounds write as GHSA-q8mh-6qm3-25g4 (fixed in 9.2.0698 for the SOFO branch), found while auditing the surrounding code. Solution: Bound the single-byte SAL result writes and the terminating NUL to MAXWLEN - 1, matching the SOFO branch. The single-byte branch of spell_soundfold_sal() guarded its writes with "reslen < MAXWLEN", allowing reslen to reach MAXWLEN (254). The trailing "res[reslen] = NUL" then wrote at index 254 of the 254-byte stack buffer res[MAXWLEN], an off-by-one out-of-bounds write. Input is case-folded to about 253 characters, so a 253-character argument together with a SAL map that does not collapse (collapse_result false) reaches the boundary. Related to previous issue [GHSA-q8mh-6qm3-25g4](https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4) (9.2.0698) Github Security Advisory: https://github.com/vim/vim/security/advisories/GHSA-m3hf-xcm3-xhm2 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- src/spell.c | 6 +++--- src/testdir/test_spellfile.vim | 24 ++++++++++++++++++++++++ src/version.c | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/spell.c b/src/spell.c index 96700782ef..1276604251 100644 --- a/src/spell.c +++ b/src/spell.c @@ -3513,7 +3513,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) // no '<' rule used i += k - 1; z = 0; - while (*s != NUL && s[1] != NUL && reslen < MAXWLEN) + while (*s != NUL && s[1] != NUL && reslen < MAXWLEN - 1) { if (reslen == 0 || res[reslen - 1] != *s) res[reslen++] = *s; @@ -3523,7 +3523,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) c = *s; if (strstr((char *)pf, "^^") != NULL) { - if (c != NUL) + if (c != NUL && reslen < MAXWLEN - 1) res[reslen++] = c; STRMOVE(word, word + i + 1); i = 0; @@ -3542,7 +3542,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) if (z0 == 0) { - if (k && !p0 && reslen < MAXWLEN && c != NUL + if (k && !p0 && reslen < MAXWLEN - 1 && c != NUL && (!slang->sl_collapse || reslen == 0 || res[reslen - 1] != c)) // condense only double letters diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim index 9ec728a768..951538d514 100644 --- a/src/testdir/test_spellfile.vim +++ b/src/testdir/test_spellfile.vim @@ -405,6 +405,30 @@ func Test_spellfile_format_error() let &rtp = save_rtp endfunc +" An over-length soundfold() argument must not overflow the MAXWLEN result +" buffer in the single-byte branch of spell_soundfold_sal(). +func Test_spellfile_soundfold_sal_overflow() + let save_enc = &encoding + set encoding=latin1 + " A SAL map that appends without collapsing, so the result is not shorter + " than the input. + call writefile(['SET ISO8859-1', 'SAL collapse_result false', + \ 'SAL a aaaa', 'SAL b bbbb'], 'Xsal.aff') + call writefile(['2', 'hello', 'world'], 'Xsal.dic') + mkspell! Xsal Xsal + set spl=Xsal.latin1.spl spell + + " 253 input characters hit the buffer boundary; the result must not exceed + " MAXWLEN - 1. + call assert_true(strlen(soundfold(repeat('a', 253))) <= 253) + + set nospell spl& spelllang& + call delete('Xsal.aff') + call delete('Xsal.dic') + call delete('Xsal.latin1.spl') + let &encoding = save_enc +endfunc + " Test for format errors in suggest file func Test_sugfile_format_error() let save_rtp = &rtp diff --git a/src/version.c b/src/version.c index 010537fa1e..ed23c9f597 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 725, /**/ 724, /**/