Goals
This should be a quick reference guide for those familiar with NodeJS on how to execute the same async tasks in Lua using luv. This is aimed towards use cases inside Neovim, but is not limited to those cases.
Task
We want to spawn a child task to convert a markdown document into HTML using pandoc. This could be used as part of a publishing flow for a blog, for writing notes, or for implementing a markdown previewer.
// in Node we could do it like this:
const {spawn} = require('child_process');
const [sourceFile, destinationFile] = process.argv.slice(2)
const convert = spawn('pandoc', [sourceFile, '--from', 'gfm', '--to', 'html5', '-o', destinationFile, '-s', '--highlight-style', 'tango'])
convert.stderr.on('data', (data) => {
console.error(data);
});
convert.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
In Lua, the code looks very similar, but a bit more verbose:
-- The same as before, but this time we want to generate the file names
-- based on the file we are currently editing instead of passing them as commandline args
function convert()
-- cut off the `.md` part of the file
local destinationFile = vim.fn.expand('%:t:r')
-- the name of the file you're editing
local sourceFile = api.nvim_buf_get_name(0)
spawn('pandoc', {
args = {sourceFile, '--from', 'gfm', '--to', 'html5', '-o', string.format('%s.html', destinationFile), '-s', '--highlight-style', 'tango'},
},
{stdout = function()end, stderr = function(data) print(data) end},
function(code) -- we want to call this function when the process is done
print('child process exited with code ' .. string.format('%d', code))
end)
end
function spawn(cmd, opts, input, onexit)
local handle, pid
-- open an new pipe for stdout
local stdout = vim.loop.new_pipe(false)
-- open an new pipe for stderr
local stderr = vim.loop.new_pipe(false)
handle, pid = vim.loop.spawn(cmd, vim.tbl_extend("force", opts, {stdio = {stdout; stderr;}}),
function(code, signal)
-- call the exit callback with the code and signal
onexit(code, signal)
-- stop reading data to stdout
vim.loop.read_stop(stdout)
-- stop reading data to stderr
vim.loop.read_stop(stderr)
-- safely shutdown child process
safe_close(handle)
-- safely shutdown stdout pipe
safe_close(stdout)
-- safely shutdown stderr pipe
safe_close(stderr)
end)
-- read child process output to stdout
vim.loop.read_start(stdout, input.stdout)
-- read child process output to stderr
vim.loop.read_start(stderr, input.stderr)
end
function safe_close(handle)
if not vim.loop.is_closing(handle) then
vim.loop.close(handle)
end
end
One of the major differences is that in Lua you are responsible for cleaning up both the process handle and any pipes you have open to receive data from that handle. It then becomes useful to create a more generalized spawn
function to handle all of these things under the hood, allowing you to just call spawn
in a similar manner to the NodeJS API.
Top comments (2)
That's not entirely true: This is not related to Lua itself, but to the nvim API. Lua itself doesn't really have a concept of pipes or processes or anything like that.
There's also no real reason to actually do all of this in Lua: you can just use RPC protocol that nvim comes with to do all of the logic in javascript in a separate process.
It's also simpler to just write this kind of stuff in vimscript, most of the time.
Minor nitpick:
this could just be
:D