Gp.nvim provides you ChatGPT like sessions and instructable text/code operations in your favorite editor.
The goal is to extend Neovim with the power of GPT models in a simple unobtrusive extensible way.
Trying to keep things as native as possible - reusing and integrating well with the natural features of (Neo)vim.
u
)neovim
, curl
, grep
and optionally sox
)The commands now work with ranges and the commands with Visual
prefix were dropped.
Specifically the commands:GpChatNew
, :GpRewrite
, :GpAppend
, :GpPrepend
, :GpEnew
, :GpPopup
and
their shortcuts now work across modes, either:
%
for the entire current buffer => :%GpRewrite
)Please update your shortcuts if you use them.
-- lazy.nvim
{
"robitx/gp.nvim",
config = function()
require("gp").setup()
-- or setup with your own config (see Install > Configuration in Readme)
-- require("gp").setup(conf)
-- shortcuts might be setup here (see Usage > Shortcuts in Readme)
end,
}
-- packer.nvim
use({
"robitx/gp.nvim",
config = function()
require("gp").setup()
-- or setup with your own config (see Install > Configuration in Readme)
-- require("gp").setup(conf)
-- shortcuts might be setup here (see Usage > Shortcuts in Readme)
end,
})
Make sure you have OpenAI API key. Get one here
and use it in the config (or setup env OPENAI_API_KEY
).
Also consider setting up usage limits so you won't get suprised at the end of the month.
The core plugin only needs curl
installed to make calls to OpenAI API and grep
for ChatFinder. So Linux, BSD and Mac OS should be covered.
Voice commands (:GpWhisper*
) depend on SoX
(Sound eXchange) to handle audio recording and processing:
brew install sox
apt-get install sox
pacman -S sox
yum install sox
nix-env -i sox
Here are the default values:
local conf = {
-- required openai api key
openai_api_key = os.getenv("OPENAI_API_KEY"),
-- api endpoint (you can change this to azure endpoint)
openai_api_endpoint = "https://api.openai.com/v1/chat/completions",
-- openai_api_endpoint = "https://$URL.openai.azure.com/openai/deployments/{{model}}/chat/completions?api-version=2023-03-15-preview",
-- prefix for all commands
cmd_prefix = "Gp",
-- directory for storing chat files
chat_dir = vim.fn.stdpath("data"):gsub("/$", "") .. "/gp/chats",
-- chat model (string with model name or table with model name and parameters)
chat_model = { model = "gpt-3.5-turbo-16k", temperature = 0.7, top_p = 1 },
-- chat model system prompt
chat_system_prompt = "You are a general AI assistant.",
-- chat user prompt prefix
chat_user_prefix = "🗨:",
-- chat assistant prompt prefix
chat_assistant_prefix = "🤖:",
-- chat topic generation prompt
chat_topic_gen_prompt = "Summarize the topic of our conversation above"
.. " in two or three words. Respond only with those words.",
-- chat topic model (string with model name or table with model name and parameters)
chat_topic_gen_model = "gpt-3.5-turbo-16k",
-- explicitly confirm deletion of a chat file
chat_confirm_delete = true,
-- conceal model parameters in chat
chat_conceal_model_params = true,
-- local shortcuts bound to the chat buffer
-- (be careful to choose something which will work across specified modes)
chat_shortcut_respond = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g><C-g>" },
chat_shortcut_delete = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>d" },
chat_shortcut_new = { modes = { "n", "i", "v", "x" }, shortcut = "<C-g>n" },
-- command config and templates bellow are used by commands like GpRewrite, GpEnew, etc.
-- command prompt prefix for asking user for input
command_prompt_prefix = "🤖 ~ ",
-- command model (string with model name or table with model name and parameters)
command_model = { model = "gpt-3.5-turbo-16k", temperature = 0.7, top_p = 1 },
-- command system prompt
command_system_prompt = "You are an AI that strictly generates just the formated final code.",
-- templates
template_selection = "I have the following code from {{filename}}:"
.. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}",
template_rewrite = "I have the following code from {{filename}}:"
.. "\n\n```{{filetype}}\n{{selection}}\n```\n\n{{command}}"
.. "\n\nRespond just with the snippet of code that should be inserted.",
template_command = "{{command}}",
-- https://platform.openai.com/docs/guides/speech-to-text/quickstart
-- Whisper costs $0.006 / minute (rounded to the nearest second)
-- by eliminating silence and speeding up the tempo of the recording
-- we can reduce the cost by 50% or more and get the results faster
-- directory for storing whisper files
whisper_dir = "/tmp/gp_whisper",
-- multiplier of RMS level dB for threshold used by sox to detect silence vs speech
whisper_silence = "1.5",
-- whisper max recording time (mm:ss)
whisper_max_time = "05:00",
-- whisper tempo (1.0 is normal speed)
whisper_tempo = "1.75",
-- example hook functions (see Extend functionality section in the README)
hooks = {
InspectPlugin = function(plugin, params)
print(string.format("Plugin structure:\n%s", vim.inspect(plugin)))
print(string.format("Command params:\n%s", vim.inspect(params)))
end,
-- GpImplement rewrites the provided selection/range based on comments in the code
Implement = function(gp, params)
local template = "Having following from {{filename}}:\n\n"
.. "```{{filetype}}\n{{selection}}\n```\n\n"
.. "Please rewrite this code according to the comment instructions."
.. "\n\nRespond only with the snippet of finalized code:"
gp.Prompt(
params,
gp.Target.rewrite,
nil, -- command will run directly without any prompting for user input
gp.config.command_model,
template,
gp.config.command_system_prompt
)
end,
-- your own functions can go here, see README for more examples like
-- :GpExplain, :GpUnitTests.., :GpBetterChatNew, ..
},
}
...
-- call setup on your config
require("gp").setup(conf)
-- shortcuts might be setup here (see Usage > Shortcuts in Readme)
Have ChatGPT experience directly in neovim:
:GpChatNew
- open fresh chat in the current window:GpChatToggle
- open chat in toggleable popup window:GpChatFinder
- open a dialog to search through chats:GpChatRespond
- request new gpt response for the current chat:GpChatDelete
- delete the current chatAsk GPT and get response to the specified output:
:GpRewrite
- answer replaces the current line, visual selection or range:GpAppend
- answers after the current line, visual selection or range:GpPrepend
- answers before the current line, selection or range:GpEnew
- answers into new buffer:GpPopup
- answers into pop up window:GpImplement
- default example hook command for finishing the codeall these command work either:
%
for the entire current buffer => :%GpRewrite
)Voice commands transcribed by Whisper API:
:GpWhisper
- transcription replaces the current line, visual selection or range:GpWhisperRewrite
- answer replaces the current line, visual selection or range:GpWhisperAppend
- answers after the current line, visual selection or range:GpWhisperPrepend
- answers before the current line, selection or range:GpWhisperEnew
- answers into new buffer:GpWhisperPopup
- answers into pop up windowTo stop the stream of currently running gpt response you can use :GpStop
Run your own custom hook commands:
:GpInspectPlugin
- inspect GPT prompt plugin objectThere are no default global shortcuts to mess with your own config. Bellow are examples for you to adjust or just use directly.
You can use the good old vim.keymap.set
and paste the following after require("gp").setup(conf)
call
(or anywhere you keep shortcuts if you want them at one place).
local function keymapOptions(desc)
return {
noremap = true,
silent = true,
nowait = true,
desc = "GPT prompt " .. desc,
}
end
-- Chat commands
vim.keymap.set({"n", "i"}, "<C-g>c", "<cmd>GpChatNew<cr>", keymapOptions("New Chat"))
vim.keymap.set({"n", "i"}, "<C-g>t", "<cmd>GpChatToggle<cr>", keymapOptions("Toggle Popup Chat"))
vim.keymap.set({"n", "i"}, "<C-g>f", "<cmd>GpChatFinder<cr>", keymapOptions("Chat Finder"))
vim.keymap.set("v", "<C-g>c", ":<C-u>'<,'>GpChatNew<cr>", keymapOptions("Visual Chat New"))
vim.keymap.set("v", "<C-g>t", ":<C-u>'<,'>GpChatToggle<cr>", keymapOptions("Visual Popup Chat"))
-- Prompt commands
vim.keymap.set({"n", "i"}, "<C-g>r", "<cmd>GpRewrite<cr>", keymapOptions("Inline Rewrite"))
vim.keymap.set({"n", "i"}, "<C-g>a", "<cmd>GpAppend<cr>", keymapOptions("Append"))
vim.keymap.set({"n", "i"}, "<C-g>b", "<cmd>GpPrepend<cr>", keymapOptions("Prepend"))
vim.keymap.set({"n", "i"}, "<C-g>e", "<cmd>GpEnew<cr>", keymapOptions("Enew"))
vim.keymap.set({"n", "i"}, "<C-g>p", "<cmd>GpPopup<cr>", keymapOptions("Popup"))
vim.keymap.set("v", "<C-g>r", ":<C-u>'<,'>GpRewrite<cr>", keymapOptions("Visual Rewrite"))
vim.keymap.set("v", "<C-g>a", ":<C-u>'<,'>GpAppend<cr>", keymapOptions("Visual Append"))
vim.keymap.set("v", "<C-g>b", ":<C-u>'<,'>GpPrepend<cr>", keymapOptions("Visual Prepend"))
vim.keymap.set("v", "<C-g>e", ":<C-u>'<,'>GpEnew<cr>", keymapOptions("Visual Enew"))
vim.keymap.set("v", "<C-g>p", ":<C-u>'<,'>GpPopup<cr>", keymapOptions("Visual Popup"))
vim.keymap.set({"n", "i", "v", "x"}, "<C-g>s", "<cmd>GpStop<cr>", keymapOptions("Stop"))
-- optional Whisper commands
vim.keymap.set({"n", "i"}, "<C-g>w", "<cmd>GpWhisper<cr>", keymapOptions("Whisper"))
vim.keymap.set({"n", "i"}, "<C-g>R", "<cmd>GpWhisperRewrite<cr>", keymapOptions("Inline Rewrite"))
vim.keymap.set({"n", "i"}, "<C-g>A", "<cmd>GpWhisperAppend<cr>", keymapOptions("Append"))
vim.keymap.set({"n", "i"}, "<C-g>B", "<cmd>GpWhisperPrepend<cr>", keymapOptions("Prepend"))
vim.keymap.set({"n", "i"}, "<C-g>E", "<cmd>GpWhisperEnew<cr>", keymapOptions("Enew"))
vim.keymap.set({"n", "i"}, "<C-g>P", "<cmd>GpWhisperPopup<cr>", keymapOptions("Popup"))
vim.keymap.set("v", "<C-g>w", ":<C-u>'<,'>GpWhisper<cr>", keymapOptions("Whisper"))
vim.keymap.set("v", "<C-g>R", ":<C-u>'<,'>GpWhisperRewrite<cr>", keymapOptions("Visual Rewrite"))
vim.keymap.set("v", "<C-g>A", ":<C-u>'<,'>GpWhisperAppend<cr>", keymapOptions("Visual Append"))
vim.keymap.set("v", "<C-g>B", ":<C-u>'<,'>GpWhisperPrepend<cr>", keymapOptions("Visual Prepend"))
vim.keymap.set("v", "<C-g>E", ":<C-u>'<,'>GpWhisperEnew<cr>", keymapOptions("Visual Enew"))
vim.keymap.set("v", "<C-g>P", ":<C-u>'<,'>GpWhisperPopup<cr>", keymapOptions("Visual Popup"))
Or go more fancy by using which-key.nvim plugin:
-- VISUAL mode mappings
-- s, x, v modes are handled the same way by which_key
require("which-key").register({
-- ...
["<C-g>"] = {
c = { ":<C-u>'<,'>GpChatNew<cr>", "Visual Chat New" },
t = { ":<C-u>'<,'>GpChatToggle<cr>", "Visual Popup Chat" },
r = { ":<C-u>'<,'>GpRewrite<cr>", "Visual Rewrite" },
a = { ":<C-u>'<,'>GpAppend<cr>", "Visual Append" },
b = { ":<C-u>'<,'>GpPrepend<cr>", "Visual Prepend" },
e = { ":<C-u>'<,'>GpEnew<cr>", "Visual Enew" },
p = { ":<C-u>'<,'>GpPopup<cr>", "Visual Popup" },
s = { "<cmd>GpStop<cr>", "Stop" },
-- optional Whisper commands
w = { ":<C-u>'<,'>GpWhisper<cr>", "Whisper" },
R = { ":<C-u>'<,'>GpWhisperRewrite<cr>", "Whisper Visual Rewrite" },
A = { ":<C-u>'<,'>GpWhisperAppend<cr>", "Whisper Visual Append" },
B = { ":<C-u>'<,'>GpWhisperPrepend<cr>", "Whisper Visual Prepend" },
E = { ":<C-u>'<,'>GpWhisperEnew<cr>", "Whisper Visual Enew" },
P = { ":<C-u>'<,'>GpWhisperPopup<cr>", "Whisper Visual Popup" },
},
-- ...
}, {
mode = "v", -- VISUAL mode
prefix = "",
buffer = nil,
silent = true,
noremap = true,
nowait = true,
})
-- NORMAL mode mappings
require("which-key").register({
-- ...
["<C-g>"] = {
c = { "<cmd>GpChatNew<cr>", "New Chat" },
t = { "<cmd>GpChatToggle<cr>", "Toggle Popup Chat" },
f = { "<cmd>GpChatFinder<cr>", "Chat Finder" },
r = { "<cmd>GpRewrite<cr>", "Inline Rewrite" },
a = { "<cmd>GpAppend<cr>", "Append" },
b = { "<cmd>GpPrepend<cr>", "Prepend" },
e = { "<cmd>GpEnew<cr>", "Enew" },
p = { "<cmd>GpPopup<cr>", "Popup" },
s = { "<cmd>GpStop<cr>", "Stop" },
-- optional Whisper commands
w = { "<cmd>GpWhisper<cr>", "Whisper" },
R = { "<cmd>GpWhisperRewrite<cr>", "Whisper Inline Rewrite" },
A = { "<cmd>GpWhisperAppend<cr>", "Whisper Append" },
B = { "<cmd>GpWhisperPrepend<cr>", "Whisper Prepend" },
E = { "<cmd>GpWhisperEnew<cr>", "Whisper Enew" },
P = { "<cmd>GpWhisperPopup<cr>", "Whisper Popup" },
},
-- ...
}, {
mode = "n", -- NORMAL mode
prefix = "",
buffer = nil,
silent = true,
noremap = true,
nowait = true,
})
-- INSERT mode mappings
require("which-key").register({
-- ...
["<C-g>"] = {
c = { "<cmd>GpChatNew<cr>", "New Chat" },
t = { "<cmd>GpChatToggle<cr>", "Toggle Popup Chat" },
f = { "<cmd>GpChatFinder<cr>", "Chat Finder" },
r = { "<cmd>GpRewrite<cr>", "Inline Rewrite" },
a = { "<cmd>GpAppend<cr>", "Append" },
b = { "<cmd>GpPrepend<cr>", "Prepend" },
e = { "<cmd>GpEnew<cr>", "Enew" },
p = { "<cmd>GpPopup<cr>", "Popup" },
s = { "<cmd>GpStop<cr>", "Stop" },
-- optional Whisper commands
w = { "<cmd>GpWhisper<cr>", "Whisper" },
R = { "<cmd>GpWhisperRewrite<cr>", "Whisper Inline Rewrite" },
A = { "<cmd>GpWhisperAppend<cr>", "Whisper Append" },
B = { "<cmd>GpWhisperPrepend<cr>", "Whisper Prepend" },
E = { "<cmd>GpWhisperEnew<cr>", "Whisper Enew" },
P = { "<cmd>GpWhisperPopup<cr>", "Whisper Popup" },
},
-- ...
}, {
mode = "i", -- INSERT mode
prefix = "",
buffer = nil,
silent = true,
noremap = true,
nowait = true,
})
You can extend/override the plugin functionality with your own, by putting functions into config.hooks
.
Hooks have access to everything (see InspectPlugin
example in defaults) and are
automatically registered as commands (GpInspectPlugin
).
Here are some more examples:
:GpUnitTests
-- example of adding command which writes unit tests for the selected code
UnitTests = function(gp, params)
local template = "I have the following code from {{filename}}:\n\n"
.. "```{{filetype}}\n{{selection}}\n```\n\n"
.. "Please respond by writing table driven unit tests for the code above."
gp.Prompt(params, gp.Target.enew, nil, gp.config.command_model,
template, gp.config.command_system_prompt)
end,
:GpExplain
-- example of adding command which explains the selected code
Explain = function(gp, params)
local template = "I have the following code from {{filename}}:\n\n"
.. "```{{filetype}}\n{{selection}}\n```\n\n"
.. "Please respond by explaining the code above."
gp.Prompt(params, gp.Target.popup, nil, gp.config.command_model,
template, gp.config.chat_system_prompt)
end,
:GpCodeReview
-- example of usig enew as a function specifying type for the new buffer
CodeReview = function(gp, params)
local template = "I have the following code from {{filename}}:\n\n"
.. "```{{filetype}}\n{{selection}}\n```\n\n"
.. "Please analyze for code smells and suggest improvements."
gp.Prompt(params, gp.Target.enew("markdown"), nil, gp.config.command_model,
template, gp.config.command_system_prompt)
end
:GpBufferChatNew
-- example of making :%GpChatNew a dedicated command which
-- opens new chat with the entire current buffer as a context
BufferChatNew = function(gp, _)
-- call GpChatNew command in range mode on whole buffer
vim.api.nvim_command("%" .. gp.config.cmd_prefix .. "ChatNew")
end,
:GpBetterChatNew
-- example of adding a custom chat command with non-default parameters
-- (configured default might be gpt-3 and sometimes you might want to use gpt-4)
BetterChatNew = function(gp, params)
local chat_model = { model = "gpt-4", temperature = 0.7, top_p = 1 }
local chat_system_prompt = "You are a general AI assistant."
gp.cmd.ChatNew(params, chat_model, chat_system_prompt)
end,
The raw plugin text editing method Prompt
has seven aprameters:
params
is a table passed to neovim user commands, Prompt
currently uses range, line1, line2
to work with ranges
params = {
args = "",
bang = false,
count = -1,
fargs = {},
line1 = 1352,
line2 = 1352,
mods = "",
name = "GpChatNew",
range = 0,
reg = "",
smods = {
browse = false,
confirm = false,
emsg_silent = false,
hide = false,
horizontal = false,
keepalt = false,
keepjumps = false,
keepmarks = false,
keeppatterns = false,
lockmarks = false,
noautocmd = false,
noswapfile = false,
sandbox = false,
silent = false,
split = "",
tab = -1,
unsilent = false,
verbose = -1,
vertical = false
}
}
target
specifying where to direct GPT response
enew/enew()/enew("markdown")/..
)M.Target = {
rewrite = 0, -- for replacing the selection, range or the current line
append = 1, -- for appending after the selection, range or the current line
prepend = 2, -- for prepending before the selection, range or the current line
popup = 3, -- for writing into the popup window
-- for writing into a new buffer
---@param filetype nil | string # nil = same as the original buffer
---@return table # a table with type=4 and filetype=filetype
enew = function(filetype)
return { type = 4, filetype = filetype }
end,
}
prompt
nil
, user is not asked to provide input (for specific predefined commands - document this, explain that, write tests ..)🤖 ~
might be used or you could use different msg to convey info about the method which is called🤖 rewrite ~
, 🤖 popup ~
, 🤖 enew ~
, 🤖 inline ~
, etc.)model
template
template of the user message send to gpt
string can include variables bellow:
name | Description |
---|---|
{{filetype}} |
filetype of the current buffer |
{{selection}} |
last or currently selected text |
{{command}} |
instructions provided by the user |
system_template
whisper
There is already a bunch of similar plugins which served as sources of inspiration