295 lines
10 KiB
Lua
295 lines
10 KiB
Lua
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
|
|
|
|
---------------------------------------------------------------------------
|
|
-- Updated run_chatgpt_command using BufWriteCmd to avoid creating a file --
|
|
---------------------------------------------------------------------------
|
|
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 normal, listed buffer so :w / :wq will work
|
|
local bufnr = vim.api.nvim_create_buf(false, false)
|
|
-- Assign a filename so Vim treats it like a normal file, but we intercept writes
|
|
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)
|
|
|
|
-- Set some initial placeholder lines
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
|
"# Enter your prompt below.",
|
|
"",
|
|
"Save & close with :wq to finalize your prompt."
|
|
})
|
|
|
|
-- Intercept the write so that no file is actually created on disk
|
|
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|
buffer = bufnr,
|
|
callback = function()
|
|
-- Gather lines
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local user_input = table.concat(lines, "\n")
|
|
|
|
-- Basic check to ensure user actually wrote something
|
|
if user_input == "" or user_input:find("^# Enter your prompt below.") then
|
|
vim.api.nvim_out_write("No valid input provided.\n")
|
|
-- Mark buffer as unmodified, so :wq can still exit
|
|
vim.api.nvim_buf_set_option(bufnr, "modified", false)
|
|
return
|
|
end
|
|
|
|
-- Build the prompt using the 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
|
|
vim.api.nvim_out_write("Too many files in project structure. The request exceeds the O1 model limit of " .. token_limit .. " tokens.\n")
|
|
else
|
|
copy_to_clipboard(prompt)
|
|
vim.api.nvim_out_write("Prompt (requesting needed files) copied to clipboard! Paste it into the ChatGPT O1 model.\n")
|
|
end
|
|
|
|
-- Mark as unmodified so :wq won't complain
|
|
vim.api.nvim_buf_set_option(bufnr, "modified", false)
|
|
end,
|
|
})
|
|
|
|
-- 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 |