Coroutines
Coroutines enable cooperative multitasking. Understand them for advanced Neovim plugin development.
Coroutine Basics
What are Coroutines?
Coroutines are like functions that can suspend and resume execution. Unlike threads, they don’t run concurrently - they cooperatively yield control.
-- Create coroutine
local co = coroutine.create(function()
print("Start")
coroutine.yield() -- Pause here
print("Resume")
coroutine.yield() -- Pause here
print("End")
end)
print(coroutine.status(co)) -- "suspended"
coroutine.resume(co) -- Prints "Start"
print(coroutine.status(co)) -- "suspended"
coroutine.resume(co) -- Prints "Resume"
coroutine.resume(co) -- Prints "End"
print(coroutine.status(co)) -- "dead"
Coroutine Functions
-- Create coroutine (returns thread)
local co = coroutine.create(fn)
-- Resume execution (returns success, values)
local ok, result = coroutine.resume(co, arg1, arg2)
-- Yield control (inside coroutine)
coroutine.yield(value1, value2)
-- Check status: "running", "suspended", "normal", "dead"
local status = coroutine.status(co)
-- Get running coroutine
local current = coroutine.running()
-- Wrap coroutine in function (auto-resume)
local wrapped = coroutine.wrap(fn)
Passing Values
Arguments and Returns
local co = coroutine.create(function(a, b)
print("Got:", a, b)
local x, y = coroutine.yield(a + b)
print("Resumed with:", x, y)
return "done"
end)
-- First resume passes initial args
local ok, result = coroutine.resume(co, 10, 20)
print("Yield returned:", result) -- 30
-- Subsequent resumes pass to yield
local ok, result = coroutine.resume(co, 100, 200)
print("Final result:", result) -- "done"
-- Output:
-- Got: 10 20
-- Yield returned: 30
-- Resumed with: 100 200
-- Final result: done
Producer-Consumer
-- Producer generates values
local function producer()
for i = 1, 5 do
coroutine.yield(i)
end
end
-- Consumer processes values
local co = coroutine.create(producer)
while true do
local ok, value = coroutine.resume(co)
if not ok or coroutine.status(co) == "dead" then
break
end
print("Consumed:", value)
end
Iterator Pattern
Coroutine as Iterator
-- Generator function
local function range(start, stop, step)
step = step or 1
return coroutine.wrap(function()
for i = start, stop, step do
coroutine.yield(i)
end
end)
end
-- Usage
for i in range(1, 10, 2) do
print(i) -- 1, 3, 5, 7, 9
end
-- File line iterator
local function lines(filename)
return coroutine.wrap(function()
local file = io.open(filename)
if file then
for line in file:lines() do
coroutine.yield(line)
end
file:close()
end
end)
end
for line in lines("/etc/hosts") do
print(line)
end
Permutations Generator
local function permutations(arr)
local function permute(a, n)
if n == 0 then
coroutine.yield(a)
else
for i = 1, n do
a[i], a[n] = a[n], a[i]
permute(a, n - 1)
a[i], a[n] = a[n], a[i]
end
end
end
return coroutine.wrap(function()
permute({table.unpack(arr)}, #arr)
end)
end
for p in permutations({1, 2, 3}) do
print(table.concat(p, ", "))
end
-- 1, 2, 3
-- 2, 1, 3
-- 3, 1, 2
-- ...
State Machines
local function traffic_light()
while true do
print("GREEN - Go")
coroutine.yield()
print("YELLOW - Slow down")
coroutine.yield()
print("RED - Stop")
coroutine.yield()
end
end
local light = coroutine.create(traffic_light)
-- Simulate time passing
for i = 1, 6 do
coroutine.resume(light)
-- In real code: vim.defer_fn or timer
end
Async Patterns
Simple Async
-- Simulate async operation
local function async_fetch(url, callback)
-- In real code: HTTP request
vim.defer_fn(function()
callback({url = url, data = "result"})
end, 1000)
end
-- Coroutine wrapper
local function fetch(url)
local co = coroutine.running()
async_fetch(url, function(result)
coroutine.resume(co, result)
end)
return coroutine.yield()
end
-- Usage (must be in coroutine)
local function main()
local result = fetch("https://api.example.com/data")
print("Got:", result.data)
end
coroutine.wrap(main)()
Async/Await Pattern
local function async(fn)
return function(...)
local args = {...}
return coroutine.wrap(function()
return fn(table.unpack(args))
end)()
end
end
local function await(promise)
local co = coroutine.running()
promise:then_(function(result)
coroutine.resume(co, result)
end)
return coroutine.yield()
end
-- Not common in Lua, but shows the pattern
Neovim Async
vim.schedule
-- Schedule function to run in main loop
vim.schedule(function()
-- Safe to call Neovim API here
vim.notify("Async complete")
end)
-- vim.defer_fn for delayed execution
vim.defer_fn(function()
print("Delayed by 1 second")
end, 1000)
Plenary Async
-- Using plenary.nvim async module
local async = require("plenary.async")
-- Define async function
local fetch_data = async.wrap(function(url, callback)
-- Async operation
vim.defer_fn(function()
callback({data = "result"})
end, 1000)
end, 2) -- 2 = number of args including callback
-- Use in async context
async.run(function()
local result = fetch_data("https://api.example.com")
vim.notify("Got: " .. result.data)
end)
Job Control
-- Run external command asynchronously
local function run_command(cmd, callback)
local output = {}
vim.fn.jobstart(cmd, {
stdout_buffered = true,
on_stdout = function(_, data)
for _, line in ipairs(data) do
if line ~= "" then
table.insert(output, line)
end
end
end,
on_exit = function(_, code)
callback(output, code)
end
})
end
-- Usage
run_command({"ls", "-la"}, function(output, code)
if code == 0 then
for _, line in ipairs(output) do
print(line)
end
end
end)
Error Handling
local co = coroutine.create(function()
error("Something went wrong")
end)
local ok, err = coroutine.resume(co)
if not ok then
print("Coroutine error:", err)
end
-- With pcall inside coroutine
local safe_co = coroutine.create(function()
local ok, result = pcall(function()
error("Protected error")
end)
if not ok then
coroutine.yield(nil, result)
end
end)
Practical Example
Batch Processor
-- Process items in batches, yielding between batches
local function batch_process(items, batch_size, processor)
return coroutine.wrap(function()
local batch = {}
for i, item in ipairs(items) do
table.insert(batch, item)
if #batch >= batch_size then
local results = processor(batch)
coroutine.yield(results)
batch = {}
end
end
-- Final batch
if #batch > 0 then
coroutine.yield(processor(batch))
end
end)
end
-- Usage
local items = {}
for i = 1, 100 do items[i] = "item-" .. i end
for batch_results in batch_process(items, 10, function(batch)
-- Process batch
return vim.tbl_map(string.upper, batch)
end) do
print("Processed batch of " .. #batch_results)
-- In Neovim, could yield to UI here
end
Next Module
Neovim Integration - vim.*, Neovim API, plugin development.