Skip to content

Instantly share code, notes, and snippets.

@VonHeikemen
Last active April 12, 2026 13:51
Show Gist options
  • Select an option

  • Save VonHeikemen/45c970f16cc8059bba7af4fafb423975 to your computer and use it in GitHub Desktop.

Select an option

Save VonHeikemen/45c970f16cc8059bba7af4fafb423975 to your computer and use it in GitHub Desktop.
Coordinate tasks using lua coroutines
local M = {}
local function pack_len(...)
return {n = select('#', ...), ...}
end
function M.run(f)
local t = coroutine.create(f)
local context = {}
context.resume = function(...)
if coroutine.status(t) ~= 'suspended' then
return
end
local ok, e = coroutine.resume(t, ...)
if not ok then
error(e)
end
end
context.yield = function()
return coroutine.yield()
end
context.await = function(f, ...)
local args = pack_len(...)
local argc = args.n + 1
args[argc] = context.resume
f(unpack(args, 1, argc))
return coroutine.yield()
end
local ok, e = coroutine.resume(t, context)
if not ok then
error(e)
end
end
return M
@VonHeikemen
Copy link
Copy Markdown
Author

VonHeikemen commented Apr 11, 2026

Example

This is about coordinating functions. I'll be using a Neovim functions as example.

In Neovim vim.ui.input() is like an interface that people extend (with something like snacks.nvim input), and when they do is usually non-blocking. This is great because you don't freeze the editor while trying to type something on the input. The downside is that we have to use callbacks.

vim.ui.input({prompt = 'New name:'}, function(name) 
  if name == nil then
    vim.print('User canceled the operation')
    return
  end

  vim.print('user finished typing')
end)

-- One does not simply use `name` after `vim.ui.input()`
-- this will emit a horrible error
local new_name = name:upper()
vim.print(new_name)

The problem is name will not be available outside the callback. As a user you are happy that the editor is not frozen. But as the author of the code is frustrating that you can't just write "normal code" that reads like a sequence of steps.

With coroutines we can pause and resume a function. Long story short, with coroutine.yield() we relinquish control to the "top level scope" that is executing our function. We are pausing the function. Waiting. coroutine.resume() gives the control back to us. If you make a few tweaks a here or there you can flatten the stack and make it easy to use.

-- Assuming you have co2.lua in your personal configuration, as a lua module.
-- co2 would be in ~/.config/nvim/lua/co2.lua
-- and the code on this snippet is located some other place

local co2 = require('co2')

co2.run(function(ctx)
  vim.ui.input({prompt = 'New name:'}, ctx.resume)
  local name = ctx.yield()

  if name == nil then
    vim.print('User canceled the operation')
    return
  end
  
  vim.print('user finished typing')

  local new_name = name:upper()
  vim.print(new_name)
end)

Provided that the callback is the last argument of the non-blocking function we could use ctx.await().

local name = ctx.await(vim.ui.input, {prompt = 'New name:'})

This eliminates the need to remember using ctx.resume() and ctx.yield(), at the cost of being less flexible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment