diff --git a/README.md b/README.md index a69c0b7..00f9940 100644 --- a/README.md +++ b/README.md @@ -24,52 +24,76 @@ Using [dein](https://github.com/Shougo/dein.vim) call dein#add('joshuavial/aider.nvim') ``` +Using [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +{ + "joshuavial/aider.nvim", + config = function() + require("aider").setup({ + -- your configuration comes here + -- if you don't want to use the default settings + auto_manage_context = true, + default_bindings = true, + debug = false, -- Set to true to enable debug logging + }) + end, +} +``` + ## Usage -The Aider Plugin for Neovim provides the `AiderOpen` and `AiderBackground` lua functions. +The Aider Plugin for Neovim provides several functions and commands: -The `AiderOpen` function opens a terminal window with the Aider command. It accepts the following arguments: -- `args`: The command line arguments to pass to `aider` - defaults to "" -- `window_type`: The window style to use 'vsplit' (default), 'hsplit' or 'editor' +1. `AiderOpen`: Opens a terminal window with the Aider command. -NOTE: if an Aider job is already running calling AiderOpen will reattach to it, even if it is called with different flags + - Arguments: + - `args`: Command line arguments to pass to `aider` (default: "") + - `window_type`: Window style to use ('vsplit' (default), 'hsplit', or 'editor') + - Note: If an Aider job is already running, calling AiderOpen will reattach to it, even with different flags. -The `AiderBackground` function runs the Aider command in the background. It accepts the following arguments: -- `args`: The command line arguments to pass to `aider` - defaults to "" -- `message`: The message to pass to the Aider command - defaults to "Complete as many todo items as you can and remove the comment for any item you complete." +2. `AiderBackground`: Runs the Aider command in the background. -When Aider opens (through either function), it will automatically add all open buffers to both commands. If you are going to use this plugin you will want to actively manage open buffers with commands like `:ls` and `:bd`. + - Arguments: + - `args`: Command line arguments to pass to `aider` (default: "") + - `message`: Message to pass to the Aider command (default: "Complete as many todo items as you can and remove the comment for any item you complete.") -Here are some examples of how to use the `AiderOpen` and `AiderBackground` functions: +3. `AiderAddModifiedFiles`: Adds all git-modified files to the Aider chat. + - This function can be called directly or through the user command `:AiderAddModifiedFiles` + +When Aider opens (through either function), it automatically adds all open buffers to both commands. It's recommended to actively manage open buffers with commands like `:ls` and `:bd`. + +Examples of using these commands: ```vim -:lua AiderOpen() -:lua AiderOpen("-3", "hsplit") -:lua AiderOpen("AIDER_NO_AUTO_COMMITS=1 aider -3", "editor") -:lua AiderBackground() -:lua AiderBackground("-3") -:lua AiderBackground("AIDER_NO_AUTO_COMMITS=1 aider -3") +:AiderOpen +:AiderOpen -3 hsplit +:AiderOpen "AIDER_NO_AUTO_COMMITS=1 aider -3" editor +:AiderBackground +:AiderBackground -3 +:AiderBackground "AIDER_NO_AUTO_COMMITS=1 aider -3" +:AiderAddModifiedFiles ``` -You can also set keybindings for the `AiderOpen` and `AiderBackground` functions in Lua. Here's an example: +You can set custom keybindings for these commands in your Neovim configuration. For example: ```lua --- set a keybinding for the AiderOpen function -vim.api.nvim_set_keymap('n', 'oa', 'lua AiderOpen()', {noremap = true, silent = true}) --- set a keybinding for the AiderBackground function -vim.api.nvim_set_keymap('n', 'ob', 'lua AiderBackground()', {noremap = true, silent = true}) +vim.api.nvim_set_keymap('n', 'ao', ':AiderOpen', {noremap = true, silent = true}) +vim.api.nvim_set_keymap('n', 'ab', ':AiderBackground', {noremap = true, silent = true}) +vim.api.nvim_set_keymap('n', 'am', ':AiderAddModifiedFiles', {noremap = true, silent = true}) ``` -In this example, pressing `oa` in normal mode will call the `AiderOpen` function, and `ob` will call the `AiderBackground` function. - -Run `aider --help` to see all the options you can pass to the cli. +Run `aider --help` to see all the options you can pass to the CLI. The plugin provides the following default keybindings: -- `` to open a terminal window with the Aider defaults (gpt-4). -- `3` to open a terminal window with the Aider command using the gpt-3.5-turbo-16k model for chat. -- `b` to run the Aider command in the background with the defaults. -- `b3` to run the Aider command in the background using the gpt-3.5-turbo-16k model for chat. +- `Ao`: Open a terminal window with the Aider defaults (gpt-4). +- `AO`: Open a terminal window with the Aider command using the gpt-3.5-turbo-16k model for chat. +- `Ab`: Run the Aider command in the background with the defaults. +- `AB`: Run the Aider command in the background using the gpt-3.5-turbo-16k model for chat. +- `Am`: Add all git-modified files to the Aider chat. + +These keybindings are set up using which-key, providing a descriptive popup menu when you press `A`. ## Setup @@ -77,13 +101,19 @@ The Aider Plugin for Neovim provides a `setup` function that you can use to conf - `auto_manage_context`: A boolean value that determines whether the plugin should automatically manage the context. If set to `true`, the plugin will automatically add and remove buffers from the context as they are opened and closed. Defaults to `true`. - `default_bindings`: A boolean value that determines whether the plugin should use the default keybindings. If set to `true`, the plugin will require the keybindings file and set the default keybindings. Defaults to `true`. +- `debug`: A boolean value that determines whether the plugin should enable debug logging. When set to true, it will print debug information to help troubleshoot issues. Defaults to false. Here is an example of how to use the `setup` function: ```lua require('aider').setup({ auto_manage_context = false, - default_bindings = false + default_bindings = false, + debug = true, + vim = true, -- Pass the `--vim` flag to Aider when opening a new chat + + -- only necessary if you want to change the default keybindings. C is not a particularly good choice. It's just shown as an example. + vim.api.nvim_set_keymap('n', 'C', ':AiderOpen', {noremap = true, silent = true}) }) ``` @@ -95,40 +125,29 @@ The plugin exposes a global variable called `aider_background_status` that you c ```lua lualine_x = {{ - function() + function() return 'A' end, color = { fg = '#8FBCBB' }, -- green - cond = function() + cond = function() return _G.aider_background_status == 'idle' end }, { - function() + function() return 'A' end, color = { fg = '#BF616A' }, -- red - cond = function() + cond = function() return _G.aider_background_status == 'working' end } } ``` -## Reloading buffers +## Reloading buffers after Aider updates the underlying code -Because the AiderOnBufferOpen command is bound to BufReadPost it will fire whenever a buffer is reloaded if you just use a `:e!`. The ReloadBuffer function below will prevent a file from being added to aider every time it's openeed. - -```lua -function ReloadBuffer() - local temp_sync_value = vim.g.aider_buffer_sync - vim.g.aider_buffer_sync = 0 - vim.api.nvim_exec2('e!', {output = false}) - vim.g.aider_buffer_sync = temp_sync_value -end -``` - -To use this function, simply call `:lua ReloadBuffer()` (or bind it to your favourite shortcut). This will refresh the current buffer and display any changes made by Aider. +Run the `:e` command to re-edit the current buffer updating its contents with any changes made since initially loading it. ## Tips for Working with Buffers in Vim @@ -140,24 +159,6 @@ If you're not familiar with buffers in Vim, here are some tips: - Use `:bd ` or `:bdelete ` to close a specific buffer. Replace `` with the buffer number. - Use `:bufdo bd` to close all buffers. -This helper function may be useful for closing all buffers that are hidden - -```lua -_G.close_hidden_buffers = function() - local curr_buf_num = vim.api.nvim_get_current_buf() - local all_buf_nums = vim.api.nvim_list_bufs() - - for _, buf_num in ipairs(all_buf_nums) do - if buf_num ~= curr_buf_num and vim.api.nvim_buf_is_valid(buf_num) and vim.api.nvim_buf_is_loaded(buf_num) and vim.fn.bufwinnr(buf_num) == -1 then - if vim.fn.getbufvar(buf_num, '&buftype') ~= 'terminal' then - vim.api.nvim_buf_delete(buf_num, { force = true }) - end - end - end -end -``` - ## NOTE if you resize a split the nvim buffer can truncate the text output, chatGPT tells me there isn't an easy work around for this. Feel free to make a PR if you think it's easy to solve without rearchitecting and using tmux or something similar. - diff --git a/lua/aider.lua b/lua/aider.lua index 38db0d1..1deb836 100644 --- a/lua/aider.lua +++ b/lua/aider.lua @@ -1,37 +1,89 @@ -local helpers = require('helpers') +local helpers = require("helpers") local M = {} M.aider_buf = nil +M.debug = false + +local function is_valid_buffer(bufnr) + local bufname = vim.api.nvim_buf_get_name(bufnr) + local buftype = vim.api.nvim_buf_get_option(bufnr, "buftype") + local filetype = vim.api.nvim_buf_get_option(bufnr, "filetype") + + -- Ignore special buffers and directories + if + buftype ~= "" + or filetype == "NvimTree" + or filetype == "neo-tree" + or bufname:match("^term://") + or not vim.fn.filereadable(bufname) + or vim.fn.isdirectory(bufname) == 1 + then + return false + end + + return true +end + +local function log(message) + if M.debug then + print(string.format("[Aider Log] %s", message)) + end +end function M.AiderBackground(args, message) + log("AiderBackground called with args: " .. (args or "nil") .. ", message: " .. (message or "nil")) helpers.showProcessingCue() local command = helpers.build_background_command(args, message) - local handle = vim.loop.spawn('bash', { - args = {'-c', command} + local handle = vim.loop.spawn("bash", { + args = { "-c", command }, }, NotifyOnExit) - vim.notify("Aider started " .. (args or '')) + vim.notify("Aider started " .. (args or "")) end - -function OnExit(code, signal) - if M.aider_buf then - vim.api.nvim_command('bd! ' .. M.aider_buf) - M.aider_buf = nil - end +local function OnExit(job_id, exit_code, event_type) + vim.schedule(function() + if M.aider_buf and vim.api.nvim_buf_is_valid(M.aider_buf) then + vim.api.nvim_buf_set_option(M.aider_buf, "modifiable", true) + local message + if exit_code == 0 then + message = "Aider process completed successfully." + else + message = "Aider process exited with code: " .. exit_code + end + vim.api.nvim_buf_set_lines(M.aider_buf, -1, -1, false, { "", message }) + vim.api.nvim_buf_set_option(M.aider_buf, "modifiable", false) + end + log("Aider process exited with code: " .. exit_code) + end) end function M.AiderOpen(args, window_type) - window_type = window_type or 'vsplit' + log("AiderOpen called with args: " .. (args or "nil") .. ", window_type: " .. (window_type or "nil")) + window_type = window_type or "vsplit" if M.aider_buf and vim.api.nvim_buf_is_valid(M.aider_buf) then + log("Existing aider buffer found, opening in new window") helpers.open_buffer_in_new_window(window_type, M.aider_buf) else - command = 'aider ' .. (args or '') + log("No existing aider buffer, creating new one") + local command = "aider " .. (args or "") + if M.config.vim then + command = command .. " --vim" + end + log("Opening window with type: " .. window_type) helpers.open_window(window_type) - command = helpers.add_buffers_to_command(command) - M.aider_job_id = vim.fn.termopen(command, {on_exit = OnExit}) + log("Adding buffers to command") + command = helpers.add_buffers_to_command(command, is_valid_buffer) + log("Final command: " .. command) + log("Opening terminal with command") M.aider_buf = vim.api.nvim_get_current_buf() + M.aider_job_id = vim.fn.termopen(command, { on_exit = OnExit }) + log("Terminal opened with job ID: " .. M.aider_job_id) + log("Set aider_buf to: " .. M.aider_buf) + vim.api.nvim_buf_set_option(M.aider_buf, "bufhidden", "hide") end + log("AiderOpen completed") + log("Final aider_buf: " .. (M.aider_buf or "nil")) end function M.AiderOnBufferOpen(bufnr) @@ -39,15 +91,14 @@ function M.AiderOnBufferOpen(bufnr) return end bufnr = tonumber(bufnr) - local bufname = vim.api.nvim_buf_get_name(bufnr) - local buftype = vim.fn.getbufvar(bufnr, '&buftype') - if not bufname or bufname:match('^term://') or buftype == 'terminal' then + if not is_valid_buffer(bufnr) then return end - local relative_filename = vim.fn.fnamemodify(bufname, ':~:.') + local bufname = vim.api.nvim_buf_get_name(bufnr) + local relative_filename = vim.fn.fnamemodify(bufname, ":~:.") if M.aider_buf and vim.api.nvim_buf_is_valid(M.aider_buf) then - local line_to_add = '/add ' .. relative_filename - vim.fn.chansend(M.aider_job_id, line_to_add .. '\n') + local line_to_add = "/add " .. relative_filename + vim.fn.chansend(M.aider_job_id, line_to_add .. "\n") end end @@ -56,38 +107,86 @@ function M.AiderOnBufferClose(bufnr) return end bufnr = tonumber(bufnr) - local bufname = vim.api.nvim_buf_get_name(bufnr) - if not bufname or bufname:match('^term://') then + if not is_valid_buffer(bufnr) then return end - local relative_filename = vim.fn.fnamemodify(bufname, ':~:.') + local bufname = vim.api.nvim_buf_get_name(bufnr) + local relative_filename = vim.fn.fnamemodify(bufname, ":~:.") if M.aider_buf and vim.api.nvim_buf_is_valid(M.aider_buf) then - local line_to_drop = '/drop ' .. relative_filename - vim.fn.chansend(M.aider_job_id, line_to_drop .. '\n') + local line_to_drop = "/drop " .. relative_filename + vim.fn.chansend(M.aider_job_id, line_to_drop .. "\n") + end +end + +local function create_commands() + log("Creating user commands") + vim.api.nvim_create_user_command("AiderOpen", function(opts) + log("AiderOpen command called with args: " .. (opts.args or "nil")) + M.AiderOpen(opts.args) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command("AiderBackground", function(opts) + log("AiderBackground command called with args: " .. (opts.args or "nil")) + M.AiderBackground(opts.args) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command("AiderAddModifiedFiles", function() + log("AiderAddModifiedFiles command called") + M.AiderAddModifiedFiles() + end, {}) + log("User commands created") +end + +function M.AiderAddModifiedFiles() + log("AiderAddModifiedFiles called") + if not M.aider_buf or not vim.api.nvim_buf_is_valid(M.aider_buf) then + log("Aider chat not open, opening it first") + M.AiderOpen() + -- Wait a bit for the Aider chat to initialize + vim.defer_fn(function() + M.AiderAddModifiedFiles() + end, 1000) + return + end + + local modified_files = helpers.get_git_modified_files() + for _, file in ipairs(modified_files) do + local line_to_add = "/add " .. file + vim.fn.chansend(M.aider_job_id, line_to_add .. "\n") end + vim.notify("Added " .. #modified_files .. " modified files to Aider chat") end function M.setup(config) M.config = config or {} M.config.auto_manage_context = M.config.auto_manage_context or true M.config.default_bindings = M.config.default_bindings or true + M.config.vim = M.config.vim or false + M.debug = M.config.debug or false vim.g.aider_buffer_sync = M.config.auto_manage_context if M.config.auto_manage_context then - vim.api.nvim_command('autocmd BufReadPost * lua AiderOnBufferOpen(vim.fn.expand(""))') - vim.api.nvim_command('autocmd BufDelete * lua AiderOnBufferClose(vim.fn.expand(""))') - _G.AiderOnBufferOpen = M.AiderOnBufferOpen - _G.AiderOnBufferClose = M.AiderOnBufferClose + vim.api.nvim_create_autocmd("BufReadPost", { + callback = function(ev) + M.AiderOnBufferOpen(ev.buf) + end, + }) + vim.api.nvim_create_autocmd("BufDelete", { + callback = function(ev) + M.AiderOnBufferClose(ev.buf) + end, + }) end - _G.AiderOpen = M.AiderOpen - _G.AiderBackground = M.AiderBackground - _G.aider_background_status = 'idle' + create_commands() + _G.aider_background_status = "idle" if M.config.default_bindings then - require('keybindings') + require("keybindings") end + + log("Aider setup completed with debug mode: " .. tostring(M.debug)) end return M diff --git a/lua/helpers.lua b/lua/helpers.lua index 152182b..748e1b8 100644 --- a/lua/helpers.lua +++ b/lua/helpers.lua @@ -1,89 +1,104 @@ local function set_idle_status(isIdle) - if isIdle then - _G.aider_background_status = 'idle' - else - _G.aider_background_status = 'working' - end - vim.cmd('redrawstatus') + if isIdle then + _G.aider_background_status = "idle" + else + _G.aider_background_status = "working" + end + vim.cmd("redrawstatus") end local function open_vsplit_window() - vim.api.nvim_command('vnew') + vim.api.nvim_command("vnew") end local function open_hsplit_window() - vim.api.nvim_command('new') + vim.api.nvim_command("new") end local function open_editor_relative_window() - local buf = vim.api.nvim_create_buf(false, true) - local width = vim.api.nvim_get_option("columns") - local height = vim.api.nvim_get_option("lines") - local win = vim.api.nvim_open_win(buf, true, {relative = 'editor', width = width - 10, height = height - 10, row = 2, col = 2}) - vim.api.nvim_set_current_win(win) - vim.bo[buf].buftype = 'nofile' + local buf = vim.api.nvim_create_buf(false, true) + local width = vim.api.nvim_get_option("columns") + local height = vim.api.nvim_get_option("lines") + local win = vim.api.nvim_open_win( + buf, + true, + { relative = "editor", width = width - 10, height = height - 10, row = 2, col = 2 } + ) + vim.api.nvim_set_current_win(win) + vim.bo[buf].buftype = "nofile" end local function open_window(window_type) - if window_type == 'vsplit' then - open_vsplit_window() - elseif window_type == 'hsplit' then - open_hsplit_window() - else - open_editor_relative_window() - end + if window_type == "vsplit" then + open_vsplit_window() + elseif window_type == "hsplit" then + open_hsplit_window() + else + open_editor_relative_window() + end end function NotifyOnExit(code, signal) - vim.schedule(function() - set_idle_status(true) - vim.api.nvim_command('echo ""') - vim.cmd('edit') - vim.notify("Aider finished with exit code " .. code) - end) + vim.schedule(function() + set_idle_status(true) + vim.api.nvim_command('echo ""') + vim.cmd("edit") + vim.notify("Aider finished with exit code " .. code) + end) end local function showProcessingCue() - vim.api.nvim_command('echo "Aider processing ..."') - set_idle_status(false) + vim.api.nvim_command('echo "Aider processing ..."') + set_idle_status(false) end -local function add_buffers_to_command(command) - local buffers = vim.api.nvim_list_bufs() - for _, buf in ipairs(buffers) do - if vim.api.nvim_buf_is_loaded(buf) then - local bufname = vim.api.nvim_buf_get_name(buf) - if not bufname:match('^term:') and not bufname:match('NeogitConsole') then - command = command .. " " .. bufname - end - end - end - return command +local function add_buffers_to_command(command, is_valid_buffer) + local buffers = vim.api.nvim_list_bufs() + for _, buf in ipairs(buffers) do + if vim.api.nvim_buf_is_loaded(buf) and is_valid_buffer(buf) then + local bufname = vim.api.nvim_buf_get_name(buf) + if vim.fn.filereadable(bufname) == 1 then + command = command .. " " .. vim.fn.shellescape(bufname) + end + end + end + return command end local function build_background_command(args, prompt) - prompt = prompt or "Complete as many todo items as you can and remove the comment for any item you complete." - local command = 'aider --msg "' .. prompt .. '" ' .. (args or '') - command = add_buffers_to_command(command) - return command + prompt = prompt or "Complete as many todo items as you can and remove the comment for any item you complete." + local command = 'aider --msg "' .. prompt .. '" ' .. (args or "") + command = add_buffers_to_command(command) + return command end function open_buffer_in_new_window(window_type, aider_buf) - if window_type == 'vsplit' then - vim.api.nvim_command('vsplit | buffer ' .. aider_buf) - elseif window_type == 'hsplit' then - vim.api.nvim_command('split | buffer ' .. aider_buf) - else - vim.api.nvim_command('buffer ' .. aider_buf) - end + if window_type == "vsplit" then + vim.api.nvim_command("vsplit | buffer " .. aider_buf) + elseif window_type == "hsplit" then + vim.api.nvim_command("split | buffer " .. aider_buf) + else + vim.api.nvim_command("buffer " .. aider_buf) + end end +local function get_git_modified_files() + local handle = io.popen("git ls-files --modified --others --exclude-standard") + local result = handle:read("*a") + handle:close() + local files = {} + for file in result:gmatch("[^\r\n]+") do + table.insert(files, file) + end + return files +end return { - open_window = open_window, - NotifyOnExit = NotifyOnExit, - showProcessingCue = showProcessingCue, - add_buffers_to_command = add_buffers_to_command, - build_background_command = build_background_command, - open_buffer_in_new_window = open_buffer_in_new_window + open_window = open_window, + NotifyOnExit = NotifyOnExit, + showProcessingCue = showProcessingCue, + add_buffers_to_command = add_buffers_to_command, + build_background_command = build_background_command, + open_buffer_in_new_window = open_buffer_in_new_window, + get_git_modified_files = get_git_modified_files, } diff --git a/lua/keybindings.lua b/lua/keybindings.lua index 32916ff..2b52941 100644 --- a/lua/keybindings.lua +++ b/lua/keybindings.lua @@ -1,5 +1,71 @@ -vim.g.mapleader = vim.g.mapleader or ' ' -vim.api.nvim_set_keymap('n', ' b', ':lua AiderBackground()', {noremap = true, silent = true}) -vim.api.nvim_set_keymap('n', ' b3', ':lua AiderBackground("-3")', {noremap = true, silent = true}) -vim.api.nvim_set_keymap('n', ' ', ':lua AiderOpen()', {noremap = true, silent = true}) -vim.api.nvim_set_keymap('n', ' 3', ':lua AiderOpen("-3")', {noremap = true, silent = true}) +vim.g.mapleader = vim.g.mapleader or " " + +-- Check if which-key is available +local status_ok, wk = pcall(require, "which-key") +local use_which_key = status_ok + +-- nice local funct! +local function aider_command_and_insert(cmd) + return function() + vim.cmd(cmd) + vim.defer_fn(function() + vim.cmd("startinsert") + end, 100) + end +end + +if use_which_key then + wk.add({ + { + "A", + group = "Aider", + }, + { + "AB", + "AiderBackground -3", + desc = "Run Aider (GPT-3.5) in Background", + mode = "n", + }, + { + "AO", + "AiderOpen -3", + desc = "Open Aider (GPT-3.5)", + mode = "n", + }, + { + "Ab", + "AiderBackground", + desc = "Run Aider in Background", + mode = "n", + }, + { + "Am", + "AiderAddModifiedFiles", + desc = "Add Modified Files to Chat", + mode = "n", + }, + { + "Ao", + "AiderOpen", + desc = "Open Aider", + mode = "n", + }, + }) +else + -- Set up the actual keybindings when which-key is not available + vim.keymap.set( + "n", + "AB", + aider_command_and_insert("AiderBackground -3"), + { desc = "Run Aider (GPT-3.5) in Background" } + ) + vim.keymap.set("n", "AO", aider_command_and_insert("AiderOpen -3"), { desc = "Open Aider (GPT-3.5)" }) + vim.keymap.set("n", "Ab", aider_command_and_insert("AiderBackground"), { desc = "Run Aider in Background" }) + vim.keymap.set( + "n", + "Am", + aider_command_and_insert("AiderAddModifiedFiles"), + { desc = "Add Modified Files to Chat" } + ) + vim.keymap.set("n", "Ao", aider_command_and_insert("AiderOpen"), { desc = "Open Aider" }) +end diff --git a/markdown b/markdown new file mode 100644 index 0000000..e69de29