Skip to content

Commit 8f9de49

Browse files
ychinchrisbra
authored andcommitted
patch 9.1.1623: Buffer menu does not handle unicode names correctly
Problem: Buffer menu does not handle unicode names correctly (after v9.1.1622) Solution: Fix the BMHash() function (Yee Cheng Chin) The Buffers menu uses a BMHash() function to generate a sortable number to be used for the menu index. It used a naive (and incorrect) way of encoding multiple ASCII values into a single integer, but assumes each character to be only in the ASCII 32-96 range. This means if we use non-ASCII file names (e.g. Unicode values like CJK or emojis) we get integer underflow and overflow, causing the menu index to wrap around. Vim's GUI implementations internally use a signed 32-bit integer for the `gui_mch_add_menu_item()` function and so we need to make sure the menu index is in the (0, 2^31-1) range. To do this, if the file name starts with a non-ASCII value, we just use the first character's value and set the high bit so it sorts after the other ASCII ones. Otherwise, we just take the first 5 characters, and use 5 bit for each character to encode a 30-bit number that can be sorted. This means Unicode file names won't be sorted beyond the first character. This is likely going to be fine as there are lots of ways to query buffers. related: #17403 closes: #17928 Signed-off-by: Yee Cheng Chin <[email protected]> Signed-off-by: Christian Brabandt <[email protected]>
1 parent cda0d17 commit 8f9de49

3 files changed

Lines changed: 60 additions & 6 deletions

File tree

runtime/menu.vim

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
" You can also use this as a start for your own set of menus.
33
"
44
" Maintainer: The Vim Project <https://github.com/vim/vim>
5-
" Last Change: 2023 Aug 10
5+
" Last Change: 2025 Aug 10
66
" Former Maintainer: Bram Moolenaar <[email protected]>
77

88
" Note that ":an" (short for ":anoremenu") is often used to make a menu work
@@ -797,8 +797,21 @@ def s:BMShow()
797797
enddef
798798

799799
def s:BMHash(name: string): number
800-
# Make name all upper case, so that chars are between 32 and 96
801-
var nm = substitute(name, ".*", '\U\0', "")
800+
# Create a sortable numeric hash of the name. This number has to be within
801+
# the bounds of a signed 32-bit integer as this is what Vim GUI uses
802+
# internally for the index.
803+
804+
# Make name all upper case, so that alphanumeric chars are between 32 and 96
805+
var nm = toupper(name)
806+
807+
if char2nr(nm[0]) < 32 || char2nr(nm[0]) > 96
808+
# We don't have an ASCII character, so just return the raw character value
809+
# for first character (clamped to 2^31) and set the high bit to make it
810+
# sort after other items. This means only the first character will be
811+
# sorted, unfortunately.
812+
return or(and(char2nr(nm), 0x7fffffff), 0x40000000)
813+
endif
814+
802815
var sp: number
803816
if has("ebcdic")
804817
# HACK: Replace all non alphabetics with 'Z'
@@ -808,12 +821,18 @@ def s:BMHash(name: string): number
808821
else
809822
sp = char2nr(' ')
810823
endif
811-
# convert first six chars into a number for sorting:
812-
return (char2nr(nm[0]) - sp) * 0x800000 + (char2nr(nm[1]) - sp) * 0x20000 + (char2nr(nm[2]) - sp) * 0x1000 + (char2nr(nm[3]) - sp) * 0x80 + (char2nr(nm[4]) - sp) * 0x20 + (char2nr(nm[5]) - sp)
824+
# convert first five chars into a number for sorting by compressing each
825+
# char into 5 bits (0-63), to a total of 30 bits. If any character is not
826+
# ASCII, it will simply be clamped to prevent overflow.
827+
return (max([0, min([63, char2nr(nm[0]) - sp])]) << 24) +
828+
(max([0, min([63, char2nr(nm[1]) - sp])]) << 18) +
829+
(max([0, min([63, char2nr(nm[2]) - sp])]) << 12) +
830+
(max([0, min([63, char2nr(nm[3]) - sp])]) << 6) +
831+
max([0, min([63, char2nr(nm[4]) - sp])])
813832
enddef
814833

815834
def s:BMHash2(name: string): string
816-
var nm = substitute(name, ".", '\L\0', "")
835+
var nm = tolower(name[0])
817836
if nm[0] < 'a' || nm[0] > 'z'
818837
return '&others.'
819838
elseif nm[0] <= 'd'

src/testdir/test_gui.vim

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,4 +1767,37 @@ func Test_CursorHold_not_triggered_at_startup()
17671767
call assert_equal(['g:cursorhold_triggered=0'], found)
17681768
endfunc
17691769

1770+
" Test that Buffers menu generates the correct index for different buffer
1771+
" names for sorting.
1772+
func Test_Buffers_Menu()
1773+
doautocmd LoadBufferMenu VimEnter
1774+
1775+
" Non-ASCII characters only use the first character as idx
1776+
let idx_emoji = or(char2nr('😑'), 0x40000000)
1777+
1778+
" Only first five letters are used for alphanumeric:
1779+
" ('a'-32) << 24 + ('b'-32) << 18 + ('c'-32) << 12 + ('d'-32) << 6 + ('e'-32)
1780+
let idx_abcde = 0x218A3925
1781+
" ('a'-32) << 24 + ('b'-32) << 18 + ('c'-32) << 12 + ('d'-32) << 6 + ('f'-32)
1782+
let idx_abcdf = 0x218A3926
1783+
" ('a'-32) << 24 + 63 (clamped) << 18 + ('c'-32) << 12 + ('d'-32) << 6 + ('e'-32)
1784+
let idx_a_emoji_cde = 0x21FE3925
1785+
1786+
let names = ['😑', '😑1', '😑2', 'abcde', 'abcdefghi', 'abcdf', 'a😑cde']
1787+
let indices = [idx_emoji, idx_emoji, idx_emoji, idx_abcde, idx_abcde, idx_abcdf, idx_a_emoji_cde]
1788+
for i in range(len(names))
1789+
let name = names[i]
1790+
let idx = indices[i]
1791+
exe ':badd ' .. name
1792+
let nr = bufnr('$')
1793+
1794+
let cmd = printf(':amenu Buffers.%s\ (%d)', name, nr)
1795+
let menu = split(execute(cmd), '\n')[1]
1796+
call assert_inrange(0, 0x7FFFFFFF, idx)
1797+
call assert_match('^' .. idx .. ' '.. name, menu)
1798+
endfor
1799+
1800+
%bw!
1801+
endfunc
1802+
17701803
" vim: shiftwidth=2 sts=2 expandtab

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,8 @@ static char *(features[]) =
719719

720720
static int included_patches[] =
721721
{ /* Add new patch number below this line */
722+
/**/
723+
1623,
722724
/**/
723725
1622,
724726
/**/

0 commit comments

Comments
 (0)