diff --git a/lua/blink-cmp-ghostty.lua b/lua/blink-cmp-ghostty.lua index 9eb94ce..2efd951 100644 --- a/lua/blink-cmp-ghostty.lua +++ b/lua/blink-cmp-ghostty.lua @@ -5,49 +5,24 @@ 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 }) 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 ---@return blink.cmp.CompletionItem[] -local function parse_keys(stdout) +local function parse_keys() local Kind = require('blink.cmp.types').CompletionItemKind + local result = vim.system({ 'ghostty', '+show-config', '--docs' }):wait() local items = {} local doc_lines = {} - for line in (stdout .. '\n'):gmatch('(.-)\n') do + for line in ((result.stdout or '') .. '\n'):gmatch('(.-)\n') do if line:match('^#') then local stripped = line:gsub('^# ?', '') doc_lines[#doc_lines + 1] = stripped @@ -67,25 +42,28 @@ local function parse_keys(stdout) return items end ----@return string? -function M.bash_completion_path() +---@return table +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 -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,10 +79,13 @@ 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 +---@return fun() +function M:get_completions(ctx, callback) + if not keys_cache then + keys_cache = parse_keys() + enums_cache = parse_enums() end + local line = ctx.line local col = ctx.cursor[2] local eq_pos = line:find('=') @@ -127,81 +108,16 @@ local function respond(ctx, callback) is_incomplete_backward = false, items = items, }) - return + return function() end end callback({ items = {} }) else callback({ is_incomplete_forward = false, is_incomplete_backward = false, - items = keys_cache, + items = vim.deepcopy(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 index 5db565d..8a6c77e 100644 --- a/lua/blink-cmp-ghostty/health.lua +++ b/lua/blink-cmp-ghostty/health.lua @@ -22,25 +22,26 @@ function M.check() 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)' - ) + 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)') + local real = vim.uv.fs_realpath(bin) + if not real then + vim.health.warn('could not resolve ghostty symlink (enum completions will be unavailable)') return end + local prefix = real:match('(.*)/bin/ghostty$') + if not prefix then + vim.health.warn('ghostty binary is not in a standard bin/ directory (enum completions will be unavailable)') + return + end + local path = prefix .. '/share/bash-completion/completions/ghostty.bash' 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)' - ) + vim.health.warn('bash completion file not found at ' .. path .. ' (enum completions will be unavailable)') end end diff --git a/lua/blink-cmp-ghostty/types.lua b/lua/blink-cmp-ghostty/types.lua deleted file mode 100644 index d311414..0000000 --- a/lua/blink-cmp-ghostty/types.lua +++ /dev/null @@ -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[] diff --git a/spec/ghostty_spec.lua b/spec/ghostty_spec.lua index 5ef7a2a..5c7e804 100644 --- a/spec/ghostty_spec.lua +++ b/spec/ghostty_spec.lua @@ -19,51 +19,31 @@ local BASH_COMPLETION = table.concat({ }, '\n') local function mock_system() - local original_system = vim.system - local original_schedule = vim.schedule + local original = vim.system ---@diagnostic disable-next-line: duplicate-set-field - vim.system = function(cmd, _, on_exit) + vim.system = function(cmd) 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 result + return { stdout = CONFIG_DOCS, code = 0 } end, } end - local result = { stdout = '', code = 1 } - if on_exit then - on_exit(result) - return {} - end return { wait = function() - return result + return { stdout = '', code = 1 } end, } end - vim.schedule = function(fn) - fn() - end return function() - vim.system = original_system - vim.schedule = original_schedule + vim.system = original 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 +57,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 +98,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')