---@mod codetyper.completion Insert mode completion for file references --- --- Provides completion for @filename inside /@ @/ tags. local M = {} local parser = require("codetyper.parser") local utils = require("codetyper.utils") --- Get list of files for completion ---@param prefix string Prefix to filter files ---@return table[] List of completion items local function get_file_completions(prefix) local cwd = vim.fn.getcwd() local current_file = vim.fn.expand("%:p") local current_dir = vim.fn.fnamemodify(current_file, ":h") local files = {} -- Use vim.fn.glob to find files matching the prefix local pattern = prefix .. "*" -- Determine base directory - use current file's directory if outside cwd local base_dir = cwd if current_dir ~= "" and not current_dir:find(cwd, 1, true) then -- File is outside project, use its directory as base base_dir = current_dir end -- Search in base directory local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true) -- Search with ** for all subdirectories local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true) for _, m in ipairs(deep_matches) do table.insert(matches, m) end -- Also search in cwd if different from base_dir if base_dir ~= cwd then local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true) for _, m in ipairs(cwd_matches) do table.insert(matches, m) end local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true) for _, m in ipairs(cwd_deep) do table.insert(matches, m) end end -- Also search specific directories if prefix doesn't have path if not prefix:find("/") then local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" } for _, dir in ipairs(search_dirs) do local dir_path = base_dir .. "/" .. dir if vim.fn.isdirectory(dir_path) == 1 then local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true) for _, m in ipairs(dir_matches) do table.insert(matches, m) end end end end -- Convert to relative paths and deduplicate local seen = {} for _, match in ipairs(matches) do -- Convert to relative path based on which base it came from local rel_path if match:find(base_dir, 1, true) == 1 then rel_path = match:sub(#base_dir + 2) elseif match:find(cwd, 1, true) == 1 then rel_path = match:sub(#cwd + 2) else rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative end -- Skip directories, coder files, and hidden/generated files if vim.fn.isdirectory(match) == 0 and not utils.is_coder_file(match) and not rel_path:match("^%.") and not rel_path:match("node_modules") and not rel_path:match("%.git/") and not rel_path:match("dist/") and not rel_path:match("build/") and not seen[rel_path] then seen[rel_path] = true table.insert(files, { word = rel_path, abbr = rel_path, kind = "File", menu = "[ref]", }) end end -- Sort by length (shorter paths first) table.sort(files, function(a, b) return #a.word < #b.word end) -- Limit results local result = {} for i = 1, math.min(#files, 15) do result[i] = files[i] end return result end --- Show file completion popup function M.show_file_completion() -- Check if we're in an open prompt tag local is_inside = parser.is_cursor_in_open_tag() if not is_inside then return false end -- Get the prefix being typed local prefix = parser.get_file_ref_prefix() if prefix == nil then return false end -- Get completions local items = get_file_completions(prefix) if #items == 0 then -- Try with empty prefix to show all files items = get_file_completions("") end if #items > 0 then -- Calculate start column (position right after @) local cursor = vim.api.nvim_win_get_cursor(0) local col = cursor[2] - #prefix + 1 -- 1-indexed for complete() -- Show completion popup vim.fn.complete(col, items) return true end return false end --- Setup completion for file references (works on ALL files) function M.setup() local group = vim.api.nvim_create_augroup("CoderCompletion", { clear = true }) -- Trigger completion on @ in insert mode (works on ALL files) vim.api.nvim_create_autocmd("InsertCharPre", { group = group, pattern = "*", callback = function() -- Skip special buffers if vim.bo.buftype ~= "" then return end if vim.v.char == "@" then -- Schedule completion popup after the @ is inserted vim.schedule(function() -- Check we're in an open tag local is_inside = parser.is_cursor_in_open_tag() if not is_inside then return end -- Check we're not typing @/ (closing tag) local cursor = vim.api.nvim_win_get_cursor(0) local line = vim.api.nvim_get_current_line() local next_char = line:sub(cursor[2] + 2, cursor[2] + 2) if next_char == "/" then return end -- Show file completion M.show_file_completion() end) end end, desc = "Trigger file completion on @ inside prompt tags", }) -- Also allow manual trigger with style keybinding in insert mode vim.keymap.set("i", "@", function() M.show_file_completion() end, { silent = true, desc = "Coder: Complete file reference" }) end return M