To: vim_dev@googlegroups.com Subject: Patch 8.2.4684 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 8.2.4684 Problem: Cannot open a channel on a Unix domain socket. Solution: Add Unix domain socket support. (closes #10062) Files: runtime/doc/channel.txt, src/channel.c, src/testdir/check.vim, src/testdir/shared.vim, src/testdir/test_channel.py, src/testdir/test_channel.vim, src/testdir/test_channel_unix.py, src/testdir/test_cmdline.vim *** ../vim-8.2.4683/runtime/doc/channel.txt 2022-03-30 10:14:41.485657271 +0100 --- runtime/doc/channel.txt 2022-04-04 15:39:23.550236560 +0100 *************** *** 119,128 **** Use |ch_status()| to see if the channel could be opened. ! {address} has the form "hostname:port". E.g., "localhost:8765". ! ! When using an IPv6 address, enclose it within square brackets. E.g., ! "[2001:db8::1]:8765". {options} is a dictionary with optional entries: *channel-open-options* --- 119,131 ---- Use |ch_status()| to see if the channel could be opened. ! *channel-address* ! {address} can be a domain name or an IP address, followed by a port number, or ! a Unix-domain socket path prefixed by "unix:". E.g. > ! www.example.com:80 " domain + port ! 127.0.0.1:1234 " IPv4 + port ! [2001:db8::1]:8765 " IPv6 + port ! unix:/tmp/my-socket " Unix-domain socket path {options} is a dictionary with optional entries: *channel-open-options* *************** *** 579,588 **** --- 582,596 ---- When opened with ch_open(): "hostname" the hostname of the address "port" the port of the address + "path" the path of the Unix-domain socket "sock_status" "open" or "closed" "sock_mode" "NL", "RAW", "JSON" or "JS" "sock_io" "socket" "sock_timeout" timeout in msec + + Note that "pair" is only present for Unix-domain sockets, for + regular ones "hostname" and "port" are present instead. + When opened with job_start(): "out_status" "open", "buffered" or "closed" "out_mode" "NL", "RAW", "JSON" or "JS" *************** *** 641,651 **** Open a channel to {address}. See |channel|. Returns a Channel. Use |ch_status()| to check for failure. ! {address} is a String and has the form "hostname:port", e.g., ! "localhost:8765". ! ! When using an IPv6 address, enclose it within square brackets. ! E.g., "[2001:db8::1]:8765". If {options} is given it must be a |Dictionary|. See |channel-open-options|. --- 649,656 ---- Open a channel to {address}. See |channel|. Returns a Channel. Use |ch_status()| to check for failure. ! {address} is a String, see |channel-address| for the possible ! accepted forms. If {options} is given it must be a |Dictionary|. See |channel-open-options|. *** ../vim-8.2.4683/src/channel.c 2022-04-04 15:16:50.738014123 +0100 --- src/channel.c 2022-04-04 15:44:12.318053555 +0100 *************** *** 44,54 **** --- 44,61 ---- # define sock_write(sd, buf, len) send((SOCKET)sd, buf, len, 0) # define sock_read(sd, buf, len) recv((SOCKET)sd, buf, len, 0) # define sock_close(sd) closesocket((SOCKET)sd) + // Support for Unix-domain sockets was added in Windows SDK 17061. + # define UNIX_PATH_MAX 108 + typedef struct sockaddr_un { + ADDRESS_FAMILY sun_family; + char sun_path[UNIX_PATH_MAX]; + } SOCKADDR_UN, *PSOCKADDR_UN; #else # include # include # include # include + # include # ifdef HAVE_LIBGEN_H # include # endif *************** *** 929,934 **** --- 936,1002 ---- } /* + * Open a socket channel to the UNIX socket at "path". + * Returns the channel for success. + * Returns NULL for failure. + */ + static channel_T * + channel_open_unix( + const char *path, + void (*nb_close_cb)(void)) + { + channel_T *channel = NULL; + int sd = -1; + size_t path_len = STRLEN(path); + struct sockaddr_un server; + size_t server_len; + int waittime = -1; + + if (*path == NUL || path_len >= sizeof(server.sun_path)) + { + semsg(_(e_invalid_argument_str), path); + return NULL; + } + + channel = add_channel(); + if (channel == NULL) + { + ch_error(NULL, "Cannot allocate channel."); + return NULL; + } + + CLEAR_FIELD(server); + server.sun_family = AF_UNIX; + STRNCPY(server.sun_path, path, sizeof(server.sun_path) - 1); + + ch_log(channel, "Trying to connect to %s", path); + + server_len = offsetof(struct sockaddr_un, sun_path) + path_len + 1; + sd = channel_connect(channel, (struct sockaddr *)&server, (int)server_len, + &waittime); + + if (sd < 0) + { + channel_free(channel); + return NULL; + } + + ch_log(channel, "Connection made"); + + channel->CH_SOCK_FD = (sock_T)sd; + channel->ch_nb_close_cb = nb_close_cb; + channel->ch_hostname = (char *)vim_strsave((char_u *)path); + channel->ch_port = 0; + channel->ch_to_be_closed |= (1U << PART_SOCK); + + #ifdef FEAT_GUI + channel_gui_register_one(channel, PART_SOCK); + #endif + + return channel; + } + + /* * Open a socket channel to "hostname":"port". * "waittime" is the time in msec to wait for the connection. * When negative wait forever. *************** *** 1301,1308 **** char_u *address; char_u *p; char *rest; ! int port; int is_ipv6 = FALSE; jobopt_T opt; channel_T *channel = NULL; --- 1369,1377 ---- char_u *address; char_u *p; char *rest; ! int port = 0; int is_ipv6 = FALSE; + int is_unix = FALSE; jobopt_T opt; channel_T *channel = NULL; *************** *** 1319,1326 **** return NULL; } ! // parse address ! if (*address == '[') { // ipv6 address is_ipv6 = TRUE; --- 1388,1405 ---- return NULL; } ! if (*address == NUL) ! { ! semsg(_(e_invalid_argument_str), address); ! return NULL; ! } ! ! if (!STRNCMP(address, "unix:", 5)) ! { ! is_unix = TRUE; ! address += 5; ! } ! else if (*address == '[') { // ipv6 address is_ipv6 = TRUE; *************** *** 1333,1338 **** --- 1412,1418 ---- } else { + // ipv4 address p = vim_strchr(address, ':'); if (p == NULL) { *************** *** 1340,1366 **** return NULL; } } ! port = strtol((char *)(p + 1), &rest, 10); ! if (*address == NUL || port <= 0 || port >= 65536 || *rest != NUL) ! { ! semsg(_(e_invalid_argument_str), address); ! return NULL; ! } ! if (is_ipv6) { ! // strip '[' and ']' ! ++address; ! *(p - 1) = NUL; } - else - *p = NUL; // parse options clear_job_options(&opt); opt.jo_mode = MODE_JSON; opt.jo_timeout = 2000; if (get_job_options(&argvars[1], &opt, ! JO_MODE_ALL + JO_CB_ALL + JO_WAITTIME + JO_TIMEOUT_ALL, 0) == FAIL) goto theend; if (opt.jo_timeout < 0) { --- 1420,1451 ---- return NULL; } } ! ! if (!is_unix) { ! port = strtol((char *)(p + 1), &rest, 10); ! if (port <= 0 || port >= 65536 || *rest != NUL) ! { ! semsg(_(e_invalid_argument_str), address); ! return NULL; ! } ! if (is_ipv6) ! { ! // strip '[' and ']' ! ++address; ! *(p - 1) = NUL; ! } ! else ! *p = NUL; } // parse options clear_job_options(&opt); opt.jo_mode = MODE_JSON; opt.jo_timeout = 2000; if (get_job_options(&argvars[1], &opt, ! JO_MODE_ALL + JO_CB_ALL + JO_TIMEOUT_ALL ! + (is_unix? 0 : JO_WAITTIME), 0) == FAIL) goto theend; if (opt.jo_timeout < 0) { *************** *** 1368,1374 **** goto theend; } ! channel = channel_open((char *)address, port, opt.jo_waittime, NULL); if (channel != NULL) { opt.jo_set = JO_ALL; --- 1453,1462 ---- goto theend; } ! if (is_unix) ! channel = channel_open_unix((char *)address, NULL); ! else ! channel = channel_open((char *)address, port, opt.jo_waittime, NULL); if (channel != NULL) { opt.jo_set = JO_ALL; *************** *** 3268,3275 **** if (channel->ch_hostname != NULL) { ! dict_add_string(dict, "hostname", (char_u *)channel->ch_hostname); ! dict_add_number(dict, "port", channel->ch_port); channel_part_info(channel, dict, "sock", PART_SOCK); } else --- 3356,3369 ---- if (channel->ch_hostname != NULL) { ! if (channel->ch_port) ! { ! dict_add_string(dict, "hostname", (char_u *)channel->ch_hostname); ! dict_add_number(dict, "port", channel->ch_port); ! } ! else ! // Unix-domain socket. ! dict_add_string(dict, "path", (char_u *)channel->ch_hostname); channel_part_info(channel, dict, "sock", PART_SOCK); } else *** ../vim-8.2.4683/src/testdir/check.vim 2021-10-16 21:58:23.206049605 +0100 --- src/testdir/check.vim 2022-04-04 15:39:23.550236560 +0100 *************** *** 95,101 **** endif endfunc ! " Command to check for running on Linix command CheckLinux call CheckLinux() func CheckLinux() if !has('linux') --- 95,101 ---- endif endfunc ! " Command to check for running on Linux command CheckLinux call CheckLinux() func CheckLinux() if !has('linux') *** ../vim-8.2.4683/src/testdir/shared.vim 2021-05-02 18:14:55.466643569 +0100 --- src/testdir/shared.vim 2022-04-04 15:39:23.550236560 +0100 *************** *** 15,24 **** if has('unix') " We also need the job feature or the pkill command to make sure the server " can be stopped. ! if !(executable('python') && (has('job') || executable('pkill'))) return '' endif ! let s:python = 'python' elseif has('win32') " Use Python Launcher for Windows (py.exe) if available. " NOTE: if you get a "Python was not found" error, disable the Python --- 15,30 ---- if has('unix') " We also need the job feature or the pkill command to make sure the server " can be stopped. ! if !(has('job') || executable('pkill')) return '' endif ! if executable('python') ! let s:python = 'python' ! elseif executable('python3') ! let s:python = 'python3' ! else ! return '' ! end elseif has('win32') " Use Python Launcher for Windows (py.exe) if available. " NOTE: if you get a "Python was not found" error, disable the Python *** ../vim-8.2.4683/src/testdir/test_channel.py 2021-07-30 20:56:07.110143138 +0100 --- src/testdir/test_channel.py 2022-04-04 15:39:23.550236560 +0100 *************** *** 22,28 **** class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def setup(self): ! self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) def handle(self): print("=== socket opened ===") --- 22,29 ---- class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def setup(self): ! if self.server.address_family != socket.AF_UNIX: ! self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) def handle(self): print("=== socket opened ===") *** ../vim-8.2.4683/src/testdir/test_channel.vim 2022-03-30 10:14:41.489657276 +0100 --- src/testdir/test_channel.vim 2022-04-04 15:39:23.550236560 +0100 *************** *** 23,28 **** --- 23,31 ---- if g:testfunc =~ '_ipv6()$' let s:localhost = '[::1]:' let s:testscript = 'test_channel_6.py' + elseif g:testfunc =~ '_unix()$' + let s:localhost = 'unix:Xtestsocket' + let s:testscript = 'test_channel_unix.py' else let s:localhost = 'localhost:' let s:testscript = 'test_channel.py' *************** *** 39,44 **** --- 42,56 ---- call RunServer(s:testscript, a:testfunc, a:000) endfunc + " Returns the address of the test server. + func s:address(port) + if s:localhost =~ '^unix:' + return s:localhost + else + return s:localhost . a:port + end + endfunc + " Return a list of open files. " Can be used to make sure no resources leaked. " Returns an empty list on systems where this is not supported. *************** *** 65,71 **** let s:chopt.drop = 'never' " Also add the noblock flag to try it out. let s:chopt.noblock = 1 ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 77,83 ---- let s:chopt.drop = 'never' " Also add the noblock flag to try it out. let s:chopt.noblock = 1 ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 77,83 **** let dict = handle->ch_info() call assert_true(dict.id != 0) call assert_equal('open', dict.status) ! call assert_equal(a:port, string(dict.port)) call assert_equal('open', dict.sock_status) call assert_equal('socket', dict.sock_io) --- 89,98 ---- let dict = handle->ch_info() call assert_true(dict.id != 0) call assert_equal('open', dict.status) ! if has_key(dict, 'port') ! " Channels using Unix sockets have no 'port' entry. ! call assert_equal(a:port, string(dict.port)) ! end call assert_equal('open', dict.sock_status) call assert_equal('socket', dict.sock_io) *************** *** 252,264 **** func Test_communicate_ipv6() CheckIPv6 call Test_communicate() endfunc " Test that we can open two channels. func Ch_two_channels(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) call assert_equal(v:t_channel, type(handle)) if handle->ch_status() == "fail" call assert_report("Can't open channel") --- 267,285 ---- func Test_communicate_ipv6() CheckIPv6 + call Test_communicate() + endfunc + func Test_communicate_unix() + CheckUnix call Test_communicate() + call delete('Xtestsocket') endfunc + " Test that we can open two channels. func Ch_two_channels(port) ! let handle = ch_open(s:address(a:port), s:chopt) call assert_equal(v:t_channel, type(handle)) if handle->ch_status() == "fail" call assert_report("Can't open channel") *************** *** 267,273 **** call assert_equal('got it', ch_evalexpr(handle, 'hello!')) ! let newhandle = ch_open(s:localhost . a:port, s:chopt) if ch_status(newhandle) == "fail" call assert_report("Can't open second channel") return --- 288,294 ---- call assert_equal('got it', ch_evalexpr(handle, 'hello!')) ! let newhandle = ch_open(s:address(a:port), s:chopt) if ch_status(newhandle) == "fail" call assert_report("Can't open second channel") return *************** *** 292,300 **** call Test_two_channels() endfunc " Test that a server crash is handled gracefully. func Ch_server_crash(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 313,327 ---- call Test_two_channels() endfunc + func Test_two_channels_unix() + CheckUnix + call Test_two_channels() + call delete('Xtestsocket') + endfunc + " Test that a server crash is handled gracefully. func Ch_server_crash(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 314,319 **** --- 341,352 ---- call Test_server_crash() endfunc + func Test_server_crash_unix() + CheckUnix + call Test_server_crash() + call delete('Xtestsocket') + endfunc + """"""""" func Ch_handler(chan, msg) *************** *** 323,329 **** endfunc func Ch_channel_handler(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 356,362 ---- endfunc func Ch_channel_handler(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 352,357 **** --- 385,396 ---- call Test_channel_handler() endfunc + func Test_channel_handler_unix() + CheckUnix + call Test_channel_handler() + call delete('Xtestsocket') + endfunc + """"""""" let g:Ch_reply = '' *************** *** 367,373 **** endfunc func Ch_channel_zero(port) ! let handle = (s:localhost .. a:port)->ch_open(s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 406,412 ---- endfunc func Ch_channel_zero(port) ! let handle = (s:address(a:port))->ch_open(s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 415,420 **** --- 454,466 ---- call Test_zero_reply() endfunc + func Test_zero_reply_unix() + CheckUnix + call Test_zero_reply() + call delete('Xtestsocket') + endfunc + + """"""""" let g:Ch_reply1 = "" *************** *** 436,442 **** endfunc func Ch_raw_one_time_callback(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 482,488 ---- endfunc func Ch_raw_one_time_callback(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 462,467 **** --- 508,519 ---- call Test_raw_one_time_callback() endfunc + func Test_raw_one_time_callback_unix() + CheckUnix + call Test_raw_one_time_callback() + call delete('Xtestsocket') + endfunc + """"""""" " Test that trying to connect to a non-existing port fails quickly. *************** *** 1398,1404 **** " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_unlet_handle(port) ! let s:channelfd = ch_open(s:localhost . a:port, s:chopt) eval s:channelfd->ch_sendexpr("test", {'callback': function('s:UnletHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc --- 1450,1456 ---- " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_unlet_handle(port) ! let s:channelfd = ch_open(s:address(a:port), s:chopt) eval s:channelfd->ch_sendexpr("test", {'callback': function('s:UnletHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc *************** *** 1422,1428 **** " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_close_handle(port) ! let s:channelfd = ch_open(s:localhost . a:port, s:chopt) call ch_sendexpr(s:channelfd, "test", {'callback': function('Ch_CloseHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc --- 1474,1480 ---- " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_close_handle(port) ! let s:channelfd = ch_open(s:address(a:port), s:chopt) call ch_sendexpr(s:channelfd, "test", {'callback': function('Ch_CloseHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc *************** *** 1439,1445 **** """""""""" func Ch_open_ipv6(port) ! let handle = ch_open('[::1]:' .. a:port, s:chopt) call assert_notequal('fail', ch_status(handle)) endfunc --- 1491,1497 ---- """""""""" func Ch_open_ipv6(port) ! let handle = ch_open(s:address(a:port), s:chopt) call assert_notequal('fail', ch_status(handle)) endfunc *************** *** 1479,1485 **** func Ch_open_delay(port) " Wait up to a second for the port to open. let s:chopt.waittime = 1000 ! let channel = ch_open(s:localhost . a:port, s:chopt) if ch_status(channel) == "fail" call assert_report("Can't open channel") return --- 1531,1537 ---- func Ch_open_delay(port) " Wait up to a second for the port to open. let s:chopt.waittime = 1000 ! let channel = ch_open(s:address(a:port), s:chopt) if ch_status(channel) == "fail" call assert_report("Can't open channel") return *************** *** 1505,1511 **** endfunc function Ch_test_call(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 1557,1563 ---- endfunc function Ch_test_call(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 1529,1534 **** --- 1581,1592 ---- call Test_call() endfunc + func Test_call_unix() + CheckUnix + call Test_call() + call delete('Xtestsocket') + endfunc + """"""""" let g:Ch_job_exit_ret = 'not yet' *************** *** 1605,1611 **** endfunc function Ch_test_close_callback(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 1663,1669 ---- endfunc function Ch_test_close_callback(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 1625,1632 **** call Test_close_callback() endfunc function Ch_test_close_partial(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 1683,1696 ---- call Test_close_callback() endfunc + func Test_close_callback_unix() + CheckUnix + call Test_close_callback() + call delete('Xtestsocket') + endfunc + function Ch_test_close_partial(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 1651,1656 **** --- 1715,1726 ---- call Test_close_partial() endfunc + func Test_close_partial_unix() + CheckUnix + call Test_close_partial() + call delete('Xtestsocket') + endfunc + func Test_job_start_fails() " this was leaking memory call assert_fails("call job_start([''])", "E474:") *************** *** 1920,1926 **** endfunc function Ch_test_close_lambda(port) ! let handle = ch_open(s:localhost . a:port, s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return --- 1990,1996 ---- endfunc function Ch_test_close_lambda(port) ! let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return *************** *** 1942,1947 **** --- 2012,2023 ---- call Test_close_lambda() endfunc + func Test_close_lambda_unix() + CheckUnix + call Test_close_lambda() + call delete('Xtestsocket') + endfunc + func s:test_list_args(cmd, out, remove_lf) try let g:out = '' *************** *** 2243,2248 **** --- 2319,2326 ---- let job = job_start("cat ", #{in_io: 'null'}) call WaitForAssert({-> assert_equal("dead", job_status(job))}) call assert_equal(0, job_info(job).exitval) + + call delete('Xtestsocket') endfunc func Test_ch_getbufnr() *** ../vim-8.2.4683/src/testdir/test_channel_unix.py 2022-04-04 15:45:57.774034889 +0100 --- src/testdir/test_channel_unix.py 2022-04-04 15:39:23.550236560 +0100 *************** *** 0 **** --- 1,50 ---- + #!/usr/bin/env python + # + # Server that will accept connections from a Vim channel. + # Used by test_channel.vim. + # + # This requires Python 2.6 or later. + + from __future__ import print_function + from test_channel import ThreadedTCPServer, ThreadedTCPRequestHandler, \ + writePortInFile + import socket + import threading + import os + + try: + FileNotFoundError + except NameError: + # Python 2 + FileNotFoundError = (IOError, OSError) + + class ThreadedUnixServer(ThreadedTCPServer): + address_family = socket.AF_UNIX + + def main(path): + server = ThreadedUnixServer(path, ThreadedTCPRequestHandler) + + # Start a thread with the server. That thread will then start a new thread + # for each connection. + server_thread = threading.Thread(target=server.serve_forever) + server_thread.start() + + # Signal the test harness we're ready, the port value has no meaning. + writePortInFile(1234) + + print("Listening on {0}".format(server.server_address)) + + # Main thread terminates, but the server continues running + # until server.shutdown() is called. + try: + while server_thread.is_alive(): + server_thread.join(1) + except (KeyboardInterrupt, SystemExit): + server.shutdown() + + if __name__ == "__main__": + try: + os.remove("Xtestsocket") + except FileNotFoundError: + pass + main("Xtestsocket") *** ../vim-8.2.4683/src/testdir/test_cmdline.vim 2022-03-31 12:33:56.485701120 +0100 --- src/testdir/test_cmdline.vim 2022-04-04 15:39:23.550236560 +0100 *************** *** 620,627 **** \ ':5s': 'substitute', \ "'<,'>s": 'substitute', \ ":'<,'>s": 'substitute', ! \ 'CheckUni': 'CheckUnix', ! \ 'CheckUnix': 'CheckUnix', \ } for [in, want] in items(tests) --- 620,627 ---- \ ':5s': 'substitute', \ "'<,'>s": 'substitute', \ ":'<,'>s": 'substitute', ! \ 'CheckLin': 'CheckLinux', ! \ 'CheckLinux': 'CheckLinux', \ } for [in, want] in items(tests) *** ../vim-8.2.4683/src/version.c 2022-04-04 15:16:50.746014138 +0100 --- src/version.c 2022-04-04 15:40:33.482194513 +0100 *************** *** 752,753 **** --- 752,755 ---- { /* Add new patch number below this line */ + /**/ + 4684, /**/ -- You can be stopped by the police for biking over 65 miles per hour. You are not allowed to walk across a street on your hands. [real standing laws in Connecticut, United States of America] /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// \\\ \\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///