From d30d91f3a49e19e61473b74e42adf68e9215220d Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Tue, 27 Jan 2026 00:18:51 +0100 Subject: [PATCH] fix(ui): only internal messages are unsafe #37462 Problem: Fast context for msg_show event inhibits vim.ui_attach from displaying a stream of messages from a single command. Solution: Remove fast context from msg_show events emitted as a result of explicit API/command calls. The fast context was originally introduced to prevent issues with internal messages. --- runtime/doc/lua.txt | 5 +- runtime/lua/vim/_extui.lua | 15 +-- runtime/lua/vim/_extui/messages.lua | 103 +++++++++++---------- runtime/lua/vim/_extui/shared.lua | 1 + runtime/lua/vim/_meta/builtin.lua | 5 +- runtime/lua/vim/health.lua | 4 - src/gen/gen_api_ui_events.lua | 2 +- src/nvim/api/ui_events.in.h | 2 +- src/nvim/ui.c | 20 +++- test/functional/lua/ui_event_spec.lua | 18 ++-- test/functional/ui/cmdline2_spec.lua | 52 ++++++++++- test/functional/ui/messages2_spec.lua | 128 ++++++++++++++------------ 12 files changed, 215 insertions(+), 140 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 45e6c0ad61..ae89776022 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -801,8 +801,9 @@ vim.ui_attach({ns}, {opts}, {callback}) *vim.ui_attach()* |ui-popupmenu| and the sections below for event format for respective events. - Callbacks for `msg_show` events are executed in |api-fast| context; - showing the message should be scheduled. + Callbacks for `msg_show` events originating from internal messages (as + opposed to events from commands or API calls) are executed in |api-fast| + context; showing the message needs to be scheduled. Excessive errors inside the callback will result in forced detachment. diff --git a/runtime/lua/vim/_extui.lua b/runtime/lua/vim/_extui.lua index f9da5629d5..4d72325dd5 100644 --- a/runtime/lua/vim/_extui.lua +++ b/runtime/lua/vim/_extui.lua @@ -38,17 +38,19 @@ ext.msg = require('vim._extui.messages') ext.cmd = require('vim._extui.cmdline') local M = {} -local function ui_callback(event, ...) +local function ui_callback(redraw_msg, event, ...) local handler = ext.msg[event] or ext.cmd[event] ext.check_targets() handler(...) - -- Cmdline mode and non-empty showcmd requires an immediate redraw. - if ext.cmd[event] or event == 'msg_showcmd' and select(1, ...)[1] then + -- Cmdline mode, non-fast message and non-empty showcmd require an immediate redraw. + if ext.cmd[event] or redraw_msg or (event == 'msg_showcmd' and select(1, ...)[1]) then + ext.redrawing = true api.nvim__redraw({ flush = handler ~= ext.cmd.cmdline_hide or nil, cursor = handler == ext.cmd[event] and true or nil, win = handler == ext.cmd[event] and ext.wins.cmd or nil, }) + ext.redrawing = false end end local scheduled_ui_callback = vim.schedule_wrap(ui_callback) @@ -86,10 +88,11 @@ function M.enable(opts) if not (ext.msg[event] or ext.cmd[event]) then return end - if vim.in_fast_event() then - scheduled_ui_callback(event, ...) + -- Ensure cmdline is placed after a scheduled message in block mode. + if vim.in_fast_event() or (event == 'cmdline_show' and ext.cmd.srow > 0) then + scheduled_ui_callback(false, event, ...) else - ui_callback(event, ...) + ui_callback(event == 'msg_show', event, ...) end return true end) diff --git a/runtime/lua/vim/_extui/messages.lua b/runtime/lua/vim/_extui/messages.lua index 08b230737c..640076345a 100644 --- a/runtime/lua/vim/_extui/messages.lua +++ b/runtime/lua/vim/_extui/messages.lua @@ -37,6 +37,13 @@ local M = { on_dialog_key = 0, -- vim.on_key namespace for paging in the dialog window. } +-- An external redraw indicates the start of a new batch of messages in the cmdline. +api.nvim_set_decoration_provider(ext.ns, { + on_start = function() + M.cmd.count = ext.redrawing and M.cmd.count or 0 + end, +}) + function M.msg:close() self.width, M.virt.msg[M.virt.idx.dupe][1] = 1, nil M.prev_msg = ext.cfg.msg.target == 'msg' and '' or M.prev_msg @@ -182,53 +189,46 @@ end -- We need to keep track of the current message column to be able to -- append or overwrite messages for :echon or carriage returns. -local col, will_full, hlopts = 0, false, { undo_restore = false, invalidate = true, priority = 1 } +local col, hlopts = 0, { undo_restore = false, invalidate = true, priority = 1 } + --- Move messages to cmdline or pager to show in full. local function msg_to_full(src) - if will_full then - return - end - will_full, M.prev_msg = true, '' - - vim.schedule(function() - -- Copy and clear message from src to enlarged cmdline that is dismissed by any - -- key press, or append to pager in case that is already open (not hidden). - local hidden = api.nvim_win_get_config(ext.wins.pager).hide - local tar = hidden and 'cmd' or 'pager' - if tar ~= src then - local srow = hidden and 0 or api.nvim_buf_line_count(ext.bufs.pager) - local marks = api.nvim_buf_get_extmarks(ext.bufs[src], -1, 0, -1, { details = true }) - local lines = api.nvim_buf_get_lines(ext.bufs[src], 0, -1, false) - api.nvim_buf_set_lines(ext.bufs[src], 0, -1, false, {}) - api.nvim_buf_set_lines(ext.bufs[tar], srow, -1, false, lines) - for _, mark in ipairs(marks) do - hlopts.end_col, hlopts.hl_group = mark[4].end_col, mark[4].hl_group - api.nvim_buf_set_extmark(ext.bufs[tar], ext.ns, srow + mark[2], mark[3], hlopts) - end - if tar == 'cmd' and ext.cmd.highlighter then - ext.cmd.highlighter.active[ext.bufs.cmd] = nil - elseif tar == 'pager' then - api.nvim_command('norm! G') - end - M.virt.msg[M.virt.idx.spill][1] = nil - else - for _, id in pairs(M.virt.ids) do - api.nvim_buf_del_extmark(ext.bufs.cmd, ext.ns, id) - end + -- Copy and clear message from src to enlarged cmdline that is dismissed by any + -- key press, or append to pager in case that is already open (not hidden). + local hidden = api.nvim_win_get_config(ext.wins.pager).hide + local tar = hidden and 'cmd' or 'pager' + if tar ~= src then + local srow = hidden and 0 or api.nvim_buf_line_count(ext.bufs.pager) + local marks = api.nvim_buf_get_extmarks(ext.bufs[src], -1, 0, -1, { details = true }) + local lines = api.nvim_buf_get_lines(ext.bufs[src], 0, -1, false) + api.nvim_buf_set_lines(ext.bufs[src], 0, -1, false, {}) + api.nvim_buf_set_lines(ext.bufs[tar], srow, -1, false, lines) + for _, mark in ipairs(marks) do + hlopts.end_col, hlopts.hl_group = mark[4].end_col, mark[4].hl_group + api.nvim_buf_set_extmark(ext.bufs[tar], ext.ns, srow + mark[2], mark[3], hlopts) end - M.msg:close() - M.set_pos(tar) - M[src].count, col, will_full = 0, 0, false - end) + if tar == 'cmd' and ext.cmd.highlighter then + ext.cmd.highlighter.active[ext.bufs.cmd] = nil + elseif tar == 'pager' then + api.nvim_command('norm! G') + end + M[src].count = 0 + M.virt.msg[M.virt.idx.spill][1] = nil + else + for _, id in pairs(M.virt.ids) do + api.nvim_buf_del_extmark(ext.bufs.cmd, ext.ns, id) + end + end + M.set_pos(tar) end +local reset_timer ---@type uv.uv_timer_t? ---@param tar 'cmd'|'dialog'|'msg'|'pager' ---@param content MsgContent ---@param replace_last boolean ---@param append boolean function M.show_msg(tar, content, replace_last, append) local msg, restart, cr, dupe, count = '', false, false, 0, 0 - append = append and col > 0 if M[tar] then -- tar == 'cmd'|'msg' if tar == ext.cfg.msg.target then @@ -244,7 +244,7 @@ function M.show_msg(tar, content, replace_last, append) count = M[tar].count + ((restart or msg == '\n') and 0 or 1) -- Ensure cmdline is clear when writing the first message. - if tar == 'cmd' and not will_full and dupe == 0 and M.cmd.count == 0 and ext.cmd.srow == 0 then + if tar == 'cmd' and dupe == 0 and M.cmd.count == 0 and ext.cmd.srow == 0 then api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) end end @@ -257,7 +257,7 @@ function M.show_msg(tar, content, replace_last, append) local line_count = api.nvim_buf_line_count(ext.bufs[tar]) ---@type integer Start row after last line in the target buffer, unless ---this is the first message, or in case of a repeated or replaced message. - local row = M[tar] and count <= 1 and not will_full and (tar == 'cmd' and ext.cmd.erow or 0) + local row = M[tar] and count <= 1 and ext.cmd.srow == 0 and 0 or line_count - ((replace_last or restart or cr or append) and 1 or 0) local curline = (cr or append) and api.nvim_buf_get_lines(ext.bufs[tar], row, row + 1, false)[1] local start_row, width = row, M.msg.width @@ -298,7 +298,6 @@ function M.show_msg(tar, content, replace_last, append) local texth = api.nvim_win_text_height(ext.wins.msg, { start_row = start_row }) if texth.all > math.ceil(o.lines * 0.5) then msg_to_full(tar) - return end M.set_pos('msg') @@ -314,10 +313,8 @@ function M.show_msg(tar, content, replace_last, append) fn.clearmatches(ext.wins.cmd) -- Clear matchparen highlights. if ext.cmd.srow > 0 then -- In block mode the cmdheight is already dynamic, so just print the full message - -- regardless of height. Spoof cmdline_show to put cmdline below message. - ext.cmd.srow = ext.cmd.srow + 1 + row - start_row - ext.cmd.cmdline_show({}, 0, ':', '', ext.cmd.indent, 0, 0) - api.nvim__redraw({ flush = true, cursor = true, win = ext.wins.cmd }) + -- regardless of height. Put cmdline below message. + ext.cmd.srow = row + 1 else api.nvim_win_set_cursor(ext.wins.cmd, { 1, 0 }) -- ensure first line is visible if ext.cmd.highlighter then @@ -331,7 +328,6 @@ function M.show_msg(tar, content, replace_last, append) if texth.all > ext.cmdheight then msg_to_full(tar) - return end end end @@ -345,10 +341,10 @@ function M.show_msg(tar, content, replace_last, append) end -- Reset message state the next event loop iteration. - if start_row == 0 or ext.cmd.srow > 0 then - vim.schedule(function() - col, M.cmd.count = 0, 0 - end) + if not reset_timer and (col > 0 or M.cmd.count > 0) then + reset_timer = vim.defer_fn(function() + reset_timer, col, M.cmd.count = nil, 0, 0 + end, 0) end end @@ -452,9 +448,14 @@ function M.msg_history_show(entries, prev_cmd) return end - if prev_cmd then - M.msg_clear() -- Showing output of previous command, clear in case still visible. + if cmd_on_key then + -- Dismiss a still open full message cmd window. + api.nvim_feedkeys(vim.keycode(''), 'n', false) + elseif prev_cmd then + -- Showing output of previous command, clear in case still visible. + M.msg_clear() end + api.nvim_buf_set_lines(ext.bufs.pager, 0, -1, false, {}) for i, entry in ipairs(entries) do M.show_msg('pager', entry[2], i == 1, entry[3]) @@ -483,7 +484,7 @@ function M.set_pos(type) } api.nvim_win_set_config(win, config) - if type == 'cmd' then + if type == 'cmd' and not cmd_on_key then -- Temporarily showing a full message in the cmdline, until next key press. local save_spill = M.virt.msg[M.virt.idx.spill][1] local spill = texth.all > height and (' [+%d]'):format(texth.all - height) diff --git a/runtime/lua/vim/_extui/shared.lua b/runtime/lua/vim/_extui/shared.lua index 0c9908f12b..4f28ab6d69 100644 --- a/runtime/lua/vim/_extui/shared.lua +++ b/runtime/lua/vim/_extui/shared.lua @@ -5,6 +5,7 @@ local M = { ns = api.nvim_create_namespace('nvim._ext_ui'), augroup = api.nvim_create_augroup('nvim._ext_ui', {}), cmdheight = vim.o.cmdheight, -- 'cmdheight' option value set by user. + redrawing = false, -- True when redrawing to display UI event. wins = { cmd = -1, dialog = -1, msg = -1, pager = -1 }, bufs = { cmd = -1, dialog = -1, msg = -1, pager = -1 }, cfg = { diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index 93e0e273b8..35398ecfcc 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -227,8 +227,9 @@ function vim.wait(time, callback, interval, fast_only) end --- {callback} receives event name plus additional parameters. See |ui-popupmenu| --- and the sections below for event format for respective events. --- ---- Callbacks for `msg_show` events are executed in |api-fast| context; showing ---- the message should be scheduled. +--- Callbacks for `msg_show` events originating from internal messages (as +--- opposed to events from commands or API calls) are executed in |api-fast| +--- context; showing the message needs to be scheduled. --- --- Excessive errors inside the callback will result in forced detachment. --- diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua index 5f736515e3..0ad3c860d2 100644 --- a/runtime/lua/vim/health.lua +++ b/runtime/lua/vim/health.lua @@ -383,10 +383,6 @@ local function progress_report(len) -- percent=0 omits the reporting of percentage, so use 1% instead -- progress.percent = progress.percent == 0 and 1 or progress.percent progress.id = vim.api.nvim_echo({ { fmt:format(...) } }, false, progress) - -- extui/ui2 shows all messages at once after the healthchecks are finished. - -- This 1ms wait ensures the messages are shown separately - vim.wait(1) - vim.cmd.redraw() end end diff --git a/src/gen/gen_api_ui_events.lua b/src/gen/gen_api_ui_events.lua index 8ba67dafff..3837f52fb4 100644 --- a/src/gen/gen_api_ui_events.lua +++ b/src/gen/gen_api_ui_events.lua @@ -136,7 +136,7 @@ for i = 1, #events do call_output:write(' }\n') call_output:write(' entered = true;\n') write_arglist(call_output, ev) - call_output:write((' ui_call_event("%s", %s, %s)'):format(ev.name, tostring(ev.fast), args)) + call_output:write((' ui_call_event("%s", %s)'):format(ev.name, args)) call_output:write(';\n entered = false;\n') elseif ev.compositor_impl then call_output:write(' ui_comp_' .. ev.name) diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index 0493355262..609d7d50a4 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -166,7 +166,7 @@ void wildmenu_hide(void) void msg_show(String kind, Array content, Boolean replace_last, Boolean history, Boolean append, Object id) - FUNC_API_SINCE(6) FUNC_API_FAST FUNC_API_REMOTE_ONLY; + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void msg_clear(void) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void msg_showcmd(Array content) diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 46d0daebd0..3db55b1b74 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -741,8 +741,26 @@ static void ui_attach_error(uint32_t ns_id, const char *name, const char *msg) msg_schedule_semsg_multiline("Error in \"%s\" UI event handler (ns=%s):\n%s", name, ns, msg); } -void ui_call_event(char *name, bool fast, Array args) +void ui_call_event(char *name, Array args) { + // Internal messages are considered unsafe and are executed in fast context. + bool fast = strcmp(name, "msg_show") == 0; + const char *not_fast[] = { + "empty", + "echo", + "echomsg", + "echoerr", + "list_cmd", + "lua_error", + "lua_print", + "progress", + NULL, + }; + + for (int i = 0; fast && not_fast[i]; i++) { + fast = !strequal(not_fast[i], args.items[0].data.string.data); + } + bool handled = false; UIEventCallback *event_cb; diff --git a/test/functional/lua/ui_event_spec.lua b/test/functional/lua/ui_event_spec.lua index 47681945f9..277394c632 100644 --- a/test/functional/lua/ui_event_spec.lua +++ b/test/functional/lua/ui_event_spec.lua @@ -429,7 +429,7 @@ describe('vim.ui_attach', function() exec_lua([[ vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function(ev) if ev == 'msg_show' then - vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] }) + error('foo') end end) ]]) @@ -437,10 +437,13 @@ describe('vim.ui_attach', function() screen:expect({ grid = [[ | - {1:~ }|*5 + {1:~ }|*2 {3: }| - {9:Error in "msg_show" UI event handler (ns=(UNKNOWN PLUGIN)):} | - {9:fast context failure} | + {9:Lua callback:} | + {9:[string ""]:3: foo} | + {9:stack traceback:} | + {9: [C]: in function 'error'} | + {9: [string ""]:3: in function <[string ""]:1>} | {100:Press ENTER or type command to continue}^ | ]], condition = function() @@ -448,17 +451,12 @@ describe('vim.ui_attach', function() end, }) feed('') - screen:expect([[ - ^ | - {1:~ }|*8 - | - ]]) -- Also when scheduled exec_lua([[ vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function(ev) if ev == 'msg_show' then - vim.schedule(function() vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] }) end) + vim.schedule(function() error('foo') end) end end) ]]) diff --git a/test/functional/ui/cmdline2_spec.lua b/test/functional/ui/cmdline2_spec.lua index ce3187f4f6..c1980eb986 100644 --- a/test/functional/ui/cmdline2_spec.lua +++ b/test/functional/ui/cmdline2_spec.lua @@ -64,7 +64,14 @@ describe('cmdline2', function() {16::}{15:if} {26:1} | {16::} ^ | ]]) - feed('echo "foo"') + feed('echo "foo"') + screen:expect([[ + | + {1:~ }|*11 + {16::}{15:if} {26:1} | + {16::} {15:echo} {26:"foo"}^ | + ]]) + feed('') screen:expect([[ | {1:~ }|*9 @@ -73,13 +80,52 @@ describe('cmdline2', function() {15:foo} | {16::} ^ | ]]) - feed('endif') + feed([[echo input("foo\nbar:")]]) screen:expect([[ | - {1:~ }|*9 + {1:~ }|*7 + :if 1 | + : echo "foo" | + foo | + : echo input("foo\nbar:") | + foo | + bar:^ | + ]]) + feed('baz') + screen:expect([[ + | + {1:~ }|*7 + :if 1 | + : echo "foo" | + foo | + : echo input("foo\nbar:") | + foo | + bar:baz^ | + ]]) + feed('') + screen:expect([[ + | + {1:~ }|*5 {16::}{15:if} {26:1} | {16::} {15:echo} {26:"foo"} | {15:foo} | + {16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} | + {15:foo} | + {15:bar}:baz | + {15:baz} | + {16::} ^ | + ]]) + feed('endif') + screen:expect([[ + | + {1:~ }|*5 + {16::}{15:if} {26:1} | + {16::} {15:echo} {26:"foo"} | + {15:foo} | + {16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} | + {15:foo} | + {15:bar}:baz | + {15:baz} | {16::} {15:endif}^ | ]]) feed('') diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index e52b4a9bf6..9e1ae41d07 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -148,6 +148,19 @@ describe('messages2', function() {1:~ }|*12 | ]]) + -- A redraw indicates the start of messages in the cmdline, which empty should clear. + command('echo "foo" | redraw | echo "bar"') + screen:expect([[ + ^ | + {1:~ }|*12 + bar | + ]]) + command('echo "foo" | redraw | echo ""') + screen:expect([[ + ^ | + {1:~ }|*12 + | + ]]) command('set cmdheight=0') command('echo "foo"') screen:expect([[ @@ -365,61 +378,6 @@ describe('messages2', function() screen:expect(top) end) - it('in cmdline_block mode', function() - feed(':if 1') - screen:expect([[ - | - {1:~ }|*11 - {16::}{15:if} {26:1} | - {16::} ^ | - ]]) - feed([[echo input("foo\nbar:")]]) - screen:expect([[ - | - {1:~ }|*9 - :if 1 | - : echo input("foo\nbar:") | - foo | - bar:^ | - ]]) - feed('baz') - screen:expect([[ - | - {1:~ }|*9 - {16::}{15:if} {26:1} | - {16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} | - {15:baz} | - {16::} ^ | - ]]) - feed([[echo input("foo\nbar:")]]) - screen:expect([[ - | - {1:~ }|*7 - :if 1 | - : echo input("foo\nbar:") | - baz | - : echo input("foo\nbar:") | - foo | - bar:^ | - ]]) - feed(':endif') - screen:expect([[ - | - {1:~ }|*8 - {16::}{15:if} {26:1} | - {16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} | - {15:baz} | - {16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} | - {16::} {16::}{15:endif}^ | - ]]) - feed('') - screen:expect([[ - ^ | - {1:~ }|*12 - | - ]]) - end) - it('FileType is fired after default options are set', function() n.exec([[ let g:set = {} @@ -430,13 +388,12 @@ describe('messages2', function() ]]) screen:expect([[ | - {1:~ }|*9 + {1:~ }|*10 {3: }| - ^foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofo| - {1: }| + foofoofoofoofoofoofoofoofo^o | | ]]) - t.eq({ filetype = 4 }, n.eval('g:set')) -- still fires for 'filetype' + t.eq({ filetype = 5 }, n.eval('g:set')) -- still fires for 'filetype' end) it('Search highlights only apply to pager', function() @@ -467,4 +424,57 @@ describe('messages2', function() {101:fo^o}{100: }| ]]) end) + + it('shows message from still running command', function() + exec_lua(function() + vim.schedule(function() + print('foo') + vim.uv.sleep(100) + print('bar') + end) + end) + screen:expect([[ + ^ | + {1:~ }|*12 + foo | + ]]) + screen:expect([[ + ^ | + {1:~ }|*10 + {3: }| + foo | + bar | + ]]) + end) + + it('properly formatted carriage return messages', function() + screen:try_resize(screen._width, 20) + command([[echon "\r" | echon "Hello" | echon " " | echon "World"]]) + screen:expect([[ + ^ | + {1:~ }|*18 + Hello World | + ]]) + exec_lua(function() + vim.api.nvim_echo({ { 'fooo\nbarbaz\n\nlol', 'statement' }, { '\rbar' } }, true, {}) + vim.api.nvim_echo({ { 'foooooooo', 'statement' }, { 'baz\rb', 'error' } }, true, {}) + vim.api.nvim_echo({ { 'fooobar', 'statement' }, { '\rbaz\n' } }, true, {}) + vim.api.nvim_echo({ { 'fooobar', 'statement' }, { '\rbaz\rb', 'error' } }, true, {}) + vim.api.nvim_echo({ { 'fooo\rbar', 'statement' }, { 'baz', 'error' } }, true, {}) + end) + screen:expect([[ + ^ | + {1:~ }|*9 + {3: }| + {15:fooo} | + {15:barbaz} | + | + bar | + {9:b}{15:oooooooo}{9:baz} | + baz{15:obar} | + | + {9:baz}{15:obar} | + {15:bar}{9:baz} | + ]]) + end) end)