local M = {} local context = require('chatgpt_nvim.context') local handler = require('chatgpt_nvim.handler') local config = require('chatgpt_nvim.config') local ok_yaml, lyaml = pcall(require, "lyaml") local function copy_to_clipboard(text) vim.fn.setreg('+', text) end local function parse_response(raw) local conf = config.load() if not ok_yaml then vim.api.nvim_err_writeln("lyaml not available. Install with `luarocks install lyaml`.") return nil end local ok, data = pcall(lyaml.load, raw) if not ok or not data then vim.api.nvim_err_writeln("Failed to parse YAML response.") if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] RAW response that failed parsing:\n" .. raw .. "\n") end return nil end if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Successfully parsed YAML response.\n") end return data end local function estimate_tokens(text) local approx_chars_per_token = 4 local length = #text return math.floor(length / approx_chars_per_token) end local function is_subpath(root, path) local root_abs = vim.fn.fnamemodify(root, ":p") local target_abs = vim.fn.fnamemodify(path, ":p") return target_abs:sub(1, #root_abs) == root_abs end local function read_file(path) local fd = vim.loop.fs_open(path, "r", 438) if not fd then return nil end local stat = vim.loop.fs_fstat(fd) if not stat then vim.loop.fs_close(fd) return nil end local data = vim.loop.fs_read(fd, stat.size, 0) vim.loop.fs_close(fd) return data end local function is_directory(path) local stat = vim.loop.fs_stat(path) return stat and stat.type == "directory" end -- Replaces the inline input() call with opening a new buffer for prompt input function M.run_chatgpt_command() local conf = config.load() if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Running :ChatGPT command.\n") end -- Create a new scratch buffer for user to type the message local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Prompt") vim.api.nvim_buf_set_option(bufnr, 'bufhidden', 'wipe') vim.api.nvim_buf_set_option(bufnr, 'filetype', 'markdown') vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "# Enter your prompt below.", "", "Save & close with :wq to finalize your prompt." }) local function on_write_post() -- Read buffer lines and join them into a single string local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) -- Skip the first lines that are placeholders, if desired local user_input = table.concat(lines, "\n") if user_input == "" or user_input:find("^# Enter your prompt below.") then print("No valid input provided.") return end -- Continue the same logic as originally, just using our new user_input local dirs = conf.directories or {"."} local project_structure = context.get_project_structure(dirs) local initial_files = conf.initial_files or {} local included_sections = {} if #initial_files > 0 then table.insert(included_sections, "\n\nIncluded files and directories (pre-selected):\n") local root = vim.fn.getcwd() for _, item in ipairs(initial_files) do local full_path = root .. "/" .. item if is_directory(full_path) then local dir_files = context.get_project_files({item}) for _, f in ipairs(dir_files) do local path = root .. "/" .. f local data = read_file(path) if data then table.insert(included_sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n") end end else local data = read_file(full_path) if data then table.insert(included_sections, "\nFile: `" .. item .. "`\n```\n" .. data .. "\n```\n") end end end end local initial_sections = { "### Basic Prompt Instructions:\n", conf.initial_prompt .. "\n\n\n", "### User Instructions:\n", user_input .. "\n\n\n", "### Context/Data:\n", "Project name: " .. (conf.project_name or "") .. "\n", "Project Structure:\n", project_structure, table.concat(included_sections, "\n") } local prompt = table.concat(initial_sections, "\n") local token_limit = conf.token_limit or 8000 local token_count = estimate_tokens(prompt) if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Prompt token count: " .. token_count .. "\n") end if token_count > token_limit then print("Too many files in project structure. The request exceeds the O1 model limit of " .. token_limit .. " tokens.") return end copy_to_clipboard(prompt) print("Prompt (requesting needed files) copied to clipboard! Paste it into the ChatGPT O1 model.") end -- Create an autocmd that triggers once when user saves the buffer (BufWritePost) vim.api.nvim_create_autocmd("BufWritePost", { buffer = bufnr, once = true, callback = on_write_post }) -- Switch to the newly created buffer vim.cmd("buffer " .. bufnr) end function M.run_chatgpt_paste_command() local conf = config.load() if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Running :ChatGPTPaste command.\n") end print("Reading ChatGPT YAML response from clipboard...") local raw = handler.get_clipboard_content() if raw == "" then vim.api.nvim_err_writeln("Clipboard is empty. Please copy the YAML response from ChatGPT first.") return end local data = parse_response(raw) if not data then return end if data.project_name and data.files then if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Received project_name and files from response.\n") end local is_final = false for _, fileinfo in ipairs(data.files) do if fileinfo.content or fileinfo.delete == true then is_final = true break end end if is_final then if data.project_name ~= conf.project_name then vim.api.nvim_err_writeln("Project name mismatch. The provided changes are for project '" .. (data.project_name or "unknown") .. "' but current project is '" .. (conf.project_name or "unconfigured") .. "'. Aborting changes.") return end local root = vim.fn.getcwd() for _, fileinfo in ipairs(data.files) do if not fileinfo.path then vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.") goto continue end if not is_subpath(root, fileinfo.path) then vim.api.nvim_err_writeln("Invalid file path outside project root: " .. fileinfo.path) goto continue end if fileinfo.delete == true then if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Deleting file: " .. fileinfo.path .. "\n") end handler.delete_file(fileinfo.path) print("Deleted file: " .. fileinfo.path) elseif fileinfo.content then if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Writing file: " .. fileinfo.path .. "\n") end handler.write_file(fileinfo.path, fileinfo.content) print("Wrote file: " .. fileinfo.path) else vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete' set to true for final changes.") end ::continue:: end return else local dirs = conf.directories or {"."} local requested_paths = {} for _, fileinfo in ipairs(data.files) do if fileinfo.path then table.insert(requested_paths, fileinfo.path) end end local file_sections = {} local root = vim.fn.getcwd() for _, f in ipairs(requested_paths) do local path = root .. "/" .. f local content = read_file(path) if content then table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n" .. content .. "\n```\n") else table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n(Could not read file)\n```\n") end end local sections = { conf.initial_prompt, "\n\nProject name: " .. (conf.project_name or ""), "\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n", table.concat(file_sections, "\n"), "\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (with `content` or `delete`) to apply changes.\n" } local prompt = table.concat(sections, "\n") local token_limit = conf.token_limit or 8000 local token_count = estimate_tokens(prompt) if conf.debug then vim.api.nvim_out_write("[chatgpt_nvim:init] Returning requested files. Token count: " .. token_count .. "\n") end if token_count > token_limit then vim.api.nvim_err_writeln("Too many requested files. Exceeds token limit.") return end copy_to_clipboard(prompt) print("Prompt (with requested files) copied to clipboard! Paste it into the ChatGPT O1 model.") end else vim.api.nvim_err_writeln("Invalid response. Expected 'project_name' and 'files'.") end end return M