diff --git a/lua/blink-cmp-ghostty.lua b/lua/blink-cmp-ghostty.lua index 7044953..9eb94ce 100644 --- a/lua/blink-cmp-ghostty.lua +++ b/lua/blink-cmp-ghostty.lua @@ -5,6 +5,9 @@ local M = {} local keys_cache = nil ---@type table? local enums_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 }) @@ -37,14 +40,14 @@ function M.enabled() return false end +---@param stdout string ---@return blink.cmp.CompletionItem[] -local function parse_keys() +local function parse_keys(stdout) local Kind = require('blink.cmp.types').CompletionItemKind - local result = vim.system({ 'ghostty', '+show-config', '--docs' }):wait() local items = {} local doc_lines = {} - for line in ((result.stdout or '') .. '\n'):gmatch('(.-)\n') do + for line in (stdout .. '\n'):gmatch('(.-)\n') do if line:match('^#') then local stripped = line:gsub('^# ?', '') doc_lines[#doc_lines + 1] = stripped @@ -64,28 +67,25 @@ local function parse_keys() return items end ----@return table -local function parse_enums() +---@return string? +function M.bash_completion_path() local bin = vim.fn.exepath('ghostty') if bin == '' then - return {} + return nil end local real = vim.uv.fs_realpath(bin) if not real then - return {} + return nil end local prefix = real:match('(.*)/bin/ghostty$') if not prefix then - return {} + return nil 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 prefix .. '/share/bash-completion/completions/ghostty.bash' +end +---@return table +local function parse_enums(content) local enums = {} for key, values in content:gmatch('%-%-([a-z][a-z0-9-]*)%) [^\n]* compgen %-W "([^"]+)"') do local vals = {} @@ -101,13 +101,10 @@ end ---@param ctx blink.cmp.Context ---@param callback fun(response: blink.cmp.CompletionResponse) ----@return fun() -function M:get_completions(ctx, callback) - if not keys_cache then - keys_cache = parse_keys() - enums_cache = parse_enums() +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('=') @@ -130,16 +127,81 @@ function M:get_completions(ctx, callback) is_incomplete_backward = false, items = items, }) - return function() end + return end callback({ items = {} }) else callback({ is_incomplete_forward = false, is_incomplete_backward = false, - items = vim.deepcopy(keys_cache), + items = keys_cache, }) end +end + +---@param ctx blink.cmp.Context +---@param callback fun(response: blink.cmp.CompletionResponse) +---@return fun() +function M:get_completions(ctx, callback) + if keys_cache then + respond(ctx, callback) + return function() end + end + + 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.schedule(function() + 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(_, data) + vim.uv.fs_close(fd) + enums_content = data or '' + on_all_done() + end) + end) + end) + end + end return function() end end diff --git a/lua/blink-cmp-ghostty/health.lua b/lua/blink-cmp-ghostty/health.lua new file mode 100644 index 0000000..5db565d --- /dev/null +++ b/lua/blink-cmp-ghostty/health.lua @@ -0,0 +1,47 @@ +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 diff --git a/lua/blink-cmp-ghostty/types.lua b/lua/blink-cmp-ghostty/types.lua new file mode 100644 index 0000000..d311414 --- /dev/null +++ b/lua/blink-cmp-ghostty/types.lua @@ -0,0 +1,16 @@ +---@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[] diff --git a/spec/ghostty_spec.lua b/spec/ghostty_spec.lua index cfac929..5ef7a2a 100644 --- a/spec/ghostty_spec.lua +++ b/spec/ghostty_spec.lua @@ -19,31 +19,51 @@ local BASH_COMPLETION = 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) if cmd[1] == 'ghostty' then + local result = { stdout = CONFIG_DOCS, code = 0 } + if on_exit then + on_exit(result) + return {} + end return { wait = function() - return { stdout = CONFIG_DOCS, code = 0 } + return result end, } end + local result = { stdout = '', code = 1 } + 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 +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 @@ -57,22 +77,41 @@ local function mock_enums() end return original_realpath(path) end - 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 - 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