From 695ff4aa46bdf77f67d1643d4abd6ea10b665d4c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 20 Feb 2026 20:18:49 -0500 Subject: [PATCH 1/3] feat: add healthcheck Problem: users had no way to diagnose why completions were missing or incomplete. Solution: add a :checkhealth module that verifies blink.cmp is installed, tmux is on PATH and responds to list-commands, and man is available for command descriptions. --- lua/blink-cmp-tmux/health.lua | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lua/blink-cmp-tmux/health.lua diff --git a/lua/blink-cmp-tmux/health.lua b/lua/blink-cmp-tmux/health.lua new file mode 100644 index 0000000..86839e3 --- /dev/null +++ b/lua/blink-cmp-tmux/health.lua @@ -0,0 +1,36 @@ +local M = {} + +function M.check() + vim.health.start('blink-cmp-tmux') + + local ok = pcall(require, 'blink.cmp') + if ok then + vim.health.ok('blink.cmp is installed') + else + vim.health.error('blink.cmp is not installed') + end + + local bin = vim.fn.exepath('tmux') + if bin ~= '' then + vim.health.ok('tmux executable found: ' .. bin) + else + vim.health.error('tmux executable not found') + return + end + + local result = vim.system({ 'tmux', 'list-commands' }):wait() + if result.code == 0 and result.stdout and result.stdout ~= '' then + vim.health.ok('tmux list-commands produces output') + else + vim.health.warn('tmux list-commands failed (completions will be unavailable)') + end + + local man_bin = vim.fn.exepath('man') + if man_bin ~= '' then + vim.health.ok('man executable found: ' .. man_bin) + else + vim.health.warn('man executable not found (command descriptions will be unavailable)') + end +end + +return M From 70039966430a616bf204a5b9775491d40282525a Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:36:41 -0500 Subject: [PATCH 2/3] feat: add healthcheck (#7) Problem: users had no way to diagnose why completions were missing or incomplete. Solution: add a :checkhealth module that verifies blink.cmp is installed, tmux is on PATH and responds to list-commands, and man is available for command descriptions. --- .luarc.json | 6 ++++++ lua/blink-cmp-tmux.lua | 6 ++---- lua/blink-cmp-tmux/health.lua | 36 +++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 lua/blink-cmp-tmux/health.lua diff --git a/.luarc.json b/.luarc.json index b438cce..0c49a3b 100644 --- a/.luarc.json +++ b/.luarc.json @@ -2,6 +2,12 @@ "runtime.version": "Lua 5.1", "runtime.path": ["lua/?.lua", "lua/?/init.lua"], "diagnostics.globals": ["vim", "jit"], + "diagnostics.disable": [ + "undefined-doc-name", + "undefined-doc-class", + "undefined-field", + "need-check-nil" + ], "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"], "workspace.checkThirdParty": false, "completion.callSnippet": "Replace" diff --git a/lua/blink-cmp-tmux.lua b/lua/blink-cmp-tmux.lua index 5faf8f7..dc8dce7 100644 --- a/lua/blink-cmp-tmux.lua +++ b/lua/blink-cmp-tmux.lua @@ -45,7 +45,7 @@ local function parse_descriptions() local j = def.line + 1 while j <= block_end do local l = lines[j] - if l:match('^%s+%(alias:') then + if l:match('^%s+%(alias:') or vim.trim(l) == '' then j = j + 1 elseif l:match('^ ') then local stripped = vim.trim(l) @@ -54,8 +54,6 @@ local function parse_descriptions() else break end - elseif vim.trim(l) == '' then - j = j + 1 else break end @@ -84,7 +82,7 @@ local function parse_descriptions() end end local desc = table.concat(parts, '\n\n') - desc = desc:gsub('\xe2\x80\x90 ', '') + desc = desc:gsub(string.char(0xe2, 0x80, 0x90) .. ' ', '') desc = desc:gsub(' +', ' ') if desc ~= '' then descs[def.cmd] = desc diff --git a/lua/blink-cmp-tmux/health.lua b/lua/blink-cmp-tmux/health.lua new file mode 100644 index 0000000..86839e3 --- /dev/null +++ b/lua/blink-cmp-tmux/health.lua @@ -0,0 +1,36 @@ +local M = {} + +function M.check() + vim.health.start('blink-cmp-tmux') + + local ok = pcall(require, 'blink.cmp') + if ok then + vim.health.ok('blink.cmp is installed') + else + vim.health.error('blink.cmp is not installed') + end + + local bin = vim.fn.exepath('tmux') + if bin ~= '' then + vim.health.ok('tmux executable found: ' .. bin) + else + vim.health.error('tmux executable not found') + return + end + + local result = vim.system({ 'tmux', 'list-commands' }):wait() + if result.code == 0 and result.stdout and result.stdout ~= '' then + vim.health.ok('tmux list-commands produces output') + else + vim.health.warn('tmux list-commands failed (completions will be unavailable)') + end + + local man_bin = vim.fn.exepath('man') + if man_bin ~= '' then + vim.health.ok('man executable found: ' .. man_bin) + else + vim.health.warn('man executable not found (command descriptions will be unavailable)') + end +end + +return M From c0c59d1e5771a8de25b547dc18eb274cbce2438c Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:39:32 -0500 Subject: [PATCH 3/3] perf: async parallel cache initialization and remove deepcopy (#6) Problem: first completion request blocked the UI with three sequential synchronous vim.system():wait() calls (man page, command names, command list), and every subsequent completion unnecessarily deep-copied the entire cache. Solution: run all three system calls concurrently via vim.system callbacks, merging results when all complete. Queue pending completion requests during loading. Return cached items directly instead of deep-copying. --- lua/blink-cmp-tmux.lua | 77 ++++++++++++++++++++++++++++++++---------- spec/tmux_spec.lua | 39 ++++++++++----------- 2 files changed, 79 insertions(+), 37 deletions(-) diff --git a/lua/blink-cmp-tmux.lua b/lua/blink-cmp-tmux.lua index dc8dce7..1653c06 100644 --- a/lua/blink-cmp-tmux.lua +++ b/lua/blink-cmp-tmux.lua @@ -3,6 +3,9 @@ local M = {} ---@type blink.cmp.CompletionItem[]? local cache = nil +local loading = false +---@type {ctx: blink.cmp.Context, callback: fun(response: blink.cmp.CompletionResponse)}[] +local pending = {} function M.new() return setmetatable({}, { __index = M }) @@ -13,18 +16,17 @@ function M.enabled() return vim.bo.filetype == 'tmux' end +---@param man_stdout string +---@param names_stdout string ---@return table -local function parse_descriptions() - local result = vim.system({ 'bash', '-c', 'MANWIDTH=80 man -P cat tmux 2>/dev/null' }):wait() - local stdout = result.stdout or '' +local function parse_descriptions(man_stdout, names_stdout) local lines = {} - for line in (stdout .. '\n'):gmatch('(.-)\n') do + for line in (man_stdout .. '\n'):gmatch('(.-)\n') do lines[#lines + 1] = line end - local cmd_result = vim.system({ 'tmux', 'list-commands', '-F', '#{command_list_name}' }):wait() local cmds = {} - for name in (cmd_result.stdout or ''):gmatch('[^\n]+') do + for name in names_stdout:gmatch('[^\n]+') do cmds[name] = true end @@ -126,27 +128,66 @@ end ---@param ctx blink.cmp.Context ---@param callback fun(response: blink.cmp.CompletionResponse) ----@return fun() -function M:get_completions(ctx, callback) - if not cache then - local ok, descs = pcall(parse_descriptions) - if not ok then - descs = {} - end - local result = vim.system({ 'tmux', 'list-commands' }):wait() - cache = parse(result.stdout or '', descs) - end - +local function respond(ctx, callback) local before = ctx.line:sub(1, ctx.cursor[2]) if before:match('^%s*[a-z-]*$') then callback({ is_incomplete_forward = false, is_incomplete_backward = false, - items = vim.deepcopy(cache), + items = cache, }) else callback({ items = {} }) end +end + +---@param ctx blink.cmp.Context +---@param callback fun(response: blink.cmp.CompletionResponse) +---@return fun() +function M:get_completions(ctx, callback) + if cache then + respond(ctx, callback) + return function() end + end + + pending[#pending + 1] = { ctx = ctx, callback = callback } + if not loading then + loading = true + local man_out, names_out, cmds_out + local remaining = 3 + + local function on_all_done() + remaining = remaining - 1 + if remaining > 0 then + return + end + vim.schedule(function() + local ok, descs = pcall(parse_descriptions, man_out, names_out) + if not ok then + descs = {} + end + cache = parse(cmds_out, descs) + loading = false + for _, p in ipairs(pending) do + respond(p.ctx, p.callback) + end + pending = {} + end) + end + + vim.system({ 'bash', '-c', 'MANWIDTH=80 man -P cat tmux 2>/dev/null' }, {}, function(result) + man_out = result.stdout or '' + on_all_done() + end) + vim.system({ 'tmux', 'list-commands', '-F', '#{command_list_name}' }, {}, function(result) + names_out = result.stdout or '' + on_all_done() + end) + vim.system({ 'tmux', 'list-commands' }, {}, function(result) + cmds_out = result.stdout or '' + on_all_done() + end) + end return function() end end diff --git a/spec/tmux_spec.lua b/spec/tmux_spec.lua index 3f47a97..da0c32e 100644 --- a/spec/tmux_spec.lua +++ b/spec/tmux_spec.lua @@ -26,37 +26,38 @@ local MAN_PAGE = table.concat({ }, '\n') local function mock_system() - local original = vim.system + local original_system = vim.system + local original_schedule = vim.schedule ---@diagnostic disable-next-line: duplicate-set-field - vim.system = function(cmd) + vim.system = function(cmd, _, on_exit) + local result if cmd[1] == 'bash' then - return { - wait = function() - return { stdout = MAN_PAGE, code = 0 } - end, - } + result = { stdout = MAN_PAGE, code = 0 } elseif cmd[1] == 'tmux' and cmd[2] == 'list-commands' then if cmd[3] == '-F' then - return { - wait = function() - return { stdout = TMUX_NAMES, code = 0 } - end, - } + result = { stdout = TMUX_NAMES, code = 0 } + else + result = { stdout = TMUX_COMMANDS, code = 0 } end - return { - wait = function() - return { stdout = TMUX_COMMANDS, code = 0 } - end, - } + else + result = { stdout = '', code = 1 } + end + if on_exit then + on_exit(result) + return {} end return { wait = function() - return { stdout = '', code = 1 } + return result end, } end + vim.schedule = function(fn) + fn() + end return function() - vim.system = original + vim.system = original_system + vim.schedule = original_schedule end end