Skip to content

wip: macro support #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
32d6a2a
refactor: helper for chat header parsing
Robitx Aug 17, 2024
fef83fe
feat: improve topic header detection
Robitx Aug 17, 2024
ccc7b70
feat: ftplugin for .gp.md files
Robitx Aug 17, 2024
90b3e33
chore: better logs
Robitx Aug 17, 2024
9687d74
feat: picker support for dynamic chat names
Robitx Aug 17, 2024
d07cd82
feat: ftplugin for chat files
Robitx Aug 17, 2024
553d49b
feat: autorename chats using topic (issue: #133)
Robitx Aug 17, 2024
800d8f6
feat: looser chat file condition (issue: #106)
Robitx Aug 17, 2024
f18f78d
feat: YAML frontmatter convention for chat templates
Robitx Aug 17, 2024
a06784e
feat: command macro support
Robitx Aug 17, 2024
161aff2
refactor: no state across macro completion
Robitx Aug 26, 2024
91e7770
feat: delayed .md ft handling (cause obsidian)
Robitx Aug 26, 2024
0b737ff
fix: handle chat text before first user prefix
Robitx Aug 26, 2024
8e2624a
feat: topic as yaml frontmatter header
Robitx Aug 26, 2024
5c408d6
chore: dummy GpDo command
Robitx Aug 26, 2024
a54f5f1
chore: todo note
Robitx Aug 26, 2024
f9c54dd
feat: GpChatHelp to toggle help comments
Robitx Aug 28, 2024
da51f14
refactor: save_buffer helper
Robitx Aug 28, 2024
921ea3f
chore: better behavior of ChatHelp toggle
Robitx Aug 30, 2024
4a46735
feat: buffer state with context dir
Robitx Sep 3, 2024
5613541
refactor: small optimizations
Robitx Sep 3, 2024
19f1c03
feat: agent macro & rm ui.input for Prompt cmds
Robitx Sep 8, 2024
449052b
feat: context_file macro
Robitx Sep 8, 2024
bcc93df
feat: chat macro support
Robitx Sep 8, 2024
9de1920
refactor: startup optimization
Robitx Sep 8, 2024
1d47d0f
chore: working target_filename macro
Robitx Sep 14, 2024
0ea6f7e
chore: update README and auto-generate vimdoc
github-actions[bot] Sep 19, 2024
b0c23ee
feat: adding more macros
Robitx Sep 23, 2024
c8fc57b
chore: update README and auto-generate vimdoc
github-actions[bot] Sep 23, 2024
b7efad9
feat: better templates for multi snippet
Robitx Apr 8, 2025
f8ef386
chore: merge main
Robitx Apr 8, 2025
c259334
chore: update README and auto-generate vimdoc
github-actions[bot] Apr 8, 2025
3229629
feat: add default o3-mini
Robitx Apr 8, 2025
01dc9f7
Merge branch 'GpDo' of github.com:Robitx/gp.nvim into GpDo
Robitx Apr 8, 2025
1be358d
chore: merge main
Robitx Apr 8, 2025
438cba1
chore: update README and auto-generate vimdoc
github-actions[bot] Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- README_REFERENCE_MARKER_REPLACE_NEXT_LINE -->
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

Expand Down
182 changes: 182 additions & 0 deletions after/ftplugin/gpchat.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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("<esc>", "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",
},
{
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 .. "<cr>"
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("<esc>", "xn")
end, rc.comment)
else
M.helpers.set_keymap({ buf }, mode, rc.shortcut, ":<C-u>'<,'>" .. 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", "TextChanged", "InsertLeave" }, {
buffer = buf,
callback = function(event)
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

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,
})
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))

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 = false,
virt_text_pos = "right_align",
virt_text = {
{ msg, "DiagnosticHint" },
},
hl_mode = "combine",
})

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.with_file"),
require("gp.macros.with_repo_instructions"),
})

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,
})
24 changes: 24 additions & 0 deletions after/ftplugin/gpmd.lua
Original file line number Diff line number Diff line change
@@ -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))
M.helpers.save_buffer(buf, "gpmd TextChanged InsertLeave autocmd")
end,
})

-- ensure normal mode
vim.cmd.stopinsert()
M.helpers.feedkeys("<esc>", "xn")
2 changes: 1 addition & 1 deletion doc/gp.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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


==============================================================================
Expand Down
39 changes: 39 additions & 0 deletions lua/gp/buffer_state.lua
Original file line number Diff line number Diff line change
@@ -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
17 changes: 10 additions & 7 deletions lua/gp/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ local config = {
chat_shortcut_delete = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>d" },
chat_shortcut_stop = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>s" },
chat_shortcut_new = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>c" },
chat_shortcut_help = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>h" },
-- default search term when using :GpChatFinder
chat_finder_pattern = "topic ",
chat_finder_mappings = {
Expand Down Expand Up @@ -377,18 +378,20 @@ 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",

-- https://platform.openai.com/docs/guides/speech-to-text/quickstart
-- Whisper costs $0.006 / minute (rounded to the nearest second)
Expand Down
21 changes: 12 additions & 9 deletions lua/gp/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,27 @@ 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: ?
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.
# Add context macros by typing @ in the chat. Toggle this help by `{{help_shortcut}}` or :{{cmd_prefix}}ChatHelp.]]

- file: {{filename}}
M.chat_template = [[
---
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.
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.
]] .. M.chat_help .. [[

---

{{user_prefix}}
]]

M.short_chat_template = [[
# topic: ?
- file: {{filename}}
---
topic: ?
---

{{user_prefix}}
Expand Down
27 changes: 23 additions & 4 deletions lua/gp/dispatcher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading