local M = {} local context = require('chatgpt_nvim.context') local handler = require('chatgpt_nvim.handler') local config = require('chatgpt_nvim.config') local ui = require('chatgpt_nvim.ui') local prompts = require('chatgpt_nvim.prompts') local tools_manager = require("chatgpt_nvim.tools.manager") local tools_module = require("chatgpt_nvim.tools") local ok_yaml, lyaml = pcall(require, "lyaml") ------------------------------------------------------------------------------ -- UTILITIES ------------------------------------------------------------------------------ local function copy_to_clipboard(text) vim.fn.setreg('+', text) end local function parse_response(raw, conf) 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.") ui.debug_log("RAW response that failed parsing:\n" .. raw) return nil end ui.debug_log("Successfully parsed YAML response.") return data 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 close_existing_buffer_by_name(pattern) for _, b in ipairs(vim.api.nvim_list_bufs()) do local name = vim.api.nvim_buf_get_name(b) if name:match(pattern) then vim.api.nvim_buf_delete(b, { force = true }) end end end ------------------------------------------------------------------------------ -- PROMPT CONSTRUCTION ------------------------------------------------------------------------------ local function build_tools_available_block() local lines = {} lines[#lines+1] = "" for _, t in ipairs(tools_module.available_tools) do lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s", t.name, t.explanation, t.usage ) end lines[#lines+1] = "" return table.concat(lines, "\n") end local function build_prompt(user_input, dirs, conf) local root = vim.fn.getcwd() local initial_files = conf.initial_files or {} local final_sections = {} -- 1) if conf.initial_prompt and conf.initial_prompt ~= "" then table.insert(final_sections, "\n" .. conf.initial_prompt .. "\n\n") end -- 2) table.insert(final_sections, build_tools_available_block()) -- 3) local task_lines = {} task_lines[#task_lines+1] = "" task_lines[#task_lines+1] = user_input for _, file_path in ipairs(initial_files) do task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path) end task_lines[#task_lines+1] = "\n" table.insert(final_sections, table.concat(task_lines, "\n")) -- 4) local file_content_blocks = {} for _, file_path in ipairs(initial_files) do local full_path = root .. "/" .. file_path if is_subpath(root, full_path) then local fdata = read_file(full_path) if fdata then file_content_blocks[#file_content_blocks+1] = string.format( "\n%s\n", file_path, fdata ) end end end if #file_content_blocks > 0 then table.insert(final_sections, table.concat(file_content_blocks, "\n\n")) end -- 5) local env_lines = {} env_lines[#env_lines+1] = "" env_lines[#env_lines+1] = "# VSCode Visible Files" for _, f in ipairs(initial_files) do env_lines[#env_lines+1] = f end env_lines[#env_lines+1] = "" env_lines[#env_lines+1] = "# VSCode Open Tabs" env_lines[#env_lines+1] = "..." env_lines[#env_lines+1] = "" env_lines[#env_lines+1] = "# Current Time" env_lines[#env_lines+1] = os.date("%x, %X (%Z)") env_lines[#env_lines+1] = "" env_lines[#env_lines+1] = "# Current Working Directory (" .. root .. ") Files" env_lines[#env_lines+1] = context.get_project_structure(dirs, conf) or "" env_lines[#env_lines+1] = "" env_lines[#env_lines+1] = "# Current Mode" env_lines[#env_lines+1] = "ACT MODE" env_lines[#env_lines+1] = "" table.insert(final_sections, table.concat(env_lines, "\n")) return table.concat(final_sections, "\n\n") end local function handle_step_by_step_if_needed(prompt, conf) local length = #prompt local limit = conf.prompt_char_limit or 8000 if (not conf.enable_step_by_step) or (length <= limit) then return { prompt } end return { prompts["step-prompt"] } end ------------------------------------------------------------------------------ -- :ChatGPT ------------------------------------------------------------------------------ local function run_chatgpt_command() local conf = config.load() ui.setup_ui(conf) ui.debug_log("Running :ChatGPT command.") local dirs = conf.directories or {"."} if conf.interactive_file_selection then dirs = ui.pick_directories(dirs, conf) if #dirs == 0 then dirs = conf.directories end end close_existing_buffer_by_name("ChatGPT_Prompt.md$") local bufnr = vim.api.nvim_create_buf(false, false) vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Prompt.md") vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown") vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") vim.api.nvim_buf_set_option(bufnr, "buftype", "") vim.api.nvim_buf_set_option(bufnr, "modifiable", true) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "# Enter your main user prompt (task) below.", "", "Save & close with :wq, :x, or :bd to finalize your prompt." }) vim.api.nvim_create_autocmd("BufWriteCmd", { buffer = bufnr, callback = function() local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local user_input = table.concat(lines, "\n") if user_input == "" or user_input:find("^# Enter your main user prompt %(task%) below.") then vim.api.nvim_out_write("No valid input provided.\n") vim.api.nvim_buf_set_option(bufnr, "modified", false) return end local final_prompt = build_prompt(user_input, dirs, conf) local chunks = handle_step_by_step_if_needed(final_prompt, conf) close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") local bufnr_ref = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") local prompt_lines = vim.split(chunks[1], "\n") vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) vim.cmd("vsplit") vim.cmd("buffer " .. bufnr_ref) copy_to_clipboard(chunks[1]) if #chunks == 1 then vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n") else vim.api.nvim_out_write("Step-by-step prompt copied to clipboard!\n") end vim.api.nvim_buf_set_option(bufnr, "modified", false) end }) vim.cmd("buffer " .. bufnr) end ------------------------------------------------------------------------------ -- :ChatGPTPaste ------------------------------------------------------------------------------ local function run_chatgpt_paste_command() local conf = config.load() ui.setup_ui(conf) ui.debug_log("Running :ChatGPTPaste command.") print("Reading ChatGPT YAML response from clipboard...") local raw = handler.get_clipboard_content(conf) 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, conf) if not data then return end -- Check if we have tools if data.tools then -- Must also verify project name if not data.project_name or data.project_name ~= conf.project_name then vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.") return end local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file) copy_to_clipboard(output_messages) print("Tool call results have been processed and copied to clipboard.") return end -- If we see project_name & files => older YAML style. We handle it but it's discouraged now. if data.project_name and data.files then if data.project_name ~= conf.project_name then vim.api.nvim_err_writeln("Project name mismatch. Aborting.") return 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 conf.preview_changes then require('chatgpt_nvim.init').preview_changes(data.files, conf) print("Close the preview window to apply changes, or use :q to cancel.") local closed = vim.wait(60000, function() local bufs = vim.api.nvim_list_bufs() for _, b in ipairs(bufs) do local name = vim.api.nvim_buf_get_name(b) if name:match("ChatGPT_Changes_Preview$") then return false end end return true end) if not closed then vim.api.nvim_err_writeln("Preview not closed in time. Aborting.") return end end local final_files = data.files if conf.partial_acceptance then final_files = require('chatgpt_nvim.init').partial_accept(data.files, conf) if #final_files == 0 then vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.") return end end local root = vim.fn.getcwd() for _, fileinfo in ipairs(final_files) do if not fileinfo.path then vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.") else if not is_subpath(root, fileinfo.path) then vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path) else if fileinfo.delete == true then ui.debug_log("Deleting file: " .. fileinfo.path) handler.delete_file(fileinfo.path, conf) print("Deleted: " .. fileinfo.path) elseif fileinfo.content then ui.debug_log("Writing file: " .. fileinfo.path) handler.write_file(fileinfo.path, fileinfo.content, conf) print("Wrote: " .. fileinfo.path) else vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.") end end end end else -- Not final => user is requesting more files local requested_paths = {} local root = vim.fn.getcwd() for _, fileinfo in ipairs(data.files) do if fileinfo.path then table.insert(requested_paths, fileinfo.path) end end local file_sections = {} for _, f in ipairs(requested_paths) do local path = root .. "/" .. f local content = read_file(path) if content then table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content)) else table.insert(file_sections, ("\nFile: `%s`\n```\n(Could not read file)\n```\n"):format(f)) 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, or use the 'tools:' approach. If you have all info, provide final changes or continue your instructions." } local prompt = table.concat(sections, "\n") local length = #prompt ui.debug_log("Returning requested files. Character count: " .. length) if length > (conf.prompt_char_limit or 8000) and conf.enable_step_by_step then local large_step = prompts["step-prompt"] copy_to_clipboard(large_step) print("Step-by-step guidance copied to clipboard!") return elseif length > (conf.prompt_char_limit or 8000) then vim.api.nvim_err_writeln("Requested files exceed prompt character limit. No step-by-step support enabled.") return end copy_to_clipboard(prompt) print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.") end else vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.") end end ------------------------------------------------------------------------------ -- :ChatGPTCurrentBuffer ------------------------------------------------------------------------------ local function run_chatgpt_current_buffer_command() local conf = config.load() ui.setup_ui(conf) ui.debug_log("Running :ChatGPTCurrentBuffer command.") local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local user_input = table.concat(lines, "\n") local dirs = conf.directories or {"."} if conf.interactive_file_selection then dirs = ui.pick_directories(dirs, conf) if #dirs == 0 then dirs = conf.directories end end local final_prompt = build_prompt(user_input, dirs, conf) local chunks = handle_step_by_step_if_needed(final_prompt, conf) close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") local bufnr_ref = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") local prompt_lines = vim.split(chunks[1], "\n") vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) vim.cmd("vsplit") vim.cmd("buffer " .. bufnr_ref) copy_to_clipboard(chunks[1]) if #chunks == 1 then vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n") else vim.api.nvim_out_write("Step-by-step prompt (from current buffer) copied to clipboard!\n") end end ------------------------------------------------------------------------------ -- PUBLIC API ------------------------------------------------------------------------------ M.run_chatgpt_command = run_chatgpt_command M.run_chatgpt_paste_command = run_chatgpt_paste_command M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command return M