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.