Compare commits

..

1 commit

Author SHA1 Message Date
8bb6a81d1f
perf: async cache initialization and remove deepcopy
Problem: first completion request blocked the UI with a synchronous
vim.system():wait() call, and every subsequent key completion
unnecessarily deep-copied the entire cache.

Solution: use vim.system with an async callback to initialize the cache
without blocking. Queue pending completion requests during loading and
serve them once parsing finishes. Return cached keys directly instead
of deep-copying.
2026-02-20 20:16:33 -05:00
4 changed files with 28 additions and 221 deletions

View file

@ -13,31 +13,9 @@ function M.new()
return setmetatable({}, { __index = M })
end
local ghostty_config_dirs = {
vim.fn.expand('$XDG_CONFIG_HOME/ghostty'),
vim.fn.expand('$HOME/.config/ghostty'),
'/etc/ghostty',
}
---@return boolean
function M.enabled()
if vim.bo.filetype == 'ghostty' then
return true
end
if vim.bo.filetype ~= 'config' and vim.bo.filetype ~= '' then
return false
end
local path = vim.api.nvim_buf_get_name(0)
if path == '' then
return false
end
local real = vim.uv.fs_realpath(path) or path
for _, dir in ipairs(ghostty_config_dirs) do
if real:find(dir, 1, true) == 1 then
return true
end
end
return false
return vim.bo.filetype == 'ghostty'
end
---@param stdout string
@ -67,25 +45,28 @@ local function parse_keys(stdout)
return items
end
---@return string?
function M.bash_completion_path()
---@return table<string, string[]>
local function parse_enums()
local bin = vim.fn.exepath('ghostty')
if bin == '' then
return nil
return {}
end
local real = vim.uv.fs_realpath(bin)
if not real then
return nil
return {}
end
local prefix = real:match('(.*)/bin/ghostty$')
if not prefix then
return nil
return {}
end
return prefix .. '/share/bash-completion/completions/ghostty.bash'
end
local path = prefix .. '/share/bash-completion/completions/ghostty.bash'
local fd = io.open(path, 'r')
if not fd then
return {}
end
local content = fd:read('*a')
fd:close()
---@return table<string, string[]>
local function parse_enums(content)
local enums = {}
for key, values in content:gmatch('%-%-([a-z][a-z0-9-]*)%) [^\n]* compgen %-W "([^"]+)"') do
local vals = {}
@ -102,9 +83,6 @@ end
---@param ctx blink.cmp.Context
---@param callback fun(response: blink.cmp.CompletionResponse)
local function respond(ctx, callback)
if not keys_cache or not enums_cache then
return
end
local line = ctx.line
local col = ctx.cursor[2]
local eq_pos = line:find('=')
@ -151,56 +129,17 @@ function M:get_completions(ctx, callback)
pending[#pending + 1] = { ctx = ctx, callback = callback }
if not loading then
loading = true
local config_out, enums_content
local remaining = 2
local function on_all_done()
remaining = remaining - 1
if remaining > 0 then
return
end
vim.system({ 'ghostty', '+show-config', '--docs' }, {}, function(result)
vim.schedule(function()
keys_cache = parse_keys(config_out)
enums_cache = parse_enums(enums_content)
keys_cache = parse_keys(result.stdout or '')
enums_cache = parse_enums()
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(_, data)
vim.uv.fs_close(fd)
enums_content = data or ''
on_all_done()
end)
end)
end)
end
end
return function() end
end

View file

@ -1,47 +0,0 @@
local M = {}
function M.check()
vim.health.start('blink-cmp-ghostty')
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('ghostty')
if bin ~= '' then
vim.health.ok('ghostty executable found: ' .. bin)
else
vim.health.error('ghostty executable not found')
return
end
local result = vim.system({ 'ghostty', '+show-config', '--docs' }):wait()
if result.code == 0 and result.stdout and result.stdout ~= '' then
vim.health.ok('ghostty +show-config --docs produces output')
else
vim.health.warn(
'ghostty +show-config --docs failed (config key documentation will be unavailable)'
)
end
local source = require('blink-cmp-ghostty')
local path = source.bash_completion_path()
if not path then
vim.health.warn('could not resolve bash completion path (enum completions will be unavailable)')
return
end
local fd = io.open(path, 'r')
if fd then
fd:close()
vim.health.ok('bash completion file found: ' .. path)
else
vim.health.warn(
'bash completion file not found at ' .. path .. ' (enum completions will be unavailable)'
)
end
end
return M

View file

@ -1,16 +0,0 @@
---@class blink.cmp.Source
---@class blink.cmp.CompletionItem
---@field label string
---@field kind? integer
---@field documentation? {kind: string, value: string}
---@field filterText? string
---@class blink.cmp.Context
---@field line string
---@field cursor integer[]
---@class blink.cmp.CompletionResponse
---@field is_incomplete_forward? boolean
---@field is_incomplete_backward? boolean
---@field items blink.cmp.CompletionItem[]

View file

@ -29,22 +29,14 @@ local function mock_system()
on_exit(result)
return {}
end
return {
wait = function()
return result
end,
}
return { wait = function() return result end }
end
local result = { stdout = '', code = 1 }
if on_exit then
on_exit(result)
return {}
end
return {
wait = function()
return result
end,
}
return { wait = function() return result end }
end
vim.schedule = function(fn)
fn()
@ -55,15 +47,10 @@ 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_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
local original_open = io.open
vim.fn.exepath = function(name)
if name == 'ghostty' then
@ -77,41 +64,22 @@ local function mock_enums()
end
return original_realpath(path)
end
vim.uv.fs_open = function(path, flags, mode, callback)
io.open = function(path, mode)
if path:match('ghostty%.bash$') then
callback(nil, MOCK_FD)
return
return {
read = function()
return BASH_COMPLETION
end,
close = function() end,
}
end
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, ...)
return original_open(path, mode)
end
return function()
vim.fn.exepath = original_exepath
vim.uv.fs_realpath = original_realpath
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
io.open = original_open
end
end
@ -137,43 +105,6 @@ describe('blink-cmp-ghostty', function()
helpers.delete_buffer(bufnr)
end)
it('returns true for config filetype in ghostty config dir', function()
local source = require('blink-cmp-ghostty')
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_set_option_value('filetype', 'config', { buf = bufnr })
local config_path = vim.fn.expand('$HOME/.config/ghostty/config')
vim.api.nvim_buf_set_name(bufnr, config_path)
local original_realpath = vim.uv.fs_realpath
vim.uv.fs_realpath = function(p)
if p == config_path then
return config_path
end
return original_realpath(p)
end
assert.is_true(source.enabled())
vim.uv.fs_realpath = original_realpath
helpers.delete_buffer(bufnr)
end)
it('returns false for config filetype outside ghostty dir', function()
local source = require('blink-cmp-ghostty')
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_set_option_value('filetype', 'config', { buf = bufnr })
vim.api.nvim_buf_set_name(bufnr, '/tmp/some-other/config')
local original_realpath = vim.uv.fs_realpath
vim.uv.fs_realpath = function(p)
if p == '/tmp/some-other/config' then
return '/tmp/some-other/config'
end
return original_realpath(p)
end
assert.is_false(source.enabled())
vim.uv.fs_realpath = original_realpath
helpers.delete_buffer(bufnr)
end)
it('returns false for other filetypes', function()
local bufnr = helpers.create_buffer({}, 'lua')
local source = require('blink-cmp-ghostty')