patch 9.2.0906: slow transstr() with long strings
Commit:
https://github.com/vim/vim/commit/124c86868c253a5ec1347e7cbe102504d2d66a07
Author: Samuel Schlesinger <
sgschl...@gmail.com>
Date: Mon Aug 3 20:30:09 2026 +0000
patch 9.2.0906: slow transstr() with long strings
Problem: transstr() appends with STRCAT()/STRLEN() from the start of
the result on every iteration, making it quadratic to the
length of the string.
Solution: Keep a tail pointer and append at it. (Samuel Schlesinger).
closes: #20925
Signed-off-by: Samuel Schlesinger <
sgschl...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/charset.c b/src/charset.c
index 1c45688fe..3be32f5fb 100644
--- a/src/charset.c
+++ b/src/charset.c
@@ -383,7 +383,10 @@ transstr(char_u *s)
if (res == NULL)
return NULL;
- *res = NUL;
+ // Keep a tail pointer to append to, appending with STRCAT would make
+ // this loop quadratic.
+ char_u *d = res;
+
p = s;
while (*p != NUL)
{
@@ -391,14 +394,28 @@ transstr(char_u *s)
{
c = (*mb_ptr2char)(p);
if (vim_isprintc(c))
- STRNCAT(res, p, l); // append printable multi-byte char
+ {
+ // append printable multi-byte char
+ mch_memmove(d, p, (size_t)l);
+ d += l;
+ }
else
- transchar_hex(res + STRLEN(res), c);
+ {
+ transchar_hex(d, c);
+ d += STRLEN(d);
+ }
p += l;
}
else
- STRCAT(res, transchar_byte(*p++));
+ {
+ char_u *trs = transchar_byte(*p++);
+ int trs_len = (int)STRLEN(trs);
+
+ mch_memmove(d, trs, (size_t)trs_len);
+ d += trs_len;
+ }
}
+ *d = NUL;
return res;
}
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 645ff531e..2f2a7fc5f 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -211,6 +211,56 @@ func Test_strwidth()
set ambiwidth&
endfunc
+func Test_strtrans()
+ " The default of 'isprint' is platform-dependent: 0x7f and 0x9f are
+ " printable on Win32 and VMS. Set it so the expectations below hold
+ " everywhere.
+ let save_isprint = &isprint
+ set isprint=@,161-255
+
+ " printable ASCII is unchanged
+ call assert_equal('', strtrans(''))
+ call assert_equal('abc', strtrans('abc'))
+
+ " control characters are displayed as ^X
+ call assert_equal('^I', strtrans(" "))
+ call assert_equal('a^Mb^[c', strtrans("a
b c"))
+ call assert_equal('^A^_^?', strtrans("\x01\x1f\x7f"))
+
+ " printable multibyte characters are unchanged, including composing
+ " characters and characters above 0xffff
+ call assert_equal('héllo 你好', strtrans('héllo 你好'))
+ let s = 'e' .. nr2char(0x301) .. 'x'
+ call assert_equal(s, strtrans(s))
+ call assert_equal(nr2char(0x1d11e), strtrans(nr2char(0x1d11e)))
+
+ " unprintable multibyte characters are displayed in <xx> hex form
+ call assert_equal('<9f>', strtrans(nr2char(0x9f)))
+ call assert_equal('<200b>', strtrans(nr2char(0x200b)))
+ call assert_equal('<feff>', strtrans(nr2char(0xfeff)))
+
+ " illegal bytes are displayed in <xx> hex form
+ call assert_equal('A<ff>B', strtrans("A\xffB"))
+
+ " a long string mixing all kinds of characters
+ call assert_equal(repeat('a^Bé<9f>', 100),
+ \ strtrans(repeat("a\x02é" .. nr2char(0x9f), 100)))
+
+ " the non-multi-byte code path
+ set encoding=latin1
+ set isprint=@,161-255
+ call assert_equal('a^Mb^[c', strtrans("a
b c"))
+ call assert_equal('^A^_^?', strtrans("\x01\x1f\x7f"))
+ " an unprintable byte above 0x7f uses the meta notation
+ call assert_equal('| ', strtrans("\xa0"))
+ " a printable high byte is unchanged
+ call assert_equal("\xe9", strtrans("\xe9"))
+ call assert_equal("x^B\xe9| y", strtrans("x\x02\xe9\xa0y"))
+ set encoding=utf-8
+
+ let &isprint = save_isprint
+endfunc
+
func Test_str2nr()
call assert_equal(0, str2nr(''))
call assert_equal(1, str2nr('1'))
diff --git a/src/version.c b/src/version.c
index 7bb915f93..13c9fc7d3 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 906,
/**/
905,
/**/