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) if conf.ignore_files then for _, pattern in ipairs(conf.ignore_files) do table.insert(ignore_patterns, gitignore_to_lua_pattern(pattern)) end end 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