Files
chatgpt.vim/lua/chatgpt_nvim/context.lua

207 lines
6.0 KiB
Lua

local M = {}
local uv = vim.loop
-- Converts a .gitignore-style pattern to a Lua pattern
local function gitignore_to_lua_pattern(gip)
-- Trim spaces
gip = gip:gsub("^%s*(.-)%s*$", "%1")
-- Escape magic chars in Lua patterns
local magic_chars = "().^$+%-*?[]"
gip = gip:gsub("["..magic_chars.."]", "%%%1")
-- Convert ** to .- (match any path, including dirs)
gip = gip:gsub("%%%%%*%%%%%*", ".*")
-- Convert * to [^/]* (match anything except /)
gip = gip:gsub("%%%%%*", "[^/]*")
-- If pattern starts with /, ensure it matches start of string
if gip:sub(1,1) == "/" then
gip = "^" .. gip:sub(2)
else
-- Otherwise allow matching anywhere
gip = gip
end
-- If pattern ends with /, ensure it matches a directory
if gip:sub(-1) == "/" then
gip = gip .. ".*"
end
return gip
end
local function load_gitignore_patterns(root, conf)
local gitignore_path = root .. "/.gitignore"
local fd = uv.fs_open(gitignore_path, "r", 438)
if not fd then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] No .gitignore found.\n")
end
return {}
end
local stat = uv.fs_fstat(fd)
local data = uv.fs_read(fd, stat.size, 0)
uv.fs_close(fd)
if not data then return {} end
local patterns = {}
for line in data:gmatch("[^\r\n]+") do
line = line:match("^%s*(.-)%s*$")
if line ~= "" and not line:match("^#") then
table.insert(patterns, gitignore_to_lua_pattern(line))
end
end
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Loaded " .. #patterns .. " gitignore patterns.\n")
end
return patterns
end
local function should_ignore_file(file, ignore_patterns, conf)
for _, pattern in ipairs(ignore_patterns) do
if file:match(pattern) then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Ignoring file/dir: " .. file .. " (matched pattern: " .. pattern .. ")\n")
end
return true
end
end
return false
end
local function is_text_file(file, conf)
local fd = uv.fs_open(file, "r", 438)
if not fd then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open file: " .. file .. " for reading.\n")
end
return false
end
local chunk = uv.fs_read(fd, 1024, 0) or ""
uv.fs_close(fd)
if chunk:find("\0") then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] File appears binary: " .. file .. "\n")
end
return false
end
return true
end
local function scandir(dir, ignore_patterns, files, conf)
local fd = uv.fs_opendir(dir, nil, 50)
if not fd then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open dir: " .. dir .. "\n")
end
return files
end
while true do
local ents = uv.fs_readdir(fd)
if not ents then break end
for _, ent in ipairs(ents) do
local fullpath = dir .. "/" .. ent.name
if not should_ignore_file(fullpath, ignore_patterns, conf) then
if ent.type == "file" and is_text_file(fullpath, conf) then
table.insert(files, fullpath)
elseif ent.type == "directory" and ent.name ~= ".git" then
scandir(fullpath, ignore_patterns, files, conf)
elseif ent.type == "link" then
local link_target = uv.fs_readlink(fullpath)
if link_target then
local st = uv.fs_stat(link_target)
if st and st.type == "directory" then
table.insert(files, fullpath .. " (symlink to directory " .. link_target .. ")")
else
table.insert(files, fullpath .. " (symlink to file " .. link_target .. ")")
end
else
table.insert(files, fullpath .. " (symlink)")
end
end
end
end
end
uv.fs_closedir(fd)
return files
end
function M.get_project_files(directories, conf)
local root = vim.fn.getcwd()
local ignore_patterns = load_gitignore_patterns(root, conf)
local all_files = {}
for _, dir in ipairs(directories) do
local abs_dir = dir
if not abs_dir:match("^/") then
abs_dir = root .. "/" .. dir
end
scandir(abs_dir, ignore_patterns, all_files, conf)
end
local rel_files = {}
for _, f in ipairs(all_files) do
local rel = vim.fn.fnamemodify(f, ":.")
table.insert(rel_files, rel)
end
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Found " .. #rel_files .. " project files.\n")
end
return rel_files
end
function M.get_project_structure(directories, conf)
local files = M.get_project_files(directories, conf)
local structure = "Files:\n" .. table.concat(files, "\n")
return structure
end
function M.get_file_contents(files, conf)
local root = vim.fn.getcwd()
local sections = {}
for _, f in ipairs(files) do
local path = root .. "/" .. f
local fd = uv.fs_open(path, "r", 438)
if fd then
local stat = uv.fs_fstat(fd)
if stat then
local data = uv.fs_read(fd, stat.size, 0)
uv.fs_close(fd)
if data then
table.insert(sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n")
end
else
uv.fs_close(fd)
end
else
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open file for content: " .. f .. "\n")
end
end
end
return table.concat(sections, "\n")
end
-- NEW FUNCTION: Build the project prompt by optionally including file contents.
function M.get_project_prompt(directories, conf)
local structure = M.get_project_structure(directories, conf)
if conf.include_file_contents then
local files = M.get_project_files(directories, conf)
local contents = M.get_file_contents(files, conf)
local total_chars = #contents
if total_chars > conf.prompt_char_limit then
vim.notify("Total file contents (" .. total_chars .. " characters) exceed the prompt limit (" .. conf.prompt_char_limit .. "). Please disable 'include_file_contents' in your config.", vim.log.levels.ERROR)
return structure
else
return structure .. "\n" .. contents
end
else
return structure
end
end
return M