From c792dbc0138afd4fb67d19466342f112ca2608bc Mon Sep 17 00:00:00 2001 From: Brian Walshe Date: Wed, 2 Oct 2024 14:22:13 +0100 Subject: [PATCH 1/2] feat(editor): :read reads the output of an Ex command into the buffer Problem: :read can read from files or stdin, but not from other Ex commands Solution: use `put=execute(cmd)` to execute cmd and send its output to the buffer --- runtime/doc/insert.txt | 22 ++++--- runtime/doc/news.txt | 2 + src/nvim/ex_docmd.c | 32 +++++++++ test/functional/ex_cmds/read_spec.lua | 94 +++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 test/functional/ex_cmds/read_spec.lua diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 48fd442b7e..01065a2356 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -2010,14 +2010,20 @@ NOTE: These commands cannot be used with |:global| or |:vglobal|. *:r!* *:read!* :[range]r[ead] [++opt] !{cmd} - Execute {cmd} and insert its standard output below - the cursor or the specified line. A temporary file is - used to store the output of the command which is then - read into the buffer. 'shellredir' is used to save - the output of the command, which can be set to include - stderr or not. {cmd} is executed like with ":!{cmd}", - any '!' is replaced with the previous command |:!|. - See |++opt| for the possible values of [++opt]. + Execute shell {cmd} and insert its standard output + below the cursor or the specified line. A temporary + file is used to store the output of the command which + is then read into the buffer. 'shellredir' is used to + save the output of the command, which can be set to + include stderr or not. {cmd} is executed like with + ":!{cmd}", any '!' is replaced with the previous + command |:!|. See |++opt| for the possible values of + [++opt]. + + *:r:* *:read:* +:[range]r[ead] :{cmd} + Execute Ex command {cmd} and insert its output below + the cursor or the specified line These commands insert the contents of a file, or the output of a command, into the buffer. They can be undone. They cannot be repeated with the "." diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index e7d4b92f7e..2a5a007491 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -212,6 +212,8 @@ EDITOR "~/" are now expanded to the user's profile directory, not a relative path to a literal "~" directory. • |hl-PmenuMatch| and |hl-PmenuMatchSel| show matched text in completion popup. +• |:read:| reads the output of an Ex command into the buffer. Example:> :read + :ls EVENTS diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index b32a8b867f..0199a570a7 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -12,8 +12,10 @@ #include #include "auto/config.h" +#include "klib/kvec.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/vim.h" #include "nvim/arglist.h" #include "nvim/ascii_defs.h" #include "nvim/autocmd.h" @@ -5820,6 +5822,31 @@ static void ex_syncbind(exarg_T *eap) } } +void do_read_cmd(exarg_T *eap) +{ + Object cmd = CSTR_AS_OBJ(eap->arg + 1); + String cmd_var_name = cstr_as_string("_ex_cmd"); + StringBuilder put_cmd = KV_INITIAL_VALUE; + Error error = ERROR_INIT; + nvim_set_var(cmd_var_name, cmd, &error); + if (error.type != kErrorTypeNone) { + emsg(error.msg); + return; + } + + kv_printf(put_cmd, "%dput=execute(g:%s) | ", eap->line2, cmd_var_name.data); + kv_printf(put_cmd, "execute 'norm! )`.' | "); + kv_printf(put_cmd, "execute 'd _' | "); + + do_cmdline(put_cmd.items, eap->ea_getline, eap->cookie, eap->flags); + kv_destroy(put_cmd); + nvim_del_var(cmd_var_name, &error); + if (error.type != kErrorTypeNone) { + emsg(error.msg); + return; + } +} + static void ex_read(exarg_T *eap) { int empty = (curbuf->b_ml.ml_flags & ML_EMPTY); @@ -5829,6 +5856,11 @@ static void ex_read(exarg_T *eap) return; } + if (*eap->arg == ':') { + do_read_cmd(eap); + return; + } + if (u_save(eap->line2, (linenr_T)(eap->line2 + 1)) == FAIL) { return; } diff --git a/test/functional/ex_cmds/read_spec.lua b/test/functional/ex_cmds/read_spec.lua new file mode 100644 index 0000000000..3c214a0afd --- /dev/null +++ b/test/functional/ex_cmds/read_spec.lua @@ -0,0 +1,94 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local eq, write_file, clear, pcall_err = t.eq, t.write_file, n.clear, t.pcall_err +local fn = n.fn +local setline, getline, setcharpos, execute = fn.setline, fn.getline, fn.setcharpos, fn.execute + +local tmp_file = 'text.txt' +local original_text = { 'First', 'Last' } +local read_text = { ' This is a line starts with a space', ' This one starts with two spaces.' } +local inserted_middle = { original_text[1], read_text[1], read_text[2], original_text[2] } +local inserted_start = { read_text[1], read_text[2], original_text[1], original_text[2] } + +local function test_read(cmd, expected) + setline(1, original_text) + setcharpos('.', { 0, 0, 0 }) + execute(cmd) + for i, e in ipairs(expected) do + eq(e, getline(i)) + end +end + +local function test_undo(cmd) + setline(1, original_text) + execute(cmd) + execute('undo') + eq(original_text, { getline(1), getline(2) }) +end + +describe(':read', function() + local function cleanup() + os.remove(tmp_file) + end + before_each(function() + clear() + cleanup() + write_file(tmp_file, table.concat(read_text, '\n'), true) + end) + after_each(cleanup) + it('inserts text from file', function() + test_read('read ' .. tmp_file, inserted_middle) + eq({ 0, 2, 2, 0 }, fn.getpos('.')) + end) + it('inserts text from shell', function() + test_read('read !cat ' .. tmp_file, inserted_middle) + eq({ 0, 3, 3, 0 }, fn.getpos('.')) + end) + it('inserts text from Ex command', function() + local make_lines = string.format('let lines="%s"', table.concat(read_text, '\\n')) + execute(make_lines) + test_read('read :echo lines', inserted_middle) + end) + it('inserts text from file at specific position', function() + test_read('0read ' .. tmp_file, inserted_start) + eq({ 0, 1, 2, 0 }, fn.getpos('.')) + end) + it('inserts text from shell cmd at specific position', function() + test_read('0read !cat ' .. tmp_file, inserted_start) + eq({ 0, 2, 3, 0 }, fn.getpos('.')) + end) + it('executes next command when using |', function() + local make_lines = string.format('let lines="%s"', table.concat(read_text, '\\n')) + execute(make_lines) + execute("let guard = 'fail'") + test_read("read :echo lines | let guard='pass'", inserted_middle) + eq('pass', vim.trim(execute('echo guard'))) + end) + it('sets fileformat, fileencoding, bomb correctly', function() + execute('set fileformat=dos') + execute('set fileencoding=latin1') + execute('set bomb') + execute('read ++edit ' .. tmp_file) + eq('fileformat=unix', vim.trim(execute('set fileformat?'))) + eq('fileencoding=utf-8', vim.trim(execute('set fileencoding?'))) + eq('nobomb', vim.trim(execute('set bomb?'))) + end) + it('file reads can be undone', function() + test_undo('read ' .. tmp_file) + end) + it('shell reads can be undone', function() + test_undo('read !cat ' .. tmp_file) + end) + it('command reads can be undone', function() + local make_lines = string.format('let lines="%s"', table.concat(read_text, '\\n')) + execute(make_lines) + test_undo('read :echo lines') + end) + it('errors out correctly when a non-existant file is used', function() + eq("Vim(read):E484: Can't open file asdfasdf", pcall_err(execute, ':read asdfasdf')) + end) + it('errors out correctly when an invalid command is used', function() + eq('Vim:E492: Not an editor command: asdfasdf', pcall_err(execute, ':read :asdfasdf')) + end) +end) From 5b410d1e491d628a01d94819e5948c5af62198e3 Mon Sep 17 00:00:00 2001 From: Brian Walshe Date: Mon, 16 Dec 2024 10:17:51 +0000 Subject: [PATCH 2/2] feat(editor): :read reads the output of an Ex command into the buffer --- runtime/doc/insert.txt | 6 +++--- runtime/doc/news.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 01065a2356..079e834734 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -2017,13 +2017,13 @@ NOTE: These commands cannot be used with |:global| or |:vglobal|. save the output of the command, which can be set to include stderr or not. {cmd} is executed like with ":!{cmd}", any '!' is replaced with the previous - command |:!|. See |++opt| for the possible values of - [++opt]. + command |:!|. + See |++opt| for the possible values of [++opt]. *:r:* *:read:* :[range]r[ead] :{cmd} Execute Ex command {cmd} and insert its output below - the cursor or the specified line + the cursor or the specified line. These commands insert the contents of a file, or the output of a command, into the buffer. They can be undone. They cannot be repeated with the "." diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 2a5a007491..0c626e0a2e 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -212,8 +212,8 @@ EDITOR "~/" are now expanded to the user's profile directory, not a relative path to a literal "~" directory. • |hl-PmenuMatch| and |hl-PmenuMatchSel| show matched text in completion popup. -• |:read:| reads the output of an Ex command into the buffer. Example:> :read - :ls +• |:read:| reads the output of an Ex command into the buffer. Example:> + :read :ls EVENTS