From 71d5505fb8c40c5f01bb279af45c37106215f61f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 22 Feb 2026 23:11:10 -0500 Subject: [PATCH 1/2] perf: async enum file read Problem: parse_enums read the bash-completion file with blocking io.open/fd:read on the main thread, stalling the event loop. Solution: read the file via vim.uv.fs_open/fstat/read with callbacks, running in parallel with the ghostty config system call using the same remaining-counter pattern as the other blink-cmp plugins. --- lua/blink-cmp-ghostty.lua | 58 +++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/lua/blink-cmp-ghostty.lua b/lua/blink-cmp-ghostty.lua index 17f659d..8b9d5d7 100644 --- a/lua/blink-cmp-ghostty.lua +++ b/lua/blink-cmp-ghostty.lua @@ -85,18 +85,7 @@ function M.bash_completion_path() end ---@return table -local function parse_enums() - local path = M.bash_completion_path() - if not path then - return {} - end - local fd = io.open(path, 'r') - if not fd then - return {} - end - local content = fd:read('*a') - fd:close() - +local function parse_enums(content) local enums = {} for key, values in content:gmatch('%-%-([a-z][a-z0-9-]*)%) [^\n]* compgen %-W "([^"]+)"') do local vals = {} @@ -162,17 +151,56 @@ function M:get_completions(ctx, callback) pending[#pending + 1] = { ctx = ctx, callback = callback } if not loading then loading = true - vim.system({ 'ghostty', '+show-config', '--docs' }, {}, function(result) + local config_out, enums_content + local remaining = 2 + + local function on_all_done() + remaining = remaining - 1 + if remaining > 0 then + return + end vim.schedule(function() - keys_cache = parse_keys(result.stdout or '') - enums_cache = parse_enums() + keys_cache = parse_keys(config_out) + enums_cache = parse_enums(enums_content) loading = false for _, p in ipairs(pending) do respond(p.ctx, p.callback) end pending = {} end) + end + + vim.system({ 'ghostty', '+show-config', '--docs' }, {}, function(result) + config_out = result.stdout or '' + on_all_done() end) + + local path = M.bash_completion_path() + if not path then + enums_content = '' + on_all_done() + else + vim.uv.fs_open(path, 'r', 438, function(err, fd) + if err or not fd then + enums_content = '' + on_all_done() + return + end + vim.uv.fs_fstat(fd, function(err2, stat) + if err2 or not stat then + vim.uv.fs_close(fd) + enums_content = '' + on_all_done() + return + end + vim.uv.fs_read(fd, stat.size, 0, function(err3, data) + vim.uv.fs_close(fd) + enums_content = data or '' + on_all_done() + end) + end) + end) + end end return function() end end From 3776133815ad8fb6a3e4019f62f1154e1e71c0f6 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 22 Feb 2026 23:21:06 -0500 Subject: [PATCH 2/2] fix: lint warning and test mocks for async file read Problem: selene flagged unused err3 variable, and test mock_enums still mocked io.open instead of the new vim.uv.fs_* calls. Solution: rename err3 to _, replace io.open mock with synchronous vim.uv.fs_open/fs_fstat/fs_read/fs_close mocks using a sentinel fd. --- lua/blink-cmp-ghostty.lua | 2 +- spec/ghostty_spec.lua | 46 +++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lua/blink-cmp-ghostty.lua b/lua/blink-cmp-ghostty.lua index 8b9d5d7..9eb94ce 100644 --- a/lua/blink-cmp-ghostty.lua +++ b/lua/blink-cmp-ghostty.lua @@ -193,7 +193,7 @@ function M:get_completions(ctx, callback) on_all_done() return end - vim.uv.fs_read(fd, stat.size, 0, function(err3, data) + vim.uv.fs_read(fd, stat.size, 0, function(_, data) vim.uv.fs_close(fd) enums_content = data or '' on_all_done() diff --git a/spec/ghostty_spec.lua b/spec/ghostty_spec.lua index d0ad426..5ef7a2a 100644 --- a/spec/ghostty_spec.lua +++ b/spec/ghostty_spec.lua @@ -55,10 +55,15 @@ local function mock_system() end end +local MOCK_FD = 99 + local function mock_enums() local original_exepath = vim.fn.exepath local original_realpath = vim.uv.fs_realpath - local original_open = io.open + local original_fs_open = vim.uv.fs_open + local original_fs_fstat = vim.uv.fs_fstat + local original_fs_read = vim.uv.fs_read + local original_fs_close = vim.uv.fs_close vim.fn.exepath = function(name) if name == 'ghostty' then @@ -72,24 +77,41 @@ local function mock_enums() end return original_realpath(path) end - -- selene: allow(incorrect_standard_library_use) - io.open = function(path, mode) + vim.uv.fs_open = function(path, flags, mode, callback) if path:match('ghostty%.bash$') then - return { - read = function() - return BASH_COMPLETION - end, - close = function() end, - } + callback(nil, MOCK_FD) + return end - return original_open(path, mode) + return original_fs_open(path, flags, mode, callback) + end + vim.uv.fs_fstat = function(fd, callback) + if fd == MOCK_FD then + callback(nil, { size = #BASH_COMPLETION }) + return + end + return original_fs_fstat(fd, callback) + end + vim.uv.fs_read = function(fd, size, offset, callback) + if fd == MOCK_FD then + callback(nil, BASH_COMPLETION) + return + end + return original_fs_read(fd, size, offset, callback) + end + vim.uv.fs_close = function(fd, ...) + if fd == MOCK_FD then + return true + end + return original_fs_close(fd, ...) end return function() vim.fn.exepath = original_exepath vim.uv.fs_realpath = original_realpath - -- selene: allow(incorrect_standard_library_use) - io.open = original_open + vim.uv.fs_open = original_fs_open + vim.uv.fs_fstat = original_fs_fstat + vim.uv.fs_read = original_fs_read + vim.uv.fs_close = original_fs_close end end