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..c073e1ad6d 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 \ @@ -517,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/README_vim.md b/README_vim.md index 387e5b2bca..569aed94d4 100644 --- a/README_vim.md +++ b/README_vim.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/builtin.txt b/runtime/doc/builtin.txt index af88740762..dbced97dc7 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 @@ -9869,15 +9869,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/change.txt b/runtime/doc/change.txt index 32cbc06248..b3381a5908 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/gui_x11.txt b/runtime/doc/gui_x11.txt index c35cf50ecc..3ed3fe13a1 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 @@ -791,5 +791,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/options.txt b/runtime/doc/options.txt index 97d8a409ac..dda46cad7f 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 @@ -6594,7 +6594,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/runtime/doc/tags b/runtime/doc/tags index edeb9941ff..750cb76c6b 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -4057,6 +4057,7 @@ $quote eval.txt /*$quote* ? pattern.txt /*?* ?? eval.txt /*??* @ repeat.txt /*@* +@# change.txt /*@#* @/ change.txt /*@\/* @: repeat.txt /*@:* @= change.txt /*@=* @@ -8366,6 +8367,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* @@ -11182,6 +11184,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/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 diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index a515589de5..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 ~ ----------------- @@ -52688,6 +52689,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/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 (?) 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/runtime/ftplugin/fennel.vim b/runtime/ftplugin/fennel.vim index 2a9623faff..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 @@ -10,7 +11,7 @@ endif let b:did_ftplugin = 1 setlocal commentstring=;\ %s -setlocal comments=:;;,:; +setlocal comments=:;;;;,:;;;,:;;,:; setlocal formatoptions-=t setlocal suffixesadd=.fnl setlocal lisp 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 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 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 diff --git a/src/Makefile b/src/Makefile index c78d55cc18..3657f5c9e6 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) @@ -1337,7 +1339,7 @@ MACVIMGUI_BUNDLE = macvim MACVIMGUI_TESTARG = VIMPROG=../$(RELEASEDIR)/MacVim.app/Contents/MacOS/$(VIMTARGET) # 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 ALL_GUI_SRC += MacVim/gui_macvim.m MacVim/MMBackend.m MacVim/MacVim.m @@ -3452,6 +3454,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 diff --git a/src/auto/configure b/src/auto/configure index 07686b00f6..6992214e6c 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -15399,7 +15399,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 #( @@ -15407,7 +15407,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 @@ -15438,7 +15439,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 #( @@ -15446,7 +15447,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/autocmd.c b/src/autocmd.c index 8e196f6bce..9490aaee85 100644 --- a/src/autocmd.c +++ b/src/autocmd.c @@ -1026,7 +1026,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); } @@ -1037,7 +1040,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); @@ -1052,12 +1055,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); @@ -1076,7 +1079,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; } } @@ -1112,6 +1115,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/clientserver.c b/src/clientserver.c index 360915a18d..0a93b4302a 100644 --- a/src/clientserver.c +++ b/src/clientserver.c @@ -611,27 +611,36 @@ 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 MAC_CLIENTSERVER res = 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"); } @@ -1203,6 +1212,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); @@ -1282,35 +1293,62 @@ 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 MAC_CLIENTSERVER r = 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/cmdexpand.c b/src/cmdexpand.c index 4ff6a2c486..8df3ca4f38 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/configure.ac b/src/configure.ac index 0c1f05ecb9..c872f71a00 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -4283,7 +4283,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); @@ -4297,7 +4297,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/drawline.c b/src/drawline.c index 6e73ba591a..ec70a77bdc 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/errors.h b/src/errors.h index 45471480ed..f3efc106f8 100644 --- a/src/errors.h +++ b/src/errors.h @@ -2292,8 +2292,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/evalfunc.c b/src/evalfunc.c index 1309e1c681..391a82f145 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -2812,8 +2812,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/ex_cmds2.c b/src/ex_cmds2.c index 414de8cbde..a29a2edbe7 100644 --- a/src/ex_cmds2.c +++ b/src/ex_cmds2.c @@ -549,7 +549,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/gui.c b/src/gui.c index b363396111..df00dbef3b 100644 --- a/src/gui.c +++ b/src/gui.c @@ -1647,6 +1647,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?). @@ -1669,6 +1677,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) && !defined(FEAT_GUI_MACVIM) xim_set_status_area(); #endif @@ -1854,6 +1865,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 75651f9681..95b93101cf 100644 --- a/src/gui.h +++ b/src/gui.h @@ -291,6 +291,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 # if !(defined(FEAT_GUI_GTK) || defined(FEAT_GUI_MACVIM)) 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 4874b2f626..06efcfec15 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,18 +290,15 @@ 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); 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 @@ -484,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 @@ -547,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 @@ -598,13 +569,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); @@ -817,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)); } @@ -887,10 +869,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 +950,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 +970,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 @@ -1936,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) { @@ -2046,6 +2049,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 +2090,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 +2111,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 +2125,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,38 +2169,79 @@ 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. - 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; 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 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; // 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 @@ -2208,52 +2303,10 @@ 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 - -#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 @@ -2274,10 +2327,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) @@ -2295,42 +2348,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; @@ -2339,9 +2358,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. @@ -2376,13 +2392,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 @@ -2770,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 @@ -2779,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 @@ -3005,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]; - - 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); + VimMenuItem *item = VIM_MENU_ITEM(vim_menu_item_new(name, + tabline_menu_event_cb, GINT_TO_POINTER(resp))); - 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 @@ -3778,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); } /* @@ -3824,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)); } @@ -3929,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); @@ -4274,17 +4197,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) @@ -4299,7 +4214,6 @@ static int last_text_area_h = 0; * ============================================================ * Menu functions * ============================================================ - * TODO: Implement using GMenu + GtkPopoverMenuBar */ /* @@ -4387,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); } @@ -4525,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); } } @@ -4584,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]); @@ -4593,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 @@ -4705,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)); } /* @@ -4896,21 +4722,42 @@ 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 + /* * ============================================================ * Browse dialogs @@ -4958,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); @@ -4986,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); @@ -5148,6 +5043,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) { @@ -5161,13 +5063,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; } @@ -5203,6 +5108,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) @@ -5245,13 +5151,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) { @@ -5270,14 +5177,44 @@ 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) { - 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); @@ -5360,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}; @@ -5410,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) { @@ -5419,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); @@ -5431,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) { @@ -5443,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; } @@ -5455,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", @@ -5496,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); @@ -5535,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); @@ -5542,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); } @@ -5557,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/gui_gtk4_da.c b/src/gui_gtk4_da.c index 34dad93a61..8db224ea67 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,12 @@ 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; + // 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 @@ -178,7 +179,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 +574,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 +1478,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 +1488,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 +1585,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/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/highlight.c b/src/highlight.c index 4b21146267..c1a29e5615 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/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/insexpand.c b/src/insexpand.c index c8f22de67d..a5bdb51996 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; @@ -1888,13 +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; + 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/job.c b/src/job.c index 20b55b16cd..cfcf700dda 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/menu.c b/src/menu.c index 0a5482cf26..87cd8ddd48 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1075,10 +1075,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/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/po/vim.pot b/src/po/vim.pot index b185a51444..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-16 19:21+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 "" @@ -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/popupmenu.c b/src/popupmenu.c index b7929607d9..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,20 @@ 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 + // 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; @@ -1660,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/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/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/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/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/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/session.c b/src/session.c index 17537cff15..ce68de1ca5 100644 --- a/src/session.c +++ b/src/session.c @@ -1341,10 +1341,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/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/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/structs.h b/src/structs.h index 527aeb2b90..b3cee0cc0b 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4379,6 +4379,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 @@ -4741,7 +4745,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/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/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_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/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_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/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_autocmd.vim b/src/testdir/test_autocmd.vim index 70adeb8d81..9a96e2e9ad 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/testdir/test_clientserver.vim b/src/testdir/test_clientserver.vim index 240b447698..869a079e9e 100644 --- a/src/testdir/test_clientserver.vim +++ b/src/testdir/test_clientserver.vim @@ -546,6 +546,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_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_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/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 93ca66f5a9..4df2efb061 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 @@ -927,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/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/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/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/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/testdir/test_terminal.vim b/src/testdir/test_terminal.vim index 4152173931..a3fdef9d3e 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() @@ -942,7 +958,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 +1002,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/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/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/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/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 270e1aa084..3208bace67 100644 --- a/src/version.c +++ b/src/version.c @@ -774,6 +774,58 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 725, +/**/ + 724, +/**/ + 723, +/**/ + 722, +/**/ + 721, +/**/ + 720, +/**/ + 719, +/**/ + 718, +/**/ + 717, +/**/ + 716, +/**/ + 715, +/**/ + 714, +/**/ + 713, +/**/ + 712, +/**/ + 711, +/**/ + 710, +/**/ + 709, +/**/ + 708, +/**/ + 707, +/**/ + 706, +/**/ + 705, +/**/ + 704, +/**/ + 703, +/**/ + 702, +/**/ + 701, +/**/ + 700, /**/ 699, /**/ diff --git a/src/vim9compile.c b/src/vim9compile.c index aa9384bbe9..3b1b9e9ef8 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; @@ -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)