From 32d6a2a392bb76c38e8b00927707e2267bb2a9c4 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 18:04:53 +0200 Subject: [PATCH 01/33] refactor: helper for chat header parsing --- lua/gp/helper.lua | 20 ++++++++++++++++++++ lua/gp/init.lua | 25 ++----------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/lua/gp/helper.lua b/lua/gp/helper.lua index 1864bde0..f3d6d780 100644 --- a/lua/gp/helper.lua +++ b/lua/gp/helper.lua @@ -293,4 +293,24 @@ _H.create_user_command = function(cmd_name, cmd_func, completion, desc) }) end +---@param lines string[] # array of lines +---@return table, table, number | nil # headers, indices, last header line +_H.parse_headers = function(lines) + local headers = {} + local indices = {} + + for i, line in ipairs(lines) do + if i > 1 and line:sub(1, 3) == "---" then + return headers, indices, i - 1 + end + + local key, value = line:match("^[-#%s]*(%w+):%s*(.*)%s*") + if key ~= nil then + headers[key] = value + indices[key] = i - 1 + end + end + + return headers, indices, nil +end return _H diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 128914cb..1afb255e 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -944,9 +944,6 @@ M.chat_respond = function(params) -- go to normal mode vim.cmd("stopinsert") - -- get all lines - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - -- check if file looks like a chat file local file_name = vim.api.nvim_buf_get_name(buf) local reason = M.not_chat(buf, file_name) @@ -955,26 +952,8 @@ M.chat_respond = function(params) return end - -- headers are fields before first --- - local headers = {} - local header_end = nil - local line_idx = 0 - ---parse headers - for _, line in ipairs(lines) do - -- first line starts with --- - if line:sub(1, 3) == "---" then - header_end = line_idx - break - end - -- parse header fields - local key, value = line:match("^[-#] (%w+): (.*)") - if key ~= nil then - headers[key] = value - end - - line_idx = line_idx + 1 - end - + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local headers, indices, header_end = M.helpers.parse_headers(lines) if header_end == nil then M.logger.error("Error while parsing headers: --- not found. Check your chat template.") return From fef83fef2b2f6d152223020f151b99ac2c876877 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 18:06:21 +0200 Subject: [PATCH 02/33] feat: improve topic header detection --- lua/gp/init.lua | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 1afb255e..8f4660ee 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -1084,8 +1084,7 @@ M.chat_respond = function(params) M.helpers.undojoin(buf) vim.api.nvim_buf_set_lines(buf, -1, -1, false, { "" }) - -- if topic is ?, then generate it - if headers.topic == "?" then + if headers.topic and headers.topic:gsub("[^%w]", "") == "" then -- insert last model response table.insert(messages, { role = "assistant", content = qt.response }) @@ -1103,23 +1102,18 @@ M.chat_respond = function(params) M.dispatcher.prepare_payload(messages, headers.model or agent.model, headers.provider or agent.provider), topic_handler, vim.schedule_wrap(function() - -- get topic from invisible buffer local topic = vim.api.nvim_buf_get_lines(topic_buf, 0, -1, false)[1] - -- close invisible buffer vim.api.nvim_buf_delete(topic_buf, { force = true }) - -- strip whitespace from ends of topic topic = topic:gsub("^%s*(.-)%s*$", "%1") - -- strip dot from end of topic topic = topic:gsub("%.$", "") - -- if topic is empty do not replace it if topic == "" then return end - -- replace topic in current buffer + local i = indices.topic M.helpers.undojoin(buf) - vim.api.nvim_buf_set_lines(buf, 0, 1, false, { "# topic: " .. topic }) + vim.api.nvim_buf_set_lines(buf, i, i + 1, false, { "# topic: " .. topic }) end) ) end From ccc7b708bb8a9a1b7a2472bb9d0786816432ff8b Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 19:41:45 +0200 Subject: [PATCH 03/33] feat: ftplugin for .gp.md files --- after/ftplugin/gpmd.lua | 24 ++++++++++++++++++++++++ lua/gp/helper.lua | 12 ++++++++++++ lua/gp/init.lua | 29 +++++++++++------------------ 3 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 after/ftplugin/gpmd.lua diff --git a/after/ftplugin/gpmd.lua b/after/ftplugin/gpmd.lua new file mode 100644 index 00000000..ccc9252b --- /dev/null +++ b/after/ftplugin/gpmd.lua @@ -0,0 +1,24 @@ +local M = require("gp") + +M.logger.debug("gpmd: loading ftplugin") + +vim.opt_local.swapfile = false +vim.opt_local.wrap = true +vim.opt_local.linebreak = true + +local buf = vim.api.nvim_get_current_buf() + +vim.api.nvim_create_autocmd({ "TextChanged", "InsertLeave" }, { + buffer = buf, + callback = function(event) + if M.helpers.deleted_invalid_autocmd(buf, event) then + return + end + M.logger.debug("gpmd: saving buffer " .. buf .. " " .. vim.json.encode(event)) + vim.api.nvim_command("silent! write") + end, +}) + +-- ensure normal mode +vim.cmd.stopinsert() +M.helpers.feedkeys("", "xn") diff --git a/lua/gp/helper.lua b/lua/gp/helper.lua index f3d6d780..c3cd1f1c 100644 --- a/lua/gp/helper.lua +++ b/lua/gp/helper.lua @@ -313,4 +313,16 @@ _H.parse_headers = function(lines) return headers, indices, nil end + +---@param buf number # buffer number +---@param event table # event object +_H.deleted_invalid_autocmd = function(buf, event) + if not vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_del_autocmd(event.id) + logger.debug("deleting invalid autocmd: " .. event.id .. " for buffer: " .. buf) + return true + end + return false +end + return _H diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 8f4660ee..6e994cec 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -209,6 +209,17 @@ M.setup = function(opts) end M.buf_handler() + vim.filetype.add({ + extension = { + md = function(path, buf) + M.logger.debug("filetype markdown: " .. path .. " buf: " .. buf) + if M.helpers.ends_with(path, ".gp.md") then + return "markdown.gpmd" + end + return "markdown" + end, + }, + }) if vim.fn.executable("curl") == 0 then M.logger.error("curl is not installed, run :checkhealth gp") @@ -591,7 +602,6 @@ M.buf_handler = function() M.prep_chat(buf, file_name) M.display_chat_agent(buf, file_name) - M.prep_context(buf, file_name) end, gid) M.helpers.autocmd({ "WinEnter" }, nil, function(event) @@ -1584,23 +1594,6 @@ M.repo_instructions = function() return table.concat(lines, "\n") end -M.prep_context = function(buf, file_name) - if not M.helpers.ends_with(file_name, ".gp.md") then - return - end - - if buf ~= vim.api.nvim_get_current_buf() then - return - end - if M._prepared_bufs[buf] then - M.logger.debug("buffer already prepared: " .. buf) - return - end - M._prepared_bufs[buf] = true - - M.prep_md(buf) -end - M.cmd.Context = function(params) M._toggle_close(M._toggle_kind.popup) -- if there is no selection, try to close context toggle From 90b3e33e5dc3d202eeae3520f47f1f9c3940e4fa Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 19:43:00 +0200 Subject: [PATCH 04/33] chore: better logs --- lua/gp/helper.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/gp/helper.lua b/lua/gp/helper.lua index c3cd1f1c..9a46f55d 100644 --- a/lua/gp/helper.lua +++ b/lua/gp/helper.lua @@ -272,12 +272,12 @@ _H.create_user_command = function(cmd_name, cmd_func, completion, desc) logger.debug( "completing user command: " .. cmd_name - .. "\narg_lead: " - .. arg_lead - .. "\ncmd_line: " - .. cmd_line - .. "\ncursor_pos: " + .. " cursor_pos: " .. cursor_pos + .. " arg_lead: " + .. vim.inspect(arg_lead) + .. " cmd_line: " + .. vim.inspect(cmd_line) ) if not completion then return {} From 9687d740c70d06271e9ac2b3768ebb9190dae1d2 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 19:47:45 +0200 Subject: [PATCH 05/33] feat: picker support for dynamic chat names --- lua/gp/init.lua | 11 ++++++++++- lua/gp/tasker.lua | 7 ------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 6e994cec..45d10f07 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -1313,13 +1313,22 @@ M.cmd.ChatFinder = function() return end + table.sort(results, function(a, b) + local af = a.file:sub(-24, -11) + local bf = b.file:sub(-24, -11) + if af == bf then + return a.lnum < b.lnum + end + return af > bf + end) + picker_files = {} preview_lines = {} local picker_lines = {} for _, f in ipairs(results) do if f.line:len() > 0 then table.insert(picker_files, dir .. "/" .. f.file) - local fline = string.format("%s:%s %s", f.file:sub(3, -11), f.lnum, f.line) + local fline = string.format("%s:%s %s", f.file:sub(-24, -11), f.lnum, f.line) table.insert(picker_lines, fline) table.insert(preview_lines, tonumber(f.lnum)) end diff --git a/lua/gp/tasker.lua b/lua/gp/tasker.lua index df630e3c..94c30c51 100644 --- a/lua/gp/tasker.lua +++ b/lua/gp/tasker.lua @@ -216,13 +216,6 @@ M.grep_directory = function(buf, directory, pattern, callback) }) end end - table.sort(results, function(a, b) - if a.file == b.file then - return a.lnum < b.lnum - else - return a.file > b.file - end - end) callback(results, re) end) end From d07cd82ed8dcca8535979bf4ae053d56947f61f0 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 20:45:13 +0200 Subject: [PATCH 06/33] feat: ftplugin for chat files --- after/ftplugin/gpchat.lua | 118 +++++++++++++++++++++++++++++ lua/gp/init.lua | 153 ++------------------------------------ 2 files changed, 123 insertions(+), 148 deletions(-) create mode 100644 after/ftplugin/gpchat.lua diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua new file mode 100644 index 00000000..6319c752 --- /dev/null +++ b/after/ftplugin/gpchat.lua @@ -0,0 +1,118 @@ +local M = require("gp") + +M.logger.debug("gpchat: loading ftplugin") + +vim.opt_local.swapfile = false +vim.opt_local.wrap = true +vim.opt_local.linebreak = true + +local buf = vim.api.nvim_get_current_buf() +local ns_id = vim.api.nvim_create_namespace("GpChatExt_" .. buf) + +-- ensure normal mode +vim.cmd.stopinsert() +M.helpers.feedkeys("", "xn") + +M.logger.debug("gpchat: ns_id " .. ns_id .. " for buffer " .. buf) + +if M.config.chat_prompt_buf_type then + vim.api.nvim_set_option_value("buftype", "prompt", { buf = buf }) + vim.fn.prompt_setprompt(buf, "") + vim.fn.prompt_setcallback(buf, function() + M.cmd.ChatRespond({ args = "" }) + end) +end + +-- setup chat specific commands +local commands = { + { + command = "ChatRespond", + modes = M.config.chat_shortcut_respond.modes, + shortcut = M.config.chat_shortcut_respond.shortcut, + comment = "GPT prompt Chat Respond", + }, + { + command = "ChatNew", + modes = M.config.chat_shortcut_new.modes, + shortcut = M.config.chat_shortcut_new.shortcut, + comment = "GPT prompt Chat New", + }, +} +for _, rc in ipairs(commands) do + local cmd = M.config.cmd_prefix .. rc.command .. "" + for _, mode in ipairs(rc.modes) do + if mode == "n" or mode == "i" then + M.helpers.set_keymap({ buf }, mode, rc.shortcut, function() + vim.api.nvim_command(M.config.cmd_prefix .. rc.command) + -- go to normal mode + vim.api.nvim_command("stopinsert") + M.helpers.feedkeys("", "xn") + end, rc.comment) + else + M.helpers.set_keymap({ buf }, mode, rc.shortcut, ":'<,'>" .. cmd, rc.comment) + end + end +end + +local ds = M.config.chat_shortcut_delete +M.helpers.set_keymap({ buf }, ds.modes, ds.shortcut, M.cmd.ChatDelete, "GPT prompt Chat Delete") + +local ss = M.config.chat_shortcut_stop +M.helpers.set_keymap({ buf }, ss.modes, ss.shortcut, M.cmd.Stop, "GPT prompt Chat Stop") + +-- conceal parameters in model header so it's not distracting +if M.config.chat_conceal_model_params then + vim.opt_local.conceallevel = 2 + vim.opt_local.concealcursor = "" + vim.fn.matchadd("Conceal", [[^- model: .*model.:.[^"]*\zs".*\ze]], 10, -1, { conceal = "…" }) + vim.fn.matchadd("Conceal", [[^- model: \zs.*model.:.\ze.*]], 10, -1, { conceal = "…" }) + vim.fn.matchadd("Conceal", [[^- role: .\{64,64\}\zs.*\ze]], 10, -1, { conceal = "…" }) + vim.fn.matchadd("Conceal", [[^- role: .[^\\]*\zs\\.*\ze]], 10, -1, { conceal = "…" }) +end + +vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, { + buffer = buf, + callback = function(event) + if M.helpers.deleted_invalid_autocmd(buf, event) then + return + end + -- M.logger.debug("gpchat: entering buffer " .. buf .. " " .. vim.json.encode(event)) + + vim.cmd("doautocmd User GpRefresh") + end, +}) + +vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "InsertLeave" }, { + buffer = buf, + callback = function(event) + if M.helpers.deleted_invalid_autocmd(buf, event) then + return + end + -- M.logger.debug("gpchat: saving buffer " .. buf .. " " .. vim.json.encode(event)) + vim.api.nvim_command("silent! write") + end, +}) +vim.api.nvim_create_autocmd({ "User" }, { + callback = function(event) + if event.event == "User" and event.match ~= "GpRefresh" then + return + end + if M.helpers.deleted_invalid_autocmd(buf, event) then + return + end + + M.logger.debug("gpchat: refreshing buffer " .. buf .. " " .. vim.json.encode(event)) + + vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) + + vim.api.nvim_buf_set_extmark(buf, ns_id, 0, 0, { + strict = false, + right_gravity = true, + virt_text_pos = "right_align", + virt_text = { + { "Current Agent: [" .. M._state.chat_agent .. "]", "DiagnosticHint" }, + }, + hl_mode = "combine", + }) + end, +}) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 45d10f07..f2ae80b4 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -208,11 +208,14 @@ M.setup = function(opts) end end - M.buf_handler() vim.filetype.add({ extension = { md = function(path, buf) M.logger.debug("filetype markdown: " .. path .. " buf: " .. buf) + if not M.not_chat(buf, path) then + return "markdown.gpchat" + end + if M.helpers.ends_with(path, ".gp.md") then return "markdown.gpmd" end @@ -286,9 +289,7 @@ M.refresh_state = function(update) M.prepare_commands() - local buf = vim.api.nvim_get_current_buf() - local file_name = vim.api.nvim_buf_get_name(buf) - M.display_chat_agent(buf, file_name) + vim.cmd("doautocmd User GpRefresh") end M.Target = { @@ -439,23 +440,6 @@ M._toggle_resolve = function(kind) return M._toggle_kind.unknown end ----@param buf number | nil # buffer number -M.prep_md = function(buf) - -- disable swapping for this buffer and set filetype to markdown - vim.api.nvim_command("setlocal noswapfile") - -- better text wrapping - vim.api.nvim_command("setlocal wrap linebreak") - -- auto save on TextChanged, InsertLeave - vim.api.nvim_command("autocmd TextChanged,InsertLeave silent! write") - - -- register shortcuts local to this buffer - buf = buf or vim.api.nvim_get_current_buf() - - -- ensure normal mode - vim.api.nvim_command("stopinsert") - M.helpers.feedkeys("", "xn") -end - ---@param buf number # buffer number ---@param file_name string # file name ---@return string | nil # reason for not being a chat or nil if it is a chat @@ -490,133 +474,6 @@ M.not_chat = function(buf, file_name) return nil end -M.display_chat_agent = function(buf, file_name) - if M.not_chat(buf, file_name) then - return - end - - if buf ~= vim.api.nvim_get_current_buf() then - return - end - - local ns_id = vim.api.nvim_create_namespace("GpChatExt_" .. file_name) - vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) - - vim.api.nvim_buf_set_extmark(buf, ns_id, 0, 0, { - strict = false, - right_gravity = true, - virt_text_pos = "right_align", - virt_text = { - { "Current Agent: [" .. M._state.chat_agent .. "]", "DiagnosticHint" }, - }, - hl_mode = "combine", - }) -end - -M._prepared_bufs = {} -M.prep_chat = function(buf, file_name) - if M.not_chat(buf, file_name) then - return - end - - if buf ~= vim.api.nvim_get_current_buf() then - return - end - - M.refresh_state({ last_chat = file_name }) - if M._prepared_bufs[buf] then - M.logger.debug("buffer already prepared: " .. buf) - return - end - M._prepared_bufs[buf] = true - - M.prep_md(buf) - - if M.config.chat_prompt_buf_type then - vim.api.nvim_set_option_value("buftype", "prompt", { buf = buf }) - vim.fn.prompt_setprompt(buf, "") - vim.fn.prompt_setcallback(buf, function() - M.cmd.ChatRespond({ args = "" }) - end) - end - - -- setup chat specific commands - local range_commands = { - { - command = "ChatRespond", - modes = M.config.chat_shortcut_respond.modes, - shortcut = M.config.chat_shortcut_respond.shortcut, - comment = "GPT prompt Chat Respond", - }, - { - command = "ChatNew", - modes = M.config.chat_shortcut_new.modes, - shortcut = M.config.chat_shortcut_new.shortcut, - comment = "GPT prompt Chat New", - }, - } - for _, rc in ipairs(range_commands) do - local cmd = M.config.cmd_prefix .. rc.command .. "" - for _, mode in ipairs(rc.modes) do - if mode == "n" or mode == "i" then - M.helpers.set_keymap({ buf }, mode, rc.shortcut, function() - vim.api.nvim_command(M.config.cmd_prefix .. rc.command) - -- go to normal mode - vim.api.nvim_command("stopinsert") - M.helpers.feedkeys("", "xn") - end, rc.comment) - else - M.helpers.set_keymap({ buf }, mode, rc.shortcut, ":'<,'>" .. cmd, rc.comment) - end - end - end - - local ds = M.config.chat_shortcut_delete - M.helpers.set_keymap({ buf }, ds.modes, ds.shortcut, M.cmd.ChatDelete, "GPT prompt Chat Delete") - - local ss = M.config.chat_shortcut_stop - M.helpers.set_keymap({ buf }, ss.modes, ss.shortcut, M.cmd.Stop, "GPT prompt Chat Stop") - - -- conceal parameters in model header so it's not distracting - if M.config.chat_conceal_model_params then - vim.opt_local.conceallevel = 2 - vim.opt_local.concealcursor = "" - vim.fn.matchadd("Conceal", [[^- model: .*model.:.[^"]*\zs".*\ze]], 10, -1, { conceal = "…" }) - vim.fn.matchadd("Conceal", [[^- model: \zs.*model.:.\ze.*]], 10, -1, { conceal = "…" }) - vim.fn.matchadd("Conceal", [[^- role: .\{64,64\}\zs.*\ze]], 10, -1, { conceal = "…" }) - vim.fn.matchadd("Conceal", [[^- role: .[^\\]*\zs\\.*\ze]], 10, -1, { conceal = "…" }) - end -end - -M.buf_handler = function() - local gid = M.helpers.create_augroup("GpBufHandler", { clear = true }) - - M.helpers.autocmd({ "BufEnter" }, nil, function(event) - local buf = event.buf - - if not vim.api.nvim_buf_is_valid(buf) then - return - end - - local file_name = vim.api.nvim_buf_get_name(buf) - - M.prep_chat(buf, file_name) - M.display_chat_agent(buf, file_name) - end, gid) - - M.helpers.autocmd({ "WinEnter" }, nil, function(event) - local buf = event.buf - - if not vim.api.nvim_buf_is_valid(buf) then - return - end - - local file_name = vim.api.nvim_buf_get_name(buf) - - M.display_chat_agent(buf, file_name) - end, gid) -end - M.BufTarget = { current = 0, -- current window popup = 1, -- popup window From 553d49bbe6b2a219d7e3029756bbd2711880eb7b Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 20:47:20 +0200 Subject: [PATCH 07/33] feat: autorename chats using topic (issue: #133) --- after/ftplugin/gpchat.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index 6319c752..4a177cf5 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -88,6 +88,27 @@ vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "InsertLeave" }, { if M.helpers.deleted_invalid_autocmd(buf, event) then return end + + local filename = vim.api.nvim_buf_get_name(buf) + local dir = vim.fn.fnamemodify(filename, ":h") + + local name = vim.fn.fnamemodify(filename, ":t") + local _, _, prefix = name:find("^(.*)_[^_]*$") + name = prefix and name:sub(#prefix + 2) or name + + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local headers, _, _ = M.helpers.parse_headers(lines) + local topic = headers["topic"] or "" + topic = topic:gsub("[^%w%s]", ""):lower() + topic = topic:gsub("%s+", "_"):gsub("^_+", ""):gsub("_+$", "") + + if topic and topic ~= "" and topic ~= prefix then + local new_filename = dir .. "/" .. topic .. "_" .. name + M.logger.debug("gpchat: renaming buffer " .. buf .. " from " .. filename .. " to " .. new_filename) + vim.api.nvim_buf_set_name(buf, new_filename) + M.helpers.delete_file(filename) + end + -- M.logger.debug("gpchat: saving buffer " .. buf .. " " .. vim.json.encode(event)) vim.api.nvim_command("silent! write") end, From 800d8f6ab9c5178f997df3575dfa4945c1d2f955 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 20:54:30 +0200 Subject: [PATCH 08/33] feat: looser chat file condition (issue: #106) - file and topic headers not mandatory - current conditions: .md, chat dir, header section break --- lua/gp/init.lua | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index f2ae80b4..50d81792 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -451,24 +451,22 @@ M.not_chat = function(buf, file_name) return "resolved file (" .. file_name .. ") not in chat dir (" .. chat_dir .. ")" end - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - if #lines < 5 then - return "file too short" + local extension = vim.fn.fnamemodify(file_name, ":e") + if extension ~= "md" then + return "file extension is not .md" end - if not lines[1]:match("^# ") then - return "missing topic header" - end + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - local header_found = nil - for i = 1, 10 do - if i < #lines and lines[i]:match("^- file: ") then - header_found = true + local header_break_found = false + for i = 2, 20 do + if i < #lines and lines[i]:match("^%-%-%-%s*$") then + header_break_found = true break end end - if not header_found then - return "missing file header" + if not header_break_found then + return "missing header break" end return nil From f18f78d51f4651ff3fa1fa6f82771b96486c53a6 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 17 Aug 2024 22:57:00 +0200 Subject: [PATCH 09/33] feat: YAML frontmatter convention for chat templates --- lua/gp/defaults.lua | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lua/gp/defaults.lua b/lua/gp/defaults.lua index 0aa1572a..abd33a23 100644 --- a/lua/gp/defaults.lua +++ b/lua/gp/defaults.lua @@ -14,23 +14,21 @@ M.code_system_prompt = "You are an AI working as a code editor.\n\n" .. "START AND END YOUR ANSWER WITH:\n\n```" M.chat_template = [[ +--- # topic: ? - -- file: {{filename}} {{optional_headers}} -Write your queries after {{user_prefix}}. Use `{{respond_shortcut}}` or :{{cmd_prefix}}ChatRespond to generate a response. -Response generation can be terminated by using `{{stop_shortcut}}` or :{{cmd_prefix}}ChatStop command. -Chats are saved automatically. To delete this chat, use `{{delete_shortcut}}` or :{{cmd_prefix}}ChatDelete. -Be cautious of very long chats. Start a fresh chat by using `{{new_shortcut}}` or :{{cmd_prefix}}ChatNew. - +# Write your queries after {{user_prefix}}. Use `{{respond_shortcut}}` or :{{cmd_prefix}}ChatRespond to generate a response. +# Response generation can be terminated by using `{{stop_shortcut}}` or :{{cmd_prefix}}ChatStop command. +# Chats are saved automatically. To delete this chat, use `{{delete_shortcut}}` or :{{cmd_prefix}}ChatDelete. +# Be cautious of very long chats. Start a fresh chat by using `{{new_shortcut}}` or :{{cmd_prefix}}ChatNew. --- {{user_prefix}} ]] M.short_chat_template = [[ +--- # topic: ? -- file: {{filename}} --- {{user_prefix}} From a06784e3fd774dc9d0c37beb9a6a87e162a786d8 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sun, 18 Aug 2024 00:33:45 +0200 Subject: [PATCH 10/33] feat: command macro support - macro for target filetype (issue: #114) --- lua/gp/init.lua | 42 ++++++++- lua/gp/macro.lua | 142 ++++++++++++++++++++++++++++++ lua/gp/macros/target.lua | 47 ++++++++++ lua/gp/macros/target_filename.lua | 44 +++++++++ lua/gp/macros/target_filetype.lua | 38 ++++++++ 5 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 lua/gp/macro.lua create mode 100644 lua/gp/macros/target.lua create mode 100644 lua/gp/macros/target_filename.lua create mode 100644 lua/gp/macros/target_filetype.lua diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 50d81792..c2859ad0 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -6,6 +6,9 @@ -------------------------------------------------------------------------------- local config = require("gp.config") +local uv = vim.uv or vim.loop +table.unpack = table.unpack or unpack -- 5.1 compatibility + local M = { _Name = "Gp", -- plugin name _state = {}, -- table of state variables @@ -24,6 +27,7 @@ local M = { tasker = require("gp.tasker"), -- tasker module vault = require("gp.vault"), -- handles secrets whisper = require("gp.whisper"), -- whisper module + macro = require("gp.macro"), -- builder for macro completion } -------------------------------------------------------------------------------- @@ -189,12 +193,41 @@ M.setup = function(opts) end) end + M.logger.debug("hook setup done") + + local ft_completion = M.macro.build_completion({ + require("gp.macros.target_filetype"), + }, {}) + + M.logger.debug("ft_completion done") + + local do_completion = M.macro.build_completion({ + require("gp.macros.target"), + require("gp.macros.target_filetype"), + require("gp.macros.target_filename"), + }, {}) + + M.logger.debug("do_completion done") + + M.command_parser = M.macro.build_parser({ + require("gp.macros.target"), + require("gp.macros.target_filetype"), + require("gp.macros.target_filename"), + }) + + M.logger.debug("command_parser done") + local completions = { ChatNew = { "popup", "split", "vsplit", "tabnew" }, ChatPaste = { "popup", "split", "vsplit", "tabnew" }, ChatToggle = { "popup", "split", "vsplit", "tabnew" }, Context = { "popup", "split", "vsplit", "tabnew" }, Agent = agent_completion, + Do = do_completion, + Enew = ft_completion, + New = ft_completion, + Vnew = ft_completion, + Tabnew = ft_completion, } -- register default commands @@ -1653,6 +1686,13 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) local filetype = M.helpers.get_filetype(buf) local filename = vim.api.nvim_buf_get_name(buf) + local state = {} + local response = M.command_parser(command, {}, state) + if response then + command = M.render.template(response.template, response.artifacts) + state = response.state + end + local sys_prompt = M.render.prompt_template(agent.system_prompt, command, selection, filetype, filename) sys_prompt = sys_prompt or "" table.insert(messages, { role = "system", content = sys_prompt }) @@ -1749,7 +1789,7 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) end, }) - local ft = target.filetype or filetype + local ft = state.target_filetype or target.filetype or filetype vim.api.nvim_set_option_value("filetype", ft, { buf = buf }) handler = M.dispatcher.create_handler(buf, win, 0, false, "", cursor) diff --git a/lua/gp/macro.lua b/lua/gp/macro.lua new file mode 100644 index 00000000..0081a248 --- /dev/null +++ b/lua/gp/macro.lua @@ -0,0 +1,142 @@ +local logger = require("gp.logger") + +---@class gp.Macro_cmd_params +---@field arg_lead string +---@field cmd_line string +---@field cursor_pos number +---@field cropped_line string + +---@class gp.Macro_parser_result +---@field template string +---@field artifacts table +---@field state table + +--- gp.Macro Interface +-- @field name string: Name of the macro. +-- @field description string: Description of the macro. +-- @field default string: Default value for the macro (optional). +-- @field max_occurrences number: Maximum number of occurrences for the macro (optional). +-- @field triggered function: Function that determines if the macro is triggered. +-- @field completion function: Function that provides completion options. +-- @field parser function: Function that processes the macro in the template. + +---@class gp.Macro +---@field name string +---@field description string +---@field default? string +---@field max_occurrences? number +---@field triggered fun(params: gp.Macro_cmd_params, state: table): boolean +---@field completion fun(params: gp.Macro_cmd_params, state: table): string[] +---@field parser fun(params: gp.Macro_parser_result): gp.Macro_parser_result + +---@param value string # string to hash +---@return string # returns hash of the string +local fnv1a_hash = function(value) + ---@type number + local hash = 2166136261 + for i = 1, #value do + hash = vim.fn.xor(hash, string.byte(value, i)) + hash = vim.fn["and"]((hash * 16777619), 0xFFFFFFFF) + end + return string.format("%08x", hash) -- return as an 8-character hex string +end + +local M = {} + +---@param prefix string # prefix for the placeholder +---@param value string # value to hash +---@return string # returns placeholder +M.generate_placeholder = function(prefix, value) + local hash_value = fnv1a_hash(value) + local placeholder = "{{" .. prefix .. "." .. hash_value .. "}}" + return placeholder +end + +---@param macros gp.Macro[] +---@return fun(template: string, artifacts: table, state: table): gp.Macro_parser_result +M.build_parser = function(macros) + ---@param template string + ---@param artifacts table + ---@param state table + ---@return {template: string, artifacts: table, state: table} + local function parser(template, artifacts, state) + template = template or "" + ---@type gp.Macro_parser_result + local result = { + template = " " .. template .. " ", + artifacts = artifacts or {}, + state = state or {}, + } + logger.debug("macro parser input: " .. vim.inspect(result)) + + for _, macro in pairs(macros) do + logger.debug("macro parser current macro: " .. vim.inspect(macro)) + result = macro.parser(result) + logger.debug("macro parser result: " .. vim.inspect(result)) + end + return result + end + + return parser +end + +---@param macros gp.Macro[] +---@param state table +---@return fun(arg_lead: string, cmd_line: string, cursor_pos: number): string[] +M.build_completion = function(macros, state) + ---@type table + local map = {} + for _, macro in pairs(macros) do + map[macro.name] = macro + state[macro.name .. "_default"] = macro.default + end + + ---@param arg_lead string + ---@param cmd_line string + ---@param cursor_pos number + ---@return string[] + local function completion(arg_lead, cmd_line, cursor_pos) + local cropped_line = cmd_line:sub(1, cursor_pos) + + ---@type gp.Macro_cmd_params + local params = { + arg_lead = arg_lead, + cmd_line = cmd_line, + cursor_pos = cursor_pos, + cropped_line = cropped_line, + } + + local suggestions = {} + + logger.debug("macro completion input: " .. vim.inspect({ + params = params, + state = state, + })) + + ---@type table + local candidates = {} + local cand = nil + for c in cropped_line:gmatch("%s@(%S+)%s") do + candidates[c] = candidates[c] and candidates[c] + 1 or 1 + cand = c + end + logger.debug("macro completion candidates: " .. vim.inspect(candidates)) + + if cand and map[cand] and map[cand].triggered(params, state) then + suggestions = map[cand].completion(params, state) + elseif cropped_line:match("%s$") or cropped_line:match("%s@%S*$") then + for _, c in pairs(macros) do + if not candidates[c.name] or candidates[c.name] < c.max_occurrences then + table.insert(suggestions, "@" .. c.name) + end + end + end + + logger.debug("macro completion suggestions: " .. vim.inspect(suggestions)) + return vim.deepcopy(suggestions) + end + + return completion +end + +return M diff --git a/lua/gp/macros/target.lua b/lua/gp/macros/target.lua new file mode 100644 index 00000000..ed71e0d7 --- /dev/null +++ b/lua/gp/macros/target.lua @@ -0,0 +1,47 @@ +local macro = require("gp.macro") + +local values = { + "rewrite", + "append", + "prepend", + "popup", + "enew", + "new", + "vnew", + "tabnew", +} + +local M = {} + +---@type gp.Macro +M = { + name = "target", + description = "handles target for commands", + default = "rewrite", + max_occurrences = 1, + + triggered = function(params, state) + local cropped_line = params.cropped_line + return cropped_line:match("@target%s+%S*$") + end, + + completion = function(params, state) + return values + end, + + parser = function(result) + local template = result.template + local s, e, value = template:find("@target%s+(%S+)") + if not value then + return result + end + + local placeholder = macro.generate_placeholder(M.name, value) + result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1) + result.state[M.name] = value + result.artifacts[placeholder] = "" + return result + end, +} + +return M diff --git a/lua/gp/macros/target_filename.lua b/lua/gp/macros/target_filename.lua new file mode 100644 index 00000000..b2e8435f --- /dev/null +++ b/lua/gp/macros/target_filename.lua @@ -0,0 +1,44 @@ +local macro = require("gp.macro") + +local M = {} + +---@type gp.Macro +M = { + name = "target_filename`", + description = "handles target buffer filename for commands", + default = nil, + max_occurrences = 1, + + triggered = function(params, state) + local cropped_line = params.cropped_line + return cropped_line:match("@target_filename`[^`]*$") + end, + + completion = function(params, state) + -- TODO state.root_dir ? + local files = vim.fn.glob("**", true, true) + -- local files = vim.fn.getcompletion("", "file") + files = vim.tbl_map(function(file) + return file .. " `" + end, files) + return files + end, + + parser = function(result) + local template = result.template + local s, e, value = template:find("@target_filename`([^`]*)`") + if not value then + return result + end + + value = value:match("^%s*(.-)%s*$") + local placeholder = macro.generate_placeholder(M.name, value) + + result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1) + result.state[M.name] = value + result.artifacts[placeholder] = "" + return result + end, +} + +return M diff --git a/lua/gp/macros/target_filetype.lua b/lua/gp/macros/target_filetype.lua new file mode 100644 index 00000000..59045422 --- /dev/null +++ b/lua/gp/macros/target_filetype.lua @@ -0,0 +1,38 @@ +local macro = require("gp.macro") + +local values = vim.fn.getcompletion("", "filetype") + +local M = {} + +---@type gp.Macro +M = { + name = "target_filetype", + description = "handles target buffer filetype for commands like GpEnew", + default = "markdown", + max_occurrences = 1, + + triggered = function(params, state) + local cropped_line = params.cropped_line + return cropped_line:match("@target_filetype%s+%S*$") + end, + + completion = function(params, state) + return values + end, + + parser = function(result) + local template = result.template + local s, e, value = template:find("@target_filetype%s+(%S+)") + if not value then + return result + end + + local placeholder = macro.generate_placeholder(M.name, value) + result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1) + result.state[M.name] = value + result.artifacts[placeholder] = "" + return result + end, +} + +return M From 161aff28bb63e1dc1a4260355419d2bb8d885066 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 21:42:42 +0200 Subject: [PATCH 11/33] refactor: no state across macro completion --- lua/gp/init.lua | 4 ++-- lua/gp/macro.lua | 35 ++++++++++++++++++++----------- lua/gp/macros/target.lua | 7 +++---- lua/gp/macros/target_filename.lua | 4 ++-- lua/gp/macros/target_filetype.lua | 7 +++---- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index c2859ad0..bf89ba83 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -197,7 +197,7 @@ M.setup = function(opts) local ft_completion = M.macro.build_completion({ require("gp.macros.target_filetype"), - }, {}) + }) M.logger.debug("ft_completion done") @@ -205,7 +205,7 @@ M.setup = function(opts) require("gp.macros.target"), require("gp.macros.target_filetype"), require("gp.macros.target_filename"), - }, {}) + }) M.logger.debug("do_completion done") diff --git a/lua/gp/macro.lua b/lua/gp/macro.lua index 0081a248..c779021d 100644 --- a/lua/gp/macro.lua +++ b/lua/gp/macro.lua @@ -25,8 +25,8 @@ local logger = require("gp.logger") ---@field description string ---@field default? string ---@field max_occurrences? number ----@field triggered fun(params: gp.Macro_cmd_params, state: table): boolean ----@field completion fun(params: gp.Macro_cmd_params, state: table): string[] +---@field triggered fun(params: gp.Macro_cmd_params): boolean +---@field completion fun(params: gp.Macro_cmd_params): string[] ---@field parser fun(params: gp.Macro_parser_result): gp.Macro_parser_result ---@param value string # string to hash @@ -81,21 +81,20 @@ M.build_parser = function(macros) end ---@param macros gp.Macro[] ----@param state table ----@return fun(arg_lead: string, cmd_line: string, cursor_pos: number): string[] -M.build_completion = function(macros, state) +---@param raw boolean | nil # which function to return (completion or raw_completion) +---@return fun(arg_lead: string, cmd_line: string, cursor_pos: number): string[], boolean | nil +M.build_completion = function(macros, raw) ---@type table local map = {} for _, macro in pairs(macros) do map[macro.name] = macro - state[macro.name .. "_default"] = macro.default end ---@param arg_lead string ---@param cmd_line string ---@param cursor_pos number - ---@return string[] - local function completion(arg_lead, cmd_line, cursor_pos) + ---@return string[], boolean # returns suggestions and whether some macro was triggered + local function raw_completion(arg_lead, cmd_line, cursor_pos) local cropped_line = cmd_line:sub(1, cursor_pos) ---@type gp.Macro_cmd_params @@ -106,11 +105,13 @@ M.build_completion = function(macros, state) cropped_line = cropped_line, } + cropped_line = " " .. cropped_line + local suggestions = {} + local triggered = false logger.debug("macro completion input: " .. vim.inspect({ params = params, - state = state, })) ---@type table @@ -122,8 +123,9 @@ M.build_completion = function(macros, state) end logger.debug("macro completion candidates: " .. vim.inspect(candidates)) - if cand and map[cand] and map[cand].triggered(params, state) then - suggestions = map[cand].completion(params, state) + if cand and map[cand] and map[cand].triggered(params) then + suggestions = map[cand].completion(params) + triggered = true elseif cropped_line:match("%s$") or cropped_line:match("%s@%S*$") then for _, c in pairs(macros) do if not candidates[c.name] or candidates[c.name] < c.max_occurrences then @@ -133,7 +135,16 @@ M.build_completion = function(macros, state) end logger.debug("macro completion suggestions: " .. vim.inspect(suggestions)) - return vim.deepcopy(suggestions) + return vim.deepcopy(suggestions), triggered + end + + local completion = function(arg_lead, cmd_line, cursor_pos) + local suggestions, _ = raw_completion(arg_lead, cmd_line, cursor_pos) + return suggestions + end + + if raw then + return raw_completion end return completion diff --git a/lua/gp/macros/target.lua b/lua/gp/macros/target.lua index ed71e0d7..5bceabb0 100644 --- a/lua/gp/macros/target.lua +++ b/lua/gp/macros/target.lua @@ -20,12 +20,11 @@ M = { default = "rewrite", max_occurrences = 1, - triggered = function(params, state) - local cropped_line = params.cropped_line - return cropped_line:match("@target%s+%S*$") + triggered = function(params) + return params.cropped_line:match("@target%s+%S*$") end, - completion = function(params, state) + completion = function(params) return values end, diff --git a/lua/gp/macros/target_filename.lua b/lua/gp/macros/target_filename.lua index b2e8435f..47becec1 100644 --- a/lua/gp/macros/target_filename.lua +++ b/lua/gp/macros/target_filename.lua @@ -9,12 +9,12 @@ M = { default = nil, max_occurrences = 1, - triggered = function(params, state) + triggered = function(params) local cropped_line = params.cropped_line return cropped_line:match("@target_filename`[^`]*$") end, - completion = function(params, state) + completion = function(params) -- TODO state.root_dir ? local files = vim.fn.glob("**", true, true) -- local files = vim.fn.getcompletion("", "file") diff --git a/lua/gp/macros/target_filetype.lua b/lua/gp/macros/target_filetype.lua index 59045422..21eb2f8a 100644 --- a/lua/gp/macros/target_filetype.lua +++ b/lua/gp/macros/target_filetype.lua @@ -11,12 +11,11 @@ M = { default = "markdown", max_occurrences = 1, - triggered = function(params, state) - local cropped_line = params.cropped_line - return cropped_line:match("@target_filetype%s+%S*$") + triggered = function(params) + return params.cropped_line:match("@target_filetype%s+%S*$") end, - completion = function(params, state) + completion = function(params) return values end, From 91e7770e429bc0f25a421b0af54382a0f0a77c75 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 22:09:59 +0200 Subject: [PATCH 12/33] feat: delayed .md ft handling (cause obsidian) --- lua/gp/init.lua | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index bf89ba83..7b15873c 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -241,20 +241,22 @@ M.setup = function(opts) end end - vim.filetype.add({ - extension = { - md = function(path, buf) - M.logger.debug("filetype markdown: " .. path .. " buf: " .. buf) - if not M.not_chat(buf, path) then - return "markdown.gpchat" + vim.api.nvim_create_autocmd("BufEnter", { + pattern = "*.md", + callback = function(ev) + vim.defer_fn(function() + M.logger.debug("Markdown BufEnter: " .. ev.file) + local path = ev.file + local buf = ev.buf + local current_ft = vim.bo[buf].filetype + if not M.not_chat(buf, path) and current_ft ~= "markdown.gpchat" then + vim.bo[buf].filetype = "markdown.gpchat" + elseif M.helpers.ends_with(path, ".gp.md") and current_ft ~= "markdown.gpmd" then + vim.bo[buf].filetype = "markdown.gpmd" end - - if M.helpers.ends_with(path, ".gp.md") then - return "markdown.gpmd" - end - return "markdown" - end, - }, + vim.cmd("doautocmd User GpRefresh") + end, 100) + end, }) if vim.fn.executable("curl") == 0 then From 0b737ffff4130b1408d39ddb3a7cbcd718306b75 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 22:13:14 +0200 Subject: [PATCH 13/33] fix: handle chat text before first user prefix (for easier use in .md buffers from obsidian and such) --- lua/gp/init.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 7b15873c..0a316b4b 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -861,16 +861,19 @@ M.chat_respond = function(params) -- message needs role and content local messages = {} - local role = "" + local role = "user" local content = "" -- iterate over lines - local start_index = header_end + 1 + local start_index = header_end + 2 local end_index = #lines if params.range == 2 then start_index = math.max(start_index, params.line1) end_index = math.min(end_index, params.line2) end + if start_index > end_index then + start_index = end_index + end local agent = M.get_chat_agent() local agent_name = agent.name @@ -924,14 +927,13 @@ M.chat_respond = function(params) table.insert(messages, { role = role, content = content }) role = "assistant" content = "" - elseif role ~= "" then + else content = content .. "\n" .. line end end -- insert last message not handled in loop table.insert(messages, { role = role, content = content }) - -- replace first empty message with system prompt content = "" if headers.role and headers.role:match("%S") then content = headers.role @@ -941,7 +943,7 @@ M.chat_respond = function(params) if content:match("%S") then -- make it multiline again if it contains escaped newlines content = content:gsub("\\n", "\n") - messages[1] = { role = "system", content = content } + table.insert(messages, 1, { role = "system", content = content }) end -- strip whitespace from ends of content @@ -949,6 +951,12 @@ M.chat_respond = function(params) message.content = message.content:gsub("^%s*(.-)%s*$", "%1") end + messages = vim.tbl_filter(function(message) + return not (message.content == "" and message.role == "user") + end, messages) + + M.logger.debug("messages: " .. vim.inspect(messages), true) + -- write assistant prompt local last_content_line = M.helpers.last_content_line(buf) vim.api.nvim_buf_set_lines(buf, last_content_line, last_content_line, false, { "", agent_prefix .. agent_suffix, "" }) From 8e2624ae071ad71b25389ffcd7c084091f07fe89 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 22:24:26 +0200 Subject: [PATCH 14/33] feat: topic as yaml frontmatter header (otherwise obsidian would drop it on safe) --- lua/gp/defaults.lua | 4 ++-- lua/gp/init.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/gp/defaults.lua b/lua/gp/defaults.lua index abd33a23..0f54e312 100644 --- a/lua/gp/defaults.lua +++ b/lua/gp/defaults.lua @@ -15,7 +15,7 @@ M.code_system_prompt = "You are an AI working as a code editor.\n\n" M.chat_template = [[ --- -# topic: ? +topic: ? {{optional_headers}} # Write your queries after {{user_prefix}}. Use `{{respond_shortcut}}` or :{{cmd_prefix}}ChatRespond to generate a response. # Response generation can be terminated by using `{{stop_shortcut}}` or :{{cmd_prefix}}ChatStop command. @@ -28,7 +28,7 @@ M.chat_template = [[ M.short_chat_template = [[ --- -# topic: ? +topic: ? --- {{user_prefix}} diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 0a316b4b..dc0ffa48 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -1021,7 +1021,7 @@ M.chat_respond = function(params) local i = indices.topic M.helpers.undojoin(buf) - vim.api.nvim_buf_set_lines(buf, i, i + 1, false, { "# topic: " .. topic }) + vim.api.nvim_buf_set_lines(buf, i, i + 1, false, { "topic: " .. topic }) end) ) end From 5c408d6de10af6e6f921e491d9953cea5673d19b Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 22:27:07 +0200 Subject: [PATCH 15/33] chore: dummy GpDo command --- lua/gp/init.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index dc0ffa48..8b7cfb54 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -34,6 +34,13 @@ local M = { -- Module helper functions and variables -------------------------------------------------------------------------------- +M.cmd.Do = function(params) + M.logger.info("Dummy Do command called:\n" .. vim.inspect(params)) + local result = M.command_parser(params.args, {}, {}) + result.template = M.render.template(result.template, result.artifacts) + M.logger.info("Dummy Do command result:\n" .. vim.inspect(result)) +end + local agent_completion = function() local buf = vim.api.nvim_get_current_buf() local file_name = vim.api.nvim_buf_get_name(buf) From a54f5f16715ebdc47293fdf5c48416e52b5e1f90 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 26 Aug 2024 22:27:41 +0200 Subject: [PATCH 16/33] chore: todo note --- lua/gp/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 8b7cfb54..7c5a14c2 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -1692,6 +1692,7 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) finish = M._selection_last_line + ll - fl end + --TODO: this bugs out when GpEnew called from welcome screen -- select from first_line to last_line vim.api.nvim_win_set_cursor(0, { start + 1, 0 }) vim.api.nvim_command("normal! V") From f9c54ddcb5513031cad6e402342675f251ba23c6 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Wed, 28 Aug 2024 23:59:40 +0200 Subject: [PATCH 17/33] feat: GpChatHelp to toggle help comments --- after/ftplugin/gpchat.lua | 8 ++++ lua/gp/config.lua | 1 + lua/gp/defaults.lua | 13 ++++-- lua/gp/helper.lua | 21 ++++++++-- lua/gp/init.lua | 88 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 120 insertions(+), 11 deletions(-) diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index 4a177cf5..dd2b178d 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -37,6 +37,12 @@ local commands = { shortcut = M.config.chat_shortcut_new.shortcut, comment = "GPT prompt Chat New", }, + { + command = "ChatHelp", + modes = M.config.chat_shortcut_help.modes, + shortcut = M.config.chat_shortcut_help.shortcut, + comment = "GPT prompt Chat Help", + }, } for _, rc in ipairs(commands) do local cmd = M.config.cmd_prefix .. rc.command .. "" @@ -124,6 +130,8 @@ vim.api.nvim_create_autocmd({ "User" }, { M.logger.debug("gpchat: refreshing buffer " .. buf .. " " .. vim.json.encode(event)) + M.chat_help(buf) + vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) vim.api.nvim_buf_set_extmark(buf, ns_id, 0, 0, { diff --git a/lua/gp/config.lua b/lua/gp/config.lua index c80c3c56..10df9af6 100644 --- a/lua/gp/config.lua +++ b/lua/gp/config.lua @@ -311,6 +311,7 @@ local config = { chat_shortcut_delete = { modes = { "n", "i", "v", "x" }, shortcut = "d" }, chat_shortcut_stop = { modes = { "n", "i", "v", "x" }, shortcut = "s" }, chat_shortcut_new = { modes = { "n", "i", "v", "x" }, shortcut = "c" }, + chat_shortcut_help = { modes = { "n", "i", "v", "x" }, shortcut = "h" }, -- default search term when using :GpChatFinder chat_finder_pattern = "topic ", chat_finder_mappings = { diff --git a/lua/gp/defaults.lua b/lua/gp/defaults.lua index 0f54e312..7d549a4c 100644 --- a/lua/gp/defaults.lua +++ b/lua/gp/defaults.lua @@ -13,14 +13,19 @@ M.code_system_prompt = "You are an AI working as a code editor.\n\n" .. "Please AVOID COMMENTARY OUTSIDE OF THE SNIPPET RESPONSE.\n" .. "START AND END YOUR ANSWER WITH:\n\n```" -M.chat_template = [[ ---- -topic: ? -{{optional_headers}} +M.chat_help = [[ # Write your queries after {{user_prefix}}. Use `{{respond_shortcut}}` or :{{cmd_prefix}}ChatRespond to generate a response. # Response generation can be terminated by using `{{stop_shortcut}}` or :{{cmd_prefix}}ChatStop command. # Chats are saved automatically. To delete this chat, use `{{delete_shortcut}}` or :{{cmd_prefix}}ChatDelete. # Be cautious of very long chats. Start a fresh chat by using `{{new_shortcut}}` or :{{cmd_prefix}}ChatNew. +# See available macros by typing @ in the chat. Toggle this help by using `{{help_shortcut}}` or :{{cmd_prefix}}ChatHelp.]] + +M.chat_template = [[ +--- +topic: ? +{{optional_headers}} +]] .. M.chat_help .. [[ + --- {{user_prefix}} diff --git a/lua/gp/helper.lua b/lua/gp/helper.lua index 9a46f55d..fd8cca5e 100644 --- a/lua/gp/helper.lua +++ b/lua/gp/helper.lua @@ -63,6 +63,18 @@ _H.autocmd = function(events, buffers, callback, gid) end end +---@param callback function # callback to schedule +---@param depth number # depth of nested scheduling +_H.schedule = function(callback, depth) + logger.debug("scheduling callback with depth: " .. depth) + if depth <= 0 then + return callback() + end + return vim.schedule(function() + _H.schedule(callback, depth - 1) + end) +end + ---@param file_name string # name of the file for which to delete buffers _H.delete_buffer = function(file_name) -- iterate over buffer list and close all buffers with the same name @@ -294,24 +306,27 @@ _H.create_user_command = function(cmd_name, cmd_func, completion, desc) end ---@param lines string[] # array of lines ----@return table, table, number | nil # headers, indices, last header line +---@return table, table, number | nil, table # headers, indices, last header line, comments _H.parse_headers = function(lines) local headers = {} local indices = {} + local comments = {} for i, line in ipairs(lines) do if i > 1 and line:sub(1, 3) == "---" then - return headers, indices, i - 1 + return headers, indices, i - 1, comments end local key, value = line:match("^[-#%s]*(%w+):%s*(.*)%s*") if key ~= nil then headers[key] = value indices[key] = i - 1 + elseif line:match("^# ") then + comments[line] = i - 1 end end - return headers, indices, nil + return headers, indices, nil, comments end ---@param buf number # buffer number diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 7c5a14c2..6490f798 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -242,17 +242,34 @@ M.setup = function(opts) if M.hooks[cmd] == nil then M.helpers.create_user_command(M.config.cmd_prefix .. cmd, function(params) M.logger.debug("running command: " .. cmd) - M.refresh_state() + if cmd ~= "ChatHelp" then + M.refresh_state() + end M.cmd[cmd](params) end, completions[cmd]) end end + vim.filetype.add({ + extension = { + md = function(path, buf) + M.logger.debug("filetype markdown: " .. path .. " buf: " .. buf) + if not M.not_chat(buf, path) then + return "markdown.gpchat" + end + + if M.helpers.ends_with(path, ".gp.md") then + return "markdown.gpmd" + end + return "markdown" + end, + }, + }) + vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.md", callback = function(ev) - vim.defer_fn(function() - M.logger.debug("Markdown BufEnter: " .. ev.file) + M.helpers.schedule(function() local path = ev.file local buf = ev.buf local current_ft = vim.bo[buf].filetype @@ -262,7 +279,7 @@ M.setup = function(opts) vim.bo[buf].filetype = "markdown.gpmd" end vim.cmd("doautocmd User GpRefresh") - end, 100) + end, 3) end, }) @@ -313,6 +330,10 @@ M.refresh_state = function(update) M._state.last_chat = nil end + if M._state.show_chat_help == nil then + M._state.show_chat_help = true + end + for k, _ in pairs(M._state) do if M._state[k] ~= old_state[k] or M._state[k] ~= disk_state[k] then M.logger.debug( @@ -699,6 +720,7 @@ M.new_chat = function(params, toggle, system_prompt, agent) ["{{stop_shortcut}}"] = M.config.chat_shortcut_stop.shortcut, ["{{delete_shortcut}}"] = M.config.chat_shortcut_delete.shortcut, ["{{new_shortcut}}"] = M.config.chat_shortcut_new.shortcut, + ["{{help_shortcut}}"] = M.config.chat_shortcut_help.shortcut, }) -- escape underscores (for markdown) @@ -1041,6 +1063,64 @@ M.chat_respond = function(params) ) end +---@param buf number +M.chat_help = function(buf) + local file_name = vim.api.nvim_buf_get_name(buf) + M.logger.debug("ChatHelp: buffer: " .. buf .. " file: " .. file_name) + local reason = M.not_chat(buf, file_name) + if reason then + M.logger.debug("File " .. vim.inspect(file_name) .. " does not look like a chat file: " .. vim.inspect(reason)) + return + end + + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local _, _, header_end, comments = M.helpers.parse_headers(lines) + if header_end == nil then + M.logger.error("Error while parsing headers: --- not found. Check your chat template.") + return + end + + local help_template = M.render.template(M.defaults.chat_help, { + ["{{user_prefix}}"] = M.config.chat_user_prefix, + ["{{respond_shortcut}}"] = M.config.chat_shortcut_respond.shortcut, + ["{{cmd_prefix}}"] = M.config.cmd_prefix, + ["{{stop_shortcut}}"] = M.config.chat_shortcut_stop.shortcut, + ["{{delete_shortcut}}"] = M.config.chat_shortcut_delete.shortcut, + ["{{new_shortcut}}"] = M.config.chat_shortcut_new.shortcut, + ["{{help_shortcut}}"] = M.config.chat_shortcut_help.shortcut, + }) + + local help_lines = vim.split(help_template, "\n") + local help_map = {} + for _, line in ipairs(help_lines) do + help_map[line] = true + end + + local insert_help = true + local drop_lines = {} + for comment, index in pairs(comments) do + if help_map[comment] then + insert_help = false + table.insert(drop_lines, index) + end + end + + if M._state.show_chat_help and insert_help then + vim.api.nvim_buf_set_lines(buf, header_end, header_end, false, help_lines) + elseif not M._state.show_chat_help and not insert_help then + table.sort(drop_lines, function(a, b) + return a > b + end) + for _, index in ipairs(drop_lines) do + vim.api.nvim_buf_set_lines(buf, index, index + 1, false, {}) + end + end +end + +M.cmd.ChatHelp = function() + M.refresh_state({ show_chat_help = not M._state.show_chat_help }) +end + M.cmd.ChatRespond = function(params) if params.args == "" and vim.v.count == 0 then M.chat_respond(params) From da51f14461a23cbe6f4121335f83991ab4908555 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Thu, 29 Aug 2024 00:01:29 +0200 Subject: [PATCH 18/33] refactor: save_buffer helper --- after/ftplugin/gpchat.lua | 17 +++-------------- after/ftplugin/gpmd.lua | 2 +- lua/gp/helper.lua | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index dd2b178d..20699e62 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -76,18 +76,6 @@ if M.config.chat_conceal_model_params then vim.fn.matchadd("Conceal", [[^- role: .[^\\]*\zs\\.*\ze]], 10, -1, { conceal = "…" }) end -vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, { - buffer = buf, - callback = function(event) - if M.helpers.deleted_invalid_autocmd(buf, event) then - return - end - -- M.logger.debug("gpchat: entering buffer " .. buf .. " " .. vim.json.encode(event)) - - vim.cmd("doautocmd User GpRefresh") - end, -}) - vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "InsertLeave" }, { buffer = buf, callback = function(event) @@ -115,8 +103,7 @@ vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "InsertLeave" }, { M.helpers.delete_file(filename) end - -- M.logger.debug("gpchat: saving buffer " .. buf .. " " .. vim.json.encode(event)) - vim.api.nvim_command("silent! write") + M.helpers.save_buffer(buf, "gpchat TextChanged InsertLeave autocmd") end, }) vim.api.nvim_create_autocmd({ "User" }, { @@ -143,5 +130,7 @@ vim.api.nvim_create_autocmd({ "User" }, { }, hl_mode = "combine", }) + + M.helpers.save_buffer(buf, "gpchat User GpRefresh autocmd") end, }) diff --git a/after/ftplugin/gpmd.lua b/after/ftplugin/gpmd.lua index ccc9252b..97b1f75a 100644 --- a/after/ftplugin/gpmd.lua +++ b/after/ftplugin/gpmd.lua @@ -15,7 +15,7 @@ vim.api.nvim_create_autocmd({ "TextChanged", "InsertLeave" }, { return end M.logger.debug("gpmd: saving buffer " .. buf .. " " .. vim.json.encode(event)) - vim.api.nvim_command("silent! write") + M.helpers.save_buffer(buf, "gpmd TextChanged InsertLeave autocmd") end, }) diff --git a/lua/gp/helper.lua b/lua/gp/helper.lua index fd8cca5e..bace9c4e 100644 --- a/lua/gp/helper.lua +++ b/lua/gp/helper.lua @@ -340,4 +340,19 @@ _H.deleted_invalid_autocmd = function(buf, event) return false end +---@param buf number # buffer number +---@param caller string | nil # cause of the save +---@return boolean # true if successful, false otherwise +_H.save_buffer = function(buf, caller) + if not vim.api.nvim_buf_is_valid(buf) then + return false + end + local success = pcall(vim.api.nvim_buf_call, buf, function() + vim.cmd('silent! write') + end) + caller = caller or "unknown" + logger.debug("saving buffer: " .. buf .. " success: " .. vim.inspect(success) .. " caller: " .. vim.inspect(caller)) + return success +end + return _H From 921ea3fb9d352d7695bdb554c54de59e510e768a Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Fri, 30 Aug 2024 17:22:38 +0200 Subject: [PATCH 19/33] chore: better behavior of ChatHelp toggle --- after/ftplugin/gpchat.lua | 11 ++++-- lua/gp/init.lua | 83 ++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index 20699e62..3e28d9fe 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -117,16 +117,21 @@ vim.api.nvim_create_autocmd({ "User" }, { M.logger.debug("gpchat: refreshing buffer " .. buf .. " " .. vim.json.encode(event)) - M.chat_help(buf) + M.chat_header(buf) vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) + local msg = "Current Agent: [" .. M._state.chat_agent .. "]" + if not M._state.show_chat_help then + msg = "Toggle help: " .. M.config.chat_shortcut_help.shortcut .. " | " .. msg + end + vim.api.nvim_buf_set_extmark(buf, ns_id, 0, 0, { strict = false, - right_gravity = true, + right_gravity = false, virt_text_pos = "right_align", virt_text = { - { "Current Agent: [" .. M._state.chat_agent .. "]", "DiagnosticHint" }, + { msg, "DiagnosticHint" }, }, hl_mode = "combine", }) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 6490f798..79320c87 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -250,22 +250,6 @@ M.setup = function(opts) end end - vim.filetype.add({ - extension = { - md = function(path, buf) - M.logger.debug("filetype markdown: " .. path .. " buf: " .. buf) - if not M.not_chat(buf, path) then - return "markdown.gpchat" - end - - if M.helpers.ends_with(path, ".gp.md") then - return "markdown.gpmd" - end - return "markdown" - end, - }, - }) - vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.md", callback = function(ev) @@ -279,7 +263,7 @@ M.setup = function(opts) vim.bo[buf].filetype = "markdown.gpmd" end vim.cmd("doautocmd User GpRefresh") - end, 3) + end, 1) end, }) @@ -731,8 +715,11 @@ M.new_chat = function(params, toggle, system_prompt, agent) -- strip leading and trailing newlines template = template:gsub("^%s*(.-)%s*$", "%1") .. "\n" + local lines = vim.split(template, "\n") + lines = M.chat_header_lines(lines) + -- create chat file - vim.fn.writefile(vim.split(template, "\n"), filename) + vim.fn.writefile(lines, filename) local target = M.resolve_buf_target(params) local buf = M.open_buf(filename, target, M._toggle_kind.chat, toggle) @@ -1063,23 +1050,23 @@ M.chat_respond = function(params) ) end ----@param buf number -M.chat_help = function(buf) - local file_name = vim.api.nvim_buf_get_name(buf) - M.logger.debug("ChatHelp: buffer: " .. buf .. " file: " .. file_name) - local reason = M.not_chat(buf, file_name) - if reason then - M.logger.debug("File " .. vim.inspect(file_name) .. " does not look like a chat file: " .. vim.inspect(reason)) - return - end - - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) +---@param lines table # array of lines to process +---@return table # updated array of lines +---@return number # original header end +---@return number # new header end +M.chat_header_lines = function(lines) local _, _, header_end, comments = M.helpers.parse_headers(lines) if header_end == nil then M.logger.error("Error while parsing headers: --- not found. Check your chat template.") - return + return lines, 0, 0 + end + + if header_end + 1 >= #lines then + return lines, 0, 0 end + local header_lines = table.concat(vim.list_slice(lines, 0, header_end + 1), "\n") + local help_template = M.render.template(M.defaults.chat_help, { ["{{user_prefix}}"] = M.config.chat_user_prefix, ["{{respond_shortcut}}"] = M.config.chat_shortcut_respond.shortcut, @@ -1105,16 +1092,48 @@ M.chat_help = function(buf) end end + local new_header_end = header_end + if M._state.show_chat_help and insert_help then - vim.api.nvim_buf_set_lines(buf, header_end, header_end, false, help_lines) + for i = #help_lines, 1, -1 do + table.insert(lines, new_header_end + 1, help_lines[i]) + end + new_header_end = new_header_end + #help_lines elseif not M._state.show_chat_help and not insert_help then table.sort(drop_lines, function(a, b) return a > b end) for _, index in ipairs(drop_lines) do - vim.api.nvim_buf_set_lines(buf, index, index + 1, false, {}) + table.remove(lines, index + 1) + end + new_header_end = new_header_end - #drop_lines + end + + local j = 1 + while j <= new_header_end do + if lines[j]:match("^%s*$") then + table.remove(lines, j) + new_header_end = new_header_end - 1 + else + j = j + 1 end end + + return lines, header_end, new_header_end +end + +---@param buf number +M.chat_header = function(buf) + local file_name = vim.api.nvim_buf_get_name(buf) + M.logger.debug("ChatHelp: buffer: " .. buf .. " file: " .. file_name) + local reason = M.not_chat(buf, file_name) + if reason then + M.logger.debug("File " .. vim.inspect(file_name) .. " does not look like a chat file: " .. vim.inspect(reason)) + return + end + + local lines, old_header_end, header_end = M.chat_header_lines(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) + vim.api.nvim_buf_set_lines(buf, 0, old_header_end + 1, false, vim.list_slice(lines, 0, header_end + 1)) end M.cmd.ChatHelp = function() From 4a467354dd326c1e1bc0282db06bd54238b8a370 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Tue, 3 Sep 2024 14:28:24 +0200 Subject: [PATCH 20/33] feat: buffer state with context dir --- after/ftplugin/gpchat.lua | 12 ++++++++++++ lua/gp/buffer_state.lua | 39 +++++++++++++++++++++++++++++++++++++++ lua/gp/defaults.lua | 2 +- lua/gp/init.lua | 29 ++++++++++++++++++++++++++--- lua/gp/macro.lua | 5 ++++- 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 lua/gp/buffer_state.lua diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index 3e28d9fe..c09357cd 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -103,6 +103,18 @@ vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "InsertLeave" }, { M.helpers.delete_file(filename) end + local context_dir = headers["contextDir"] or "?" + local new_context_dir = nil + if context_dir ~= "?" and context_dir ~= "" then + local full_path = vim.fn.fnamemodify(context_dir, ":p") + if vim.fn.isdirectory(full_path) == 1 then + new_context_dir = vim.fn.resolve(full_path) + else + M.logger.warning("gpchat: contextDir " .. full_path .. " is not a directory") + end + end + M.buffer_state.set(buf, "context_dir", new_context_dir) + M.helpers.save_buffer(buf, "gpchat TextChanged InsertLeave autocmd") end, }) diff --git a/lua/gp/buffer_state.lua b/lua/gp/buffer_state.lua new file mode 100644 index 00000000..3f3b9607 --- /dev/null +++ b/lua/gp/buffer_state.lua @@ -0,0 +1,39 @@ +local logger = require("gp.logger") + +local M = {} + +local state = {} + +---@param buf number # buffer number +M.clear = function(buf) + logger.debug("buffer state[" .. buf .. "] clear: current state: " .. vim.inspect(state[buf])) + state[buf] = nil +end + +---@param buf number # buffer number +---@return table # buffer state +M.get = function(buf) + logger.debug("buffer state[" .. buf .. "]: get: " .. vim.inspect(state[buf])) + return state[buf] or {} +end + +---@param buf number # buffer number +---@param key string # key to get +---@return any # value of the key +M.get_key = function(buf, key) + local value = state[buf] and state[buf][key] or nil + logger.debug("buffer state[" .. buf .. "] get_key: key '" .. key .. "' value: " .. vim.inspect(value)) + return value +end + +---@param buf number # buffer number +---@param key string # key to set +---@param value any # value to set +M.set = function(buf, key, value) + logger.debug("buffer state[" .. buf .. "]: set: key '" .. key .. "' to value: " .. vim.inspect(value)) + state[buf] = state[buf] or {} + state[buf][key] = value + logger.debug("buffer state[" .. buf .. "]: set: updated state: " .. vim.inspect(state[buf])) +end + +return M diff --git a/lua/gp/defaults.lua b/lua/gp/defaults.lua index 7d549a4c..aaf3f702 100644 --- a/lua/gp/defaults.lua +++ b/lua/gp/defaults.lua @@ -18,7 +18,7 @@ M.chat_help = [[ # Response generation can be terminated by using `{{stop_shortcut}}` or :{{cmd_prefix}}ChatStop command. # Chats are saved automatically. To delete this chat, use `{{delete_shortcut}}` or :{{cmd_prefix}}ChatDelete. # Be cautious of very long chats. Start a fresh chat by using `{{new_shortcut}}` or :{{cmd_prefix}}ChatNew. -# See available macros by typing @ in the chat. Toggle this help by using `{{help_shortcut}}` or :{{cmd_prefix}}ChatHelp.]] +# Add context macros by typing @ in the chat. Toggle this help by `{{help_shortcut}}` or :{{cmd_prefix}}ChatHelp.]] M.chat_template = [[ --- diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 79320c87..f43f3722 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -28,6 +28,7 @@ local M = { vault = require("gp.vault"), -- handles secrets whisper = require("gp.whisper"), -- whisper module macro = require("gp.macro"), -- builder for macro completion + buffer_state = require("gp.buffer_state"), -- buffer state module } -------------------------------------------------------------------------------- @@ -267,6 +268,23 @@ M.setup = function(opts) end, }) + vim.api.nvim_create_autocmd("BufEnter", { + callback = function(ev) + local buf = ev.buf + local context_dir = M.buffer_state.get_key(buf, "context_dir") + context_dir = context_dir or M.helpers.find_git_root() + if context_dir == "" then + context_dir = vim.fn.getcwd() + end + + local full_path = vim.fn.fnamemodify(context_dir, ":p") + if vim.fn.isdirectory(full_path) == 1 then + full_path = vim.fn.resolve(full_path) + M.buffer_state.set(buf, "context_dir", full_path) + end + end, + }) + if vim.fn.executable("curl") == 0 then M.logger.error("curl is not installed, run :checkhealth gp") end @@ -695,9 +713,16 @@ M.new_chat = function(params, toggle, system_prompt, agent) system_prompt = "" end + local context_dir = M.buffer_state.get_key(vim.api.nvim_get_current_buf(), "context_dir") + context_dir = context_dir or M.helpers.find_git_root() + if context_dir == "" then + context_dir = vim.fn.getcwd() + end + context_dir = "contextDir: " .. context_dir .. "\n" + local template = M.render.template(M.config.chat_template or require("gp.defaults").chat_template, { ["{{filename}}"] = string.match(filename, "([^/]+)$"), - ["{{optional_headers}}"] = model .. provider .. system_prompt, + ["{{optional_headers}}"] = model .. provider .. system_prompt .. context_dir, ["{{user_prefix}}"] = M.config.chat_user_prefix, ["{{respond_shortcut}}"] = M.config.chat_shortcut_respond.shortcut, ["{{cmd_prefix}}"] = M.config.cmd_prefix, @@ -1065,8 +1090,6 @@ M.chat_header_lines = function(lines) return lines, 0, 0 end - local header_lines = table.concat(vim.list_slice(lines, 0, header_end + 1), "\n") - local help_template = M.render.template(M.defaults.chat_help, { ["{{user_prefix}}"] = M.config.chat_user_prefix, ["{{respond_shortcut}}"] = M.config.chat_shortcut_respond.shortcut, diff --git a/lua/gp/macro.lua b/lua/gp/macro.lua index c779021d..d0bd1579 100644 --- a/lua/gp/macro.lua +++ b/lua/gp/macro.lua @@ -1,10 +1,12 @@ local logger = require("gp.logger") +local buffer_state = require("gp.buffer_state") ---@class gp.Macro_cmd_params ---@field arg_lead string ---@field cmd_line string ---@field cursor_pos number ---@field cropped_line string +---@field state table ---@class gp.Macro_parser_result ---@field template string @@ -65,7 +67,7 @@ M.build_parser = function(macros) local result = { template = " " .. template .. " ", artifacts = artifacts or {}, - state = state or {}, + state = state or buffer_state.get(vim.api.nvim_get_current_buf()), } logger.debug("macro parser input: " .. vim.inspect(result)) @@ -103,6 +105,7 @@ M.build_completion = function(macros, raw) cmd_line = cmd_line, cursor_pos = cursor_pos, cropped_line = cropped_line, + state = buffer_state.get(vim.api.nvim_get_current_buf()), } cropped_line = " " .. cropped_line From 5613541a1863fcec4a964d272642e3ded648312c Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Tue, 3 Sep 2024 14:30:34 +0200 Subject: [PATCH 21/33] refactor: small optimizations --- lua/gp/init.lua | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index f43f3722..9da9f749 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -179,15 +179,10 @@ M.setup = function(opts) table.sort(M._chat_agents) table.sort(M._command_agents) - M.refresh_state() - - if M.config.default_command_agent then - M.refresh_state({ command_agent = M.config.default_command_agent }) - end - - if M.config.default_chat_agent then - M.refresh_state({ chat_agent = M.config.default_chat_agent }) - end + M.refresh_state({ + command_agent = M.config.default_command_agent, + chat_agent = M.config.default_chat_agent, + }) -- register user commands for hook, _ in pairs(M.hooks) do @@ -238,14 +233,18 @@ M.setup = function(opts) Tabnew = ft_completion, } + local updates = { + ChatHelp = function() + return { show_chat_help = not M._state.show_chat_help } + end, + } + -- register default commands for cmd, _ in pairs(M.cmd) do if M.hooks[cmd] == nil then M.helpers.create_user_command(M.config.cmd_prefix .. cmd, function(params) M.logger.debug("running command: " .. cmd) - if cmd ~= "ChatHelp" then - M.refresh_state() - end + M.refresh_state((updates[cmd] or function() end)()) M.cmd[cmd](params) end, completions[cmd]) end @@ -255,15 +254,20 @@ M.setup = function(opts) pattern = "*.md", callback = function(ev) M.helpers.schedule(function() - local path = ev.file local buf = ev.buf local current_ft = vim.bo[buf].filetype - if not M.not_chat(buf, path) and current_ft ~= "markdown.gpchat" then - vim.bo[buf].filetype = "markdown.gpchat" - elseif M.helpers.ends_with(path, ".gp.md") and current_ft ~= "markdown.gpmd" then - vim.bo[buf].filetype = "markdown.gpmd" + + if current_ft == "markdown.gpchat" then + vim.cmd("doautocmd User GpRefresh") + elseif current_ft ~= "markdown.gpmd" then + local path = ev.file + if M.helpers.ends_with(path, ".gp.md") then + vim.bo[buf].filetype = "markdown.gpmd" + elseif M.not_chat(buf, path) == nil then + vim.bo[buf].filetype = "markdown.gpchat" + vim.cmd("doautocmd User GpRefresh") + end end - vim.cmd("doautocmd User GpRefresh") end, 1) end, }) @@ -1159,9 +1163,7 @@ M.chat_header = function(buf) vim.api.nvim_buf_set_lines(buf, 0, old_header_end + 1, false, vim.list_slice(lines, 0, header_end + 1)) end -M.cmd.ChatHelp = function() - M.refresh_state({ show_chat_help = not M._state.show_chat_help }) -end +M.cmd.ChatHelp = function() end M.cmd.ChatRespond = function(params) if params.args == "" and vim.v.count == 0 then From 19f1c035ac99bd9fd634a1c7331e928603e954d8 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sun, 8 Sep 2024 10:08:53 +0200 Subject: [PATCH 22/33] feat: agent macro & rm ui.input for Prompt cmds --- lua/gp/init.lua | 65 ++++++++++++++++++++++++++++++++++++----- lua/gp/macros/agent.lua | 43 +++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 lua/gp/macros/agent.lua diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 9da9f749..7d5bf777 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -200,11 +200,17 @@ M.setup = function(opts) local ft_completion = M.macro.build_completion({ require("gp.macros.target_filetype"), + require("gp.macros.agent"), + }) + + local base_completion = M.macro.build_completion({ + require("gp.macros.agent"), }) M.logger.debug("ft_completion done") local do_completion = M.macro.build_completion({ + require("gp.macros.agent"), require("gp.macros.target"), require("gp.macros.target_filetype"), require("gp.macros.target_filename"), @@ -213,6 +219,7 @@ M.setup = function(opts) M.logger.debug("do_completion done") M.command_parser = M.macro.build_parser({ + require("gp.macros.agent"), require("gp.macros.target"), require("gp.macros.target_filetype"), require("gp.macros.target_filename"), @@ -231,6 +238,10 @@ M.setup = function(opts) New = ft_completion, Vnew = ft_completion, Tabnew = ft_completion, + Rewrite = base_completion, + Prepend = base_completion, + Append = base_completion, + Popup = base_completion, } local updates = { @@ -286,6 +297,9 @@ M.setup = function(opts) full_path = vim.fn.resolve(full_path) M.buffer_state.set(buf, "context_dir", full_path) end + + local filename = vim.api.nvim_buf_get_name(buf) + M.buffer_state.set(buf, "is_chat", M.not_chat(buf, filename) == nil) end, }) @@ -396,6 +410,32 @@ M.Target = { end, } +---@param target number | table # target to get name for +---@return string # name of the target +---@return string | nil # filetype of the target, if applicable +M.get_target_name = function(target) + local names = {} + for name, value in pairs(M.Target) do + if type(value) == "number" then + names[value] = name + elseif type(value) == "function" then + local result = value() + if type(result) == "table" and result.type then + names[result.type] = name + end + end + end + + if type(target) == "number" then + return names[target] or "unknown" + elseif type(target) == "table" and target.type then + return names[target.type] or "unknown", target.filetype + end + + M.logger.error("Invalid target type: " .. vim.inspect(target)) + return "unknown" +end + -- creates prompt commands for each target M.prepare_commands = function() for name, target in pairs(M.Target) do @@ -1828,13 +1868,15 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) local filetype = M.helpers.get_filetype(buf) local filename = vim.api.nvim_buf_get_name(buf) - local state = {} + local state = M.buffer_state.get(buf) local response = M.command_parser(command, {}, state) if response then command = M.render.template(response.template, response.artifacts) state = response.state end + agent = state.agent or agent + local sys_prompt = M.render.prompt_template(agent.system_prompt, command, selection, filetype, filename) sys_prompt = sys_prompt or "" table.insert(messages, { role = "system", content = sys_prompt }) @@ -1964,13 +2006,20 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) return end - -- if prompt is provided, ask the user to enter the command - vim.ui.input({ prompt = prompt, default = whisper }, function(input) - if not input or input == "" then - return - end - cb(input) - end) + -- old shortcuts might produce stuff like `:GpRewrite` this + -- used to be handled by vim.ui.input, which has trouble with completion + local command = ":" .. start_line .. "," .. end_line .. "Gp" + local targetName, filetype = M.get_target_name(target) + targetName = targetName:gsub("^%l", string.upper) + command = command .. targetName + command = command .. " @agent " .. agent.name + filetype = filetype and " @target_filetype " .. filetype or "" + command = command .. filetype + whisper = whisper and " " .. whisper or "" + command = command .. whisper + command = command .. " " + + vim.api.nvim_feedkeys(command, "n", false) end) end diff --git a/lua/gp/macros/agent.lua b/lua/gp/macros/agent.lua new file mode 100644 index 00000000..18a6c759 --- /dev/null +++ b/lua/gp/macros/agent.lua @@ -0,0 +1,43 @@ +local macro = require("gp.macro") +local gp = require("gp") + +local M = {} + +---@type gp.Macro +M = { + name = "agent", + description = "handles agent selection for commands", + default = "", + max_occurrences = 1, + + triggered = function(params) + return params.cropped_line:match("@agent%s+%S*$") + end, + + completion = function(params) + if params.state.is_chat then + return gp._chat_agents + end + return gp._command_agents + end, + + parser = function(result) + local template = result.template + local s, e, value = template:find("@agent%s+(%S+)") + if not value then + return result + end + + local placeholder = macro.generate_placeholder(M.name, value) + result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1) + if result.state.is_chat then + result.state[M.name] = gp.get_chat_agent(value) + else + result.state[M.name] = gp.get_command_agent(value) + end + result.artifacts[placeholder] = "" + return result + end, +} + +return M From 449052b6c02113db066598db9273aa8d5172dd38 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sun, 8 Sep 2024 21:19:19 +0200 Subject: [PATCH 23/33] feat: context_file macro --- lua/gp/config.lua | 2 ++ lua/gp/init.lua | 4 +++ lua/gp/macros/context_file.lua | 66 ++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 lua/gp/macros/context_file.lua diff --git a/lua/gp/config.lua b/lua/gp/config.lua index 10df9af6..5b9c1e5a 100644 --- a/lua/gp/config.lua +++ b/lua/gp/config.lua @@ -370,6 +370,8 @@ local config = { .. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}" .. "\n\nRespond exclusively with the snippet that should be prepended before the selection above.", template_command = "{{command}}", + template_context_file = "\n\nHere is a file {{filename}} for additional context:" + .. "\n\n```\n{{content}}\n```\n\n", -- https://platform.openai.com/docs/guides/speech-to-text/quickstart -- Whisper costs $0.006 / minute (rounded to the nearest second) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 7d5bf777..40f2a925 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -201,10 +201,12 @@ M.setup = function(opts) local ft_completion = M.macro.build_completion({ require("gp.macros.target_filetype"), require("gp.macros.agent"), + require("gp.macros.context_file"), }) local base_completion = M.macro.build_completion({ require("gp.macros.agent"), + require("gp.macros.context_file"), }) M.logger.debug("ft_completion done") @@ -214,6 +216,7 @@ M.setup = function(opts) require("gp.macros.target"), require("gp.macros.target_filetype"), require("gp.macros.target_filename"), + require("gp.macros.context_file"), }) M.logger.debug("do_completion done") @@ -223,6 +226,7 @@ M.setup = function(opts) require("gp.macros.target"), require("gp.macros.target_filetype"), require("gp.macros.target_filename"), + require("gp.macros.context_file"), }) M.logger.debug("command_parser done") diff --git a/lua/gp/macros/context_file.lua b/lua/gp/macros/context_file.lua new file mode 100644 index 00000000..2fdd457e --- /dev/null +++ b/lua/gp/macros/context_file.lua @@ -0,0 +1,66 @@ +local macro = require("gp.macro") +local gp = require("gp") + +local M = {} + +---@type gp.Macro +M = { + name = "context_file`", + description = "replaces the macro with the content of the specified file", + default = nil, + max_occurrences = 100, + + triggered = function(params) + local cropped_line = params.cropped_line + return cropped_line:match("@context_file`[^`]*$") + end, + + completion = function(params) + local root_dir = params.state.context_dir or vim.fn.getcwd() + local files = vim.fn.globpath(root_dir, "**", false, true) + local root_dir_length = #root_dir + 2 + files = vim.tbl_map(function(file) + return file:sub(root_dir_length) .. " `" + end, files) + return files + end, + + parser = function(result) + local template = result.template + local macro_pattern = "@context_file`([^`]*)`" + + for _ = 1, M.max_occurrences do + local s, e, value = template:find(macro_pattern) + if not value then + break + end + + value = value:match("^%s*(.-)%s*$") + local placeholder = macro.generate_placeholder(M.name, value) + + local full_path = value + if vim.fn.fnamemodify(full_path, ":p") ~= value then + full_path = vim.fn.fnamemodify(result.state.context_dir .. "/" .. value, ":p") + end + + if vim.fn.filereadable(full_path) == 0 then + result.artifacts[placeholder] = "" + gp.logger.error("Context file not found: " .. full_path) + else + local content = table.concat(vim.fn.readfile(full_path), "\n") + content = gp.render.template(gp.config.template_context_file, { + ["{{content}}"] = content, + ["{{filename}}"] = full_path, + }) + result.artifacts[placeholder] = content + end + + template = template:sub(1, s - 1) .. placeholder .. template:sub(e + 1) + end + + result.template = template + return result + end, +} + +return M From bcc93df80185d0dff818e177ce6d8f847193529b Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sun, 8 Sep 2024 21:23:42 +0200 Subject: [PATCH 24/33] feat: chat macro support - starting with @context_file (issue: #206) --- after/ftplugin/gpchat.lua | 28 ++++++++++++ lua/gp/init.lua | 11 ++++- lua/gp/macro.lua | 91 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index c09357cd..e5188906 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -151,3 +151,31 @@ vim.api.nvim_create_autocmd({ "User" }, { M.helpers.save_buffer(buf, "gpchat User GpRefresh autocmd") end, }) + +local has_cmp, cmp = pcall(require, "cmp") +if not has_cmp then + M.logger.debug("gpchat: cmp not found, skipping cmp setup") + return +end + +M.macro.build_cmp_source("gpchat", { + require("gp.macros.context_file"), +}) + +local sources = { + { name = "gpchat" }, +} +for _, source in pairs(cmp.get_config().sources) do + if source.name ~= "gpchat" and source.name ~= "buffer" then + table.insert(sources, source) + end +end + +M.logger.debug("gpchat: cmp sources " .. vim.inspect(sources)) + +cmp.setup.buffer({ + -- keyword_length = 1, + max_item_count = 100, + completion = { autocomplete = { require("cmp.types").cmp.TriggerEvent.TextChanged } }, + sources = sources, +}) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 40f2a925..cb5157bd 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -229,7 +229,9 @@ M.setup = function(opts) require("gp.macros.context_file"), }) - M.logger.debug("command_parser done") + M.chat_parser = M.macro.build_parser({ + require("gp.macros.context_file"), + }) local completions = { ChatNew = { "popup", "split", "vsplit", "tabnew" }, @@ -1035,8 +1037,13 @@ M.chat_respond = function(params) table.insert(messages, 1, { role = "system", content = content }) end - -- strip whitespace from ends of content + local state = M.buffer_state.get(buf) for _, message in ipairs(messages) do + local response = M.chat_parser(message.content, {}, state) + if response then + message.content = M.render.template(response.template, response.artifacts) + state = response.state + end message.content = message.content:gsub("^%s*(.-)%s*$", "%1") end diff --git a/lua/gp/macro.lua b/lua/gp/macro.lua index d0bd1579..48a569c2 100644 --- a/lua/gp/macro.lua +++ b/lua/gp/macro.lua @@ -153,4 +153,95 @@ M.build_completion = function(macros, raw) return completion end +local registered_cmp_sources = {} +M.build_cmp_source = function(name, macros) + if registered_cmp_sources[name] then + logger.debug("cmp source " .. name .. " already registered") + return nil + end + local source = {} + + source.new = function() + return setmetatable({}, { __index = source }) + end + + source.get_trigger_characters = function() + return { "@", " " } + end + + local completion = M.build_completion(macros, true) + + source.complete = function(self, params, callback) + local ctx = params.context + local suggestions, triggered = completion(ctx.cursor_before_line:match("%S*$"), ctx.cursor_line, ctx.cursor.col) + + if not triggered and not ctx.cursor_before_line:match("%s*@%S*$") then + suggestions = {} + end + + logger.debug("macro completion suggestions: " .. vim.inspect(suggestions)) + + local items = {} + for _, suggestion in ipairs(suggestions) do + table.insert(items, { + label = suggestion, + kind = require("cmp").lsp.CompletionItemKind.Keyword, + documentation = name, + }) + end + logger.debug("macro cmp complete output: " .. vim.inspect(items)) + + callback(items) + end + + local has_cmp, cmp = pcall(require, "cmp") + if not has_cmp then + logger.warning("cmp not found, skipping cmp source registration") + return source + end + + cmp.register_source(name, source) + registered_cmp_sources[name] = true + + if true then + return source + end + + cmp.event:on("complete_done", function(event) + if not event or not event.entry or event.entry.source.name ~= name then + return + end + local ctx = event.entry.source.context + local suggestions, triggered = completion(ctx.cursor_before_line:match("%S*$"), ctx.cursor_line, ctx.cursor.col) + logger.debug( + "macro cmp complete_done suggestions: " .. vim.inspect(suggestions) .. " triggered: " .. vim.inspect(triggered) + ) + if not suggestions or not triggered then + return + end + + vim.schedule(function() + -- insert a space if not already present at the cursor + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local line = vim.api.nvim_get_current_line() + logger.debug( + "macro cmp complete_done cursor_col: " + .. cursor_col + .. " line: " + .. line + .. " char: " + .. line:sub(cursor_col, cursor_col) + ) + if line:sub(cursor_col, cursor_col) ~= " " then + vim.api.nvim_put({ " " }, "c", false, true) + end + vim.schedule(function() + cmp.complete(suggestions) + end) + end) + end) + + return source +end + return M From 9de19209f4ee621637535b27c688d5f5c3769250 Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 9 Sep 2024 00:44:03 +0200 Subject: [PATCH 25/33] refactor: startup optimization --- lua/gp/dispatcher.lua | 27 +++++++++++++++++++++++---- lua/gp/logger.lua | 7 +++++-- lua/gp/macros/target_filetype.lua | 5 ++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lua/gp/dispatcher.lua b/lua/gp/dispatcher.lua index 26fc76a1..c616798e 100644 --- a/lua/gp/dispatcher.lua +++ b/lua/gp/dispatcher.lua @@ -2,6 +2,8 @@ -- Dispatcher handles the communication between the plugin and LLM providers. -------------------------------------------------------------------------------- +local uv = vim.uv or vim.loop + local logger = require("gp.logger") local tasker = require("gp.tasker") local vault = require("gp.vault") @@ -18,7 +20,7 @@ local D = { ---@param opts table # user config D.setup = function(opts) - logger.debug("dispatcher setup started\n" .. vim.inspect(opts)) + logger.debug("dispatcher: setup started\n" .. vim.inspect(opts)) D.config.curl_params = opts.curl_params or default_config.curl_params @@ -52,9 +54,26 @@ D.setup = function(opts) D.query_dir = helpers.prepare_dir(D.query_dir, "query store") - local files = vim.fn.glob(D.query_dir .. "/*.json", false, true) + local files = {} + local handle = uv.fs_scandir(D.query_dir) + if handle then + local name, type + while true do + name, type = uv.fs_scandir_next(handle) + if not name then + break + end + local path = D.query_dir .. "/" .. name + type = type or uv.fs_stat(path).type + if type == "file" and name:match("%.json$") then + table.insert(files, path) + end + end + end + + logger.debug("dispatcher: query files: " .. #files) if #files > 200 then - logger.debug("too many query files, truncating cache") + logger.debug("dispatcher: too many query files, truncating cache") table.sort(files, function(a, b) return a > b end) @@ -63,7 +82,7 @@ D.setup = function(opts) end end - logger.debug("dispatcher setup finished\n" .. vim.inspect(D)) + logger.debug("dispatcher: setup finished\n" .. vim.inspect(D)) end ---@param messages table diff --git a/lua/gp/logger.lua b/lua/gp/logger.lua index 82509411..bfac06fc 100644 --- a/lua/gp/logger.lua +++ b/lua/gp/logger.lua @@ -33,10 +33,13 @@ M.setup = function(path, sensitive) if vim.fn.isdirectory(dir) == 0 then vim.fn.mkdir(dir, "p") end + + local file_stats = uv.fs_stat(path) + M.debug("Log file " .. file .. " has " .. (file_stats and file_stats.size or 0) .. " bytes") + file = path - -- truncate log file if it's too big - if uv.fs_stat(file) then + if file_stats and file_stats.size > 5 * 1024 * 1024 then local content = {} for line in io.lines(file) do table.insert(content, line) diff --git a/lua/gp/macros/target_filetype.lua b/lua/gp/macros/target_filetype.lua index 21eb2f8a..91be8bcf 100644 --- a/lua/gp/macros/target_filetype.lua +++ b/lua/gp/macros/target_filetype.lua @@ -1,6 +1,6 @@ local macro = require("gp.macro") -local values = vim.fn.getcompletion("", "filetype") +local values = nil local M = {} @@ -16,6 +16,9 @@ M = { end, completion = function(params) + if not values then + values = vim.fn.getcompletion("", "filetype") + end return values end, From 1d47d0fd6c3fd537b871ede2a0b52b89337f87aa Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Sat, 14 Sep 2024 10:19:24 +0200 Subject: [PATCH 26/33] chore: working target_filename macro --- lua/gp/init.lua | 18 ++++++++++++------ lua/gp/macros/target_filename.lua | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lua/gp/init.lua b/lua/gp/init.lua index cb5157bd..073fc351 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -199,9 +199,10 @@ M.setup = function(opts) M.logger.debug("hook setup done") local ft_completion = M.macro.build_completion({ - require("gp.macros.target_filetype"), require("gp.macros.agent"), require("gp.macros.context_file"), + require("gp.macros.target_filename"), + require("gp.macros.target_filetype"), }) local base_completion = M.macro.build_completion({ @@ -213,20 +214,20 @@ M.setup = function(opts) local do_completion = M.macro.build_completion({ require("gp.macros.agent"), + require("gp.macros.context_file"), require("gp.macros.target"), - require("gp.macros.target_filetype"), require("gp.macros.target_filename"), - require("gp.macros.context_file"), + require("gp.macros.target_filetype"), }) M.logger.debug("do_completion done") M.command_parser = M.macro.build_parser({ require("gp.macros.agent"), + require("gp.macros.context_file"), require("gp.macros.target"), - require("gp.macros.target_filetype"), require("gp.macros.target_filename"), - require("gp.macros.context_file"), + require("gp.macros.target_filetype"), }) M.chat_parser = M.macro.build_parser({ @@ -1969,7 +1970,7 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) win = vim.api.nvim_get_current_win() end - buf = vim.api.nvim_create_buf(true, true) + buf = vim.api.nvim_create_buf(true, false) vim.api.nvim_set_current_buf(buf) local group = M.helpers.create_augroup("GpScratchSave" .. M.helpers.uuid(), { clear = true }) @@ -1986,6 +1987,11 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) local ft = state.target_filetype or target.filetype or filetype vim.api.nvim_set_option_value("filetype", ft, { buf = buf }) + local name = state.target_filename + if name then + vim.api.nvim_buf_set_name(buf, name) + M.helpers.save_buffer(buf, "Prompt created buffer") + end handler = M.dispatcher.create_handler(buf, win, 0, false, "", cursor) end diff --git a/lua/gp/macros/target_filename.lua b/lua/gp/macros/target_filename.lua index 47becec1..e5d6d20f 100644 --- a/lua/gp/macros/target_filename.lua +++ b/lua/gp/macros/target_filename.lua @@ -1,4 +1,5 @@ local macro = require("gp.macro") +local gp = require("gp") local M = {} @@ -15,11 +16,11 @@ M = { end, completion = function(params) - -- TODO state.root_dir ? - local files = vim.fn.glob("**", true, true) - -- local files = vim.fn.getcompletion("", "file") + local root_dir = params.state.context_dir or vim.fn.getcwd() + local files = vim.fn.globpath(root_dir, "**", false, true) + local root_dir_length = #root_dir + 2 files = vim.tbl_map(function(file) - return file .. " `" + return file:sub(root_dir_length) .. " `" end, files) return files end, @@ -34,9 +35,14 @@ M = { value = value:match("^%s*(.-)%s*$") local placeholder = macro.generate_placeholder(M.name, value) - result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1) - result.state[M.name] = value + local full_path = value + if vim.fn.fnamemodify(full_path, ":p") ~= value then + full_path = vim.fn.fnamemodify(result.state.context_dir .. "/" .. value, ":p") + end + result.artifacts[placeholder] = "" + result.template = template:sub(1, s - 1) .. placeholder .. template:sub(e + 1) + result.state[M.name:sub(1, -2)] = full_path return result end, } From 0ea6f7e97721c9a39acb82da40c833496c9b517f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Sep 2024 09:14:11 +0000 Subject: [PATCH 27/33] chore: update README and auto-generate vimdoc --- README.md | 2 +- doc/gp.nvim.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 84aa201a..31edb0a5 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Voice commands (`:GpWhisper*`) depend on `SoX` (Sound eXchange) to handle audio Below is a linked snippet with the default values, but I suggest starting with minimal config possible (just `openai_api_key` if you don't have `OPENAI_API_KEY` env set up). Defaults change over time to improve things, options might get deprecated and so on - it's better to change only things where the default doesn't fit your needs. -https://github.com/Robitx/gp.nvim/blob/a88225e90f22acdbc943079f8fa5912e8c101db8/lua/gp/config.lua#L10-L607 +https://github.com/Robitx/gp.nvim/blob/449052b6c02113db066598db9273aa8d5172dd38/lua/gp/config.lua#L10-L610 # Usage diff --git a/doc/gp.nvim.txt b/doc/gp.nvim.txt index 81d7c6ea..f8078ca0 100644 --- a/doc/gp.nvim.txt +++ b/doc/gp.nvim.txt @@ -252,7 +252,7 @@ options might get deprecated and so on - it’s better to change only things where the default doesn’t fit your needs. -https://github.com/Robitx/gp.nvim/blob/a88225e90f22acdbc943079f8fa5912e8c101db8/lua/gp/config.lua#L10-L607 +https://github.com/Robitx/gp.nvim/blob/449052b6c02113db066598db9273aa8d5172dd38/lua/gp/config.lua#L10-L610 ============================================================================== From b0c23ee48469d708a1699b63129849440aa9471c Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Mon, 23 Sep 2024 14:31:40 +0200 Subject: [PATCH 28/33] feat: adding more macros - context_file => with_file - with_current_file - with_repo_instructions --- after/ftplugin/gpchat.lua | 3 +- lua/gp/init.lua | 45 +++++--------- lua/gp/macros/with_current_buf.lua | 48 ++++++++++++++ .../{context_file.lua => with_file.lua} | 6 +- lua/gp/macros/with_repo_instructions.lua | 62 +++++++++++++++++++ 5 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 lua/gp/macros/with_current_buf.lua rename lua/gp/macros/{context_file.lua => with_file.lua} (92%) create mode 100644 lua/gp/macros/with_repo_instructions.lua diff --git a/after/ftplugin/gpchat.lua b/after/ftplugin/gpchat.lua index e5188906..3ad2a900 100644 --- a/after/ftplugin/gpchat.lua +++ b/after/ftplugin/gpchat.lua @@ -159,7 +159,8 @@ if not has_cmp then end M.macro.build_cmp_source("gpchat", { - require("gp.macros.context_file"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) local sources = { diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 073fc351..c6b066ed 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -200,38 +200,47 @@ M.setup = function(opts) local ft_completion = M.macro.build_completion({ require("gp.macros.agent"), - require("gp.macros.context_file"), require("gp.macros.target_filename"), require("gp.macros.target_filetype"), + require("gp.macros.with_current_buf"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) local base_completion = M.macro.build_completion({ require("gp.macros.agent"), - require("gp.macros.context_file"), + require("gp.macros.with_current_buf"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) M.logger.debug("ft_completion done") local do_completion = M.macro.build_completion({ require("gp.macros.agent"), - require("gp.macros.context_file"), require("gp.macros.target"), require("gp.macros.target_filename"), require("gp.macros.target_filetype"), + require("gp.macros.with_current_buf"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) M.logger.debug("do_completion done") M.command_parser = M.macro.build_parser({ require("gp.macros.agent"), - require("gp.macros.context_file"), require("gp.macros.target"), require("gp.macros.target_filename"), require("gp.macros.target_filetype"), + require("gp.macros.with_current_buf"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) M.chat_parser = M.macro.build_parser({ - require("gp.macros.context_file"), + require("gp.macros.with_file"), + require("gp.macros.with_repo_instructions"), }) local completions = { @@ -1665,25 +1674,6 @@ M.get_chat_agent = function(name) } end --- tries to find an .gp.md file in the root of current git repo ----@return string # returns instructions from the .gp.md file -M.repo_instructions = function() - local git_root = M.helpers.find_git_root() - - if git_root == "" then - return "" - end - - local instruct_file = git_root .. "/.gp.md" - - if vim.fn.filereadable(instruct_file) == 0 then - return "" - end - - local lines = vim.fn.readfile(instruct_file) - return table.concat(lines, "\n") -end - M.cmd.Context = function(params) M._toggle_close(M._toggle_kind.popup) -- if there is no selection, try to close context toggle @@ -1893,11 +1883,6 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) sys_prompt = sys_prompt or "" table.insert(messages, { role = "system", content = sys_prompt }) - local repo_instructions = M.repo_instructions() - if repo_instructions ~= "" then - table.insert(messages, { role = "system", content = repo_instructions }) - end - local user_prompt = M.render.prompt_template(template, command, selection, filetype, filename) table.insert(messages, { role = "user", content = user_prompt }) @@ -2034,6 +2019,8 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback) command = command .. filetype whisper = whisper and " " .. whisper or "" command = command .. whisper + command = command .. " @with_repo_instructions" + command = command .. " @with_current_buf" command = command .. " " vim.api.nvim_feedkeys(command, "n", false) diff --git a/lua/gp/macros/with_current_buf.lua b/lua/gp/macros/with_current_buf.lua new file mode 100644 index 00000000..7f1424b4 --- /dev/null +++ b/lua/gp/macros/with_current_buf.lua @@ -0,0 +1,48 @@ +local macro = require("gp.macro") +local gp = require("gp") + +local M = {} + +---@type gp.Macro +M = { + name = "with_current_buf", + description = "replaces the macro with the content of the current file", + default = nil, + max_occurrences = 1, + + triggered = function(_) + return false + end, + + completion = function(_) + return {} + end, + + parser = function(result) + local template = result.template + local macro_pattern = "@with_current_buf" + + local s, e = template:find(macro_pattern) + if not s then + return result + end + + local placeholder = macro.generate_placeholder(M.name, "") + + local current_buf = vim.api.nvim_get_current_buf() + local content = table.concat(vim.api.nvim_buf_get_lines(current_buf, 0, -1, false), "\n") + local full_path = vim.api.nvim_buf_get_name(current_buf) + + content = gp.render.template(gp.config.template_context_file, { + ["{{content}}"] = content, + ["{{filename}}"] = full_path, + }) + result.artifacts[placeholder] = content + + result.template = template:sub(1, s - 1) .. placeholder .. template:sub(e + 1) + + return result + end, +} + +return M diff --git a/lua/gp/macros/context_file.lua b/lua/gp/macros/with_file.lua similarity index 92% rename from lua/gp/macros/context_file.lua rename to lua/gp/macros/with_file.lua index 2fdd457e..046beefe 100644 --- a/lua/gp/macros/context_file.lua +++ b/lua/gp/macros/with_file.lua @@ -5,14 +5,14 @@ local M = {} ---@type gp.Macro M = { - name = "context_file`", + name = "with_file`", description = "replaces the macro with the content of the specified file", default = nil, max_occurrences = 100, triggered = function(params) local cropped_line = params.cropped_line - return cropped_line:match("@context_file`[^`]*$") + return cropped_line:match("@with_file`[^`]*$") end, completion = function(params) @@ -27,7 +27,7 @@ M = { parser = function(result) local template = result.template - local macro_pattern = "@context_file`([^`]*)`" + local macro_pattern = "@with_file`([^`]*)`" for _ = 1, M.max_occurrences do local s, e, value = template:find(macro_pattern) diff --git a/lua/gp/macros/with_repo_instructions.lua b/lua/gp/macros/with_repo_instructions.lua new file mode 100644 index 00000000..315f0783 --- /dev/null +++ b/lua/gp/macros/with_repo_instructions.lua @@ -0,0 +1,62 @@ +local macro = require("gp.macro") +local gp = require("gp") + +---@param git_root? string # optional git root directory +---@return string # returns instructions from the .gp.md file +local repo_instructions = function(git_root) + git_root = git_root or gp.helpers.find_git_root() + + if git_root == "" then + return "" + end + + local instruct_file = (git_root:gsub("/$", "")) .. "/.gp.md" + + if vim.fn.filereadable(instruct_file) == 0 then + return "" + end + + local lines = vim.fn.readfile(instruct_file) + return table.concat(lines, "\n") +end + +local M = {} + +---@type gp.Macro +M = { + name = "with_repo_instructions", + description = "replaces the macro with the content of the .gp.md file in the git root", + default = nil, + max_occurrences = 1, + + triggered = function(_) + return false + end, + + completion = function(_) + return {} + end, + + parser = function(result) + local template = result.template + local macro_pattern = "@with_repo_instructions" + + local s, e = template:find(macro_pattern) + if not s then + return result + end + + local placeholder = macro.generate_placeholder(M.name, "") + + local instructions = repo_instructions(result.state.context_dir) + result.artifacts[placeholder] = gp.render.template(gp.config.template_context_file, { + ["{{content}}"] = instructions, + ["{{filename}}"] = ".repository_instructions.md", + }) + + result.template = template:sub(1, s - 1) .. placeholder .. template:sub(e + 1) + return result + end, +} + +return M From c8fc57be487c03e3cf2053242f01d944b53a02de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Sep 2024 12:32:50 +0000 Subject: [PATCH 29/33] chore: update README and auto-generate vimdoc --- doc/gp.nvim.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gp.nvim.txt b/doc/gp.nvim.txt index f8078ca0..33621575 100644 --- a/doc/gp.nvim.txt +++ b/doc/gp.nvim.txt @@ -1,4 +1,4 @@ -*gp.nvim.txt* For Neovim Last change: 2024 September 19 +*gp.nvim.txt* For Neovim Last change: 2024 September 23 ============================================================================== Table of Contents *gp.nvim-table-of-contents* From b7efad9f7d12bbc089d467aa33f51e687fb6100a Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Tue, 8 Apr 2025 22:21:04 +0200 Subject: [PATCH 30/33] feat: better templates for multi snippet --- lua/gp/config.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/gp/config.lua b/lua/gp/config.lua index 5b9c1e5a..0eb3ed54 100644 --- a/lua/gp/config.lua +++ b/lua/gp/config.lua @@ -358,17 +358,17 @@ local config = { command_auto_select_response = true, -- templates - template_selection = "I have the following from {{filename}}:" + template_selection = "I have the following primary snippet from {{filename}}:" .. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}", - template_rewrite = "I have the following from {{filename}}:" + template_rewrite = "I have the following primary snippet from {{filename}}:" .. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}" - .. "\n\nRespond exclusively with the snippet that should replace the selection above.", - template_append = "I have the following from {{filename}}:" + .. "\n\nRespond exclusively with the snippet that should replace the primary selection above.", + template_append = "I have the following primary snippet from {{filename}}:" .. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}" - .. "\n\nRespond exclusively with the snippet that should be appended after the selection above.", - template_prepend = "I have the following from {{filename}}:" + .. "\n\nRespond exclusively with the snippet that should be appended after the primary selection above.", + template_prepend = "I have the following primary snippet from {{filename}}:" .. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}" - .. "\n\nRespond exclusively with the snippet that should be prepended before the selection above.", + .. "\n\nRespond exclusively with the snippet that should be prepended before the primary selection above.", template_command = "{{command}}", template_context_file = "\n\nHere is a file {{filename}} for additional context:" .. "\n\n```\n{{content}}\n```\n\n", From c259334655005587b95fd686cfe077d296d1c4bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Apr 2025 20:25:35 +0000 Subject: [PATCH 31/33] chore: update README and auto-generate vimdoc --- README.md | 2 +- doc/gp.nvim.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31edb0a5..92a4854b 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Voice commands (`:GpWhisper*`) depend on `SoX` (Sound eXchange) to handle audio Below is a linked snippet with the default values, but I suggest starting with minimal config possible (just `openai_api_key` if you don't have `OPENAI_API_KEY` env set up). Defaults change over time to improve things, options might get deprecated and so on - it's better to change only things where the default doesn't fit your needs. -https://github.com/Robitx/gp.nvim/blob/449052b6c02113db066598db9273aa8d5172dd38/lua/gp/config.lua#L10-L610 +https://github.com/Robitx/gp.nvim/blob/b7efad9f7d12bbc089d467aa33f51e687fb6100a/lua/gp/config.lua#L10-L610 # Usage diff --git a/doc/gp.nvim.txt b/doc/gp.nvim.txt index 9d56d390..f536b5a9 100644 --- a/doc/gp.nvim.txt +++ b/doc/gp.nvim.txt @@ -252,7 +252,7 @@ options might get deprecated and so on - it’s better to change only things where the default doesn’t fit your needs. -https://github.com/Robitx/gp.nvim/blob/449052b6c02113db066598db9273aa8d5172dd38/lua/gp/config.lua#L10-L610 +https://github.com/Robitx/gp.nvim/blob/b7efad9f7d12bbc089d467aa33f51e687fb6100a/lua/gp/config.lua#L10-L610 ============================================================================== From 32296297ab575613da805c037448aa49587d688a Mon Sep 17 00:00:00 2001 From: Tibor Schmidt Date: Tue, 8 Apr 2025 22:58:00 +0200 Subject: [PATCH 32/33] feat: add default o3-mini --- lua/gp/config.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lua/gp/config.lua b/lua/gp/config.lua index 0eb3ed54..dd0cba6e 100644 --- a/lua/gp/config.lua +++ b/lua/gp/config.lua @@ -121,6 +121,16 @@ local config = { -- system prompt (use this to specify the persona/role of the AI) system_prompt = require("gp.defaults").chat_system_prompt, }, + { + provider = "openai", + name = "ChatGPT-o3-mini", + chat = true, + command = false, + -- string with model name or table with model name and parameters + model = { model = "o3-mini", temperature = 1.1, top_p = 1 }, + -- system prompt (use this to specify the persona/role of the AI) + system_prompt = require("gp.defaults").chat_system_prompt, + }, { provider = "copilot", name = "ChatCopilot", @@ -211,6 +221,16 @@ local config = { -- system prompt (use this to specify the persona/role of the AI) system_prompt = require("gp.defaults").code_system_prompt, }, + { + provider = "openai", + name = "CodeGPT-o3-mini", + chat = false, + command = true, + -- string with model name or table with model name and parameters + model = { model = "o3-mini", temperature = 0.8, top_p = 1 }, + -- system prompt (use this to specify the persona/role of the AI) + system_prompt = require("gp.defaults").code_system_prompt, + }, { provider = "openai", name = "CodeGPT4o-mini", From 438cba178ee79545d50b507eafd842b501934730 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Apr 2025 21:18:30 +0000 Subject: [PATCH 33/33] chore: update README and auto-generate vimdoc --- README.md | 2 +- doc/gp.nvim.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dabc40e4..06903b82 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Voice commands (`:GpWhisper*`) depend on `SoX` (Sound eXchange) to handle audio Below is a linked snippet with the default values, but I suggest starting with minimal config possible (just `openai_api_key` if you don't have `OPENAI_API_KEY` env set up). Defaults change over time to improve things, options might get deprecated and so on - it's better to change only things where the default doesn't fit your needs. -https://github.com/Robitx/gp.nvim/blob/8dd99d85adfcfcb326f85a1f15bcd254f628df59/lua/gp/config.lua#L10-L627 +https://github.com/Robitx/gp.nvim/blob/1be358df28e39132f894871d387d9373ab3636fa/lua/gp/config.lua#L10-L630 # Usage diff --git a/doc/gp.nvim.txt b/doc/gp.nvim.txt index 41805d11..653e9e55 100644 --- a/doc/gp.nvim.txt +++ b/doc/gp.nvim.txt @@ -252,7 +252,7 @@ options might get deprecated and so on - it’s better to change only things where the default doesn’t fit your needs. -https://github.com/Robitx/gp.nvim/blob/8dd99d85adfcfcb326f85a1f15bcd254f628df59/lua/gp/config.lua#L10-L627 +https://github.com/Robitx/gp.nvim/blob/1be358df28e39132f894871d387d9373ab3636fa/lua/gp/config.lua#L10-L630 ==============================================================================