diff --git a/lua/avante/api.lua b/lua/avante/api.lua index fc4496b..e4006fe 100644 --- a/lua/avante/api.lua +++ b/lua/avante/api.lua @@ -156,13 +156,15 @@ function M.ask(opts) return ask() end ----@param question? string -function M.edit(question) +---@param request? string +---@param line1? integer +---@param line2? integer +function M.edit(request, line1, line2) local _, selection = require("avante").get() if not selection then return end - selection:create_editing_input() - if question ~= nil or question ~= "" then - vim.api.nvim_exec_autocmds("User", { pattern = "AvanteEditSubmitted", data = { request = question } }) + selection:create_editing_input(request, line1, line2) + if request ~= nil and request ~= "" then + vim.api.nvim_exec_autocmds("User", { pattern = "AvanteEditSubmitted", data = { request = request } }) end end diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 69b29d8..3658423 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -449,7 +449,7 @@ M._defaults = { height = 8, -- Height of the input window in vertical layout }, edit = { - border = "rounded", + border = { " ", " ", " ", " ", " ", " ", " ", " " }, start_insert = true, -- Start insert mode when opening the edit window }, ask = { diff --git a/lua/avante/selection.lua b/lua/avante/selection.lua index c901343..d4010ae 100644 --- a/lua/avante/selection.lua +++ b/lua/avante/selection.lua @@ -4,6 +4,8 @@ local Llm = require("avante.llm") local Provider = require("avante.providers") local RepoMap = require("avante.repo_map") local PromptInput = require("avante.ui.prompt_input") +local SelectionResult = require("avante.selection_result") +local Range = require("avante.range") local api = vim.api local fn = vim.fn @@ -19,6 +21,7 @@ local PRIORITY = vim.highlight.priorities.user ---@field selected_code_extmark_id integer | nil ---@field augroup integer | nil ---@field code_winid integer | nil +---@field code_bufnr integer | nil ---@field prompt_input avante.ui.PromptInput | nil local Selection = {} Selection.__index = Selection @@ -34,6 +37,7 @@ function Selection:new(id) selection = nil, cursor_pos = nil, code_winid = nil, + code_bufnr = nil, prompt_input = nil, }, Selection) end @@ -88,7 +92,7 @@ function Selection:close_editing_input() self.selected_code_extmark_id = nil end end - if self.cursor_pos and self.code_winid then + if self.cursor_pos and self.code_winid and api.nvim_win_is_valid(self.code_winid) then vim.schedule(function() local bufnr = api.nvim_win_get_buf(self.code_winid) local line_count = api.nvim_buf_line_count(bufnr) @@ -100,7 +104,124 @@ function Selection:close_editing_input() end end -function Selection:create_editing_input() +function Selection:submit_input(input) + if not input then + Utils.error("No input provided", { once = true, title = "Avante" }) + return + end + if self.prompt_input and self.prompt_input.spinner_active then + Utils.error( + "Please wait for the previous request to finish before submitting another", + { once = true, title = "Avante" } + ) + return + end + local code_lines = api.nvim_buf_get_lines(self.code_bufnr, 0, -1, false) + local code_content = table.concat(code_lines, "\n") + + local full_response = "" + local start_line = self.selection.range.start.lnum + local finish_line = self.selection.range.finish.lnum + + local original_first_line_indentation = Utils.get_indentation(code_lines[self.selection.range.start.lnum]) + + local need_prepend_indentation = false + + if self.prompt_input then self.prompt_input:start_spinner() end + + ---@type AvanteLLMStartCallback + local function on_start(_) end + + ---@type AvanteLLMChunkCallback + local function on_chunk(chunk) + full_response = full_response .. chunk + local response_lines_ = vim.split(full_response, "\n") + local response_lines = {} + local in_code_block = false + for _, line in ipairs(response_lines_) do + if line:match("^") then + in_code_block = true + line = line:gsub("^", "") + if line ~= "" then table.insert(response_lines, line) end + elseif line:match("") then + in_code_block = false + line = line:gsub(".*$", "") + if line ~= "" then table.insert(response_lines, line) end + elseif in_code_block then + table.insert(response_lines, line) + end + end + if #response_lines == 1 then + local first_line = response_lines[1] + local first_line_indentation = Utils.get_indentation(first_line) + need_prepend_indentation = first_line_indentation ~= original_first_line_indentation + end + if need_prepend_indentation then + for i, line in ipairs(response_lines) do + response_lines[i] = original_first_line_indentation .. line + end + end + api.nvim_buf_set_lines(self.code_bufnr, start_line - 1, finish_line, true, response_lines) + finish_line = start_line + #response_lines - 1 + end + + ---@type AvanteLLMStopCallback + local function on_stop(stop_opts) + if stop_opts.error then + -- NOTE: in Ubuntu 22.04+ you will see this ignorable error from ~/.local/share/nvim/lazy/avante.nvim/lua/avante/llm.lua `on_error = function(err)`, check to avoid showing this error. + if type(stop_opts.error) == "table" and stop_opts.error.exit == nil and stop_opts.error.stderr == "{}" then + return + end + Utils.error( + "Error occurred while processing the response: " .. vim.inspect(stop_opts.error), + { once = true, title = "Avante" } + ) + return + end + if self.prompt_input then self.prompt_input:stop_spinner() end + vim.defer_fn(function() self:close_editing_input() end, 0) + Utils.debug("full response:", full_response) + end + + local filetype = api.nvim_get_option_value("filetype", { buf = self.code_bufnr }) + local file_ext = api.nvim_buf_get_name(self.code_bufnr):match("^.+%.(.+)$") + + local mentions = Utils.extract_mentions(input) + input = mentions.new_content + local project_context = mentions.enable_project_context and RepoMap.get_repo_map(file_ext) or nil + + local diagnostics = Utils.get_current_selection_diagnostics(self.code_bufnr, self.selection) + + ---@type AvanteSelectedCode | nil + local selected_code = nil + + if self.selection then + selected_code = { + content = self.selection.content, + file_type = self.selection.filetype, + path = self.selection.filepath, + } + end + + Llm.stream({ + ask = true, + project_context = vim.json.encode(project_context), + diagnostics = vim.json.encode(diagnostics), + selected_files = { { content = code_content, file_type = filetype, path = "" } }, + code_lang = filetype, + selected_code = selected_code, + instructions = input, + mode = "editing", + on_start = on_start, + on_chunk = on_chunk, + on_stop = on_stop, + }) +end + +---@param request? string +---@param line1? integer +---@param line2? integer +function Selection:create_editing_input(request, line1, line2) self:close_editing_input() if not vim.g.avante_login or vim.g.avante_login == false then @@ -108,14 +229,24 @@ function Selection:create_editing_input() vim.g.avante_login = true end - local code_bufnr = api.nvim_get_current_buf() - local code_winid = api.nvim_get_current_win() - self.cursor_pos = api.nvim_win_get_cursor(code_winid) - self.code_winid = code_winid - local code_lines = api.nvim_buf_get_lines(code_bufnr, 0, -1, false) - local code_content = table.concat(code_lines, "\n") + self.code_bufnr = api.nvim_get_current_buf() + self.code_winid = api.nvim_get_current_win() + self.cursor_pos = api.nvim_win_get_cursor(self.code_winid) + local code_lines = api.nvim_buf_get_lines(self.code_bufnr, 0, -1, false) - self.selection = Utils.get_visual_selection_and_range() + if line1 ~= nil and line2 ~= nil then + local filepath = vim.fn.expand("%:p") + local filetype = Utils.get_filetype(filepath) + local content_lines = vim.list_slice(code_lines, line1, line2) + local content = table.concat(content_lines, "\n") + local range = Range:new( + { lnum = line1, col = #content_lines[1] }, + { lnum = line2, col = #content_lines[#content_lines] } + ) + self.selection = SelectionResult:new(filepath, filetype, content, range) + else + self.selection = Utils.get_visual_selection_and_range() + end if self.selection == nil then Utils.error("No visual selection found", { once = true, title = "Avante" }) @@ -138,116 +269,18 @@ function Selection:create_editing_input() end_col = math.min(self.selection.range.finish.col, #code_lines[self.selection.range.finish.lnum]) end - self.selected_code_extmark_id = api.nvim_buf_set_extmark(code_bufnr, SELECTED_CODE_NAMESPACE, start_row, start_col, { - hl_group = "Visual", - hl_mode = "combine", - end_row = end_row, - end_col = end_col, - priority = PRIORITY, - }) - - local function submit_input(input) - local full_response = "" - local start_line = self.selection.range.start.lnum - local finish_line = self.selection.range.finish.lnum - - local original_first_line_indentation = Utils.get_indentation(code_lines[self.selection.range.start.lnum]) - - local need_prepend_indentation = false - - self.prompt_input:start_spinner() - - ---@type AvanteLLMStartCallback - local function on_start(start_opts) end - - ---@type AvanteLLMChunkCallback - local function on_chunk(chunk) - full_response = full_response .. chunk - local response_lines_ = vim.split(full_response, "\n") - local response_lines = {} - local in_code_block = false - for _, line in ipairs(response_lines_) do - if line:match("^") then - in_code_block = true - line = line:gsub("^", "") - if line ~= "" then table.insert(response_lines, line) end - elseif line:match("") then - in_code_block = false - line = line:gsub(".*$", "") - if line ~= "" then table.insert(response_lines, line) end - elseif in_code_block then - table.insert(response_lines, line) - end - end - if #response_lines == 1 then - local first_line = response_lines[1] - local first_line_indentation = Utils.get_indentation(first_line) - need_prepend_indentation = first_line_indentation ~= original_first_line_indentation - end - if need_prepend_indentation then - for i, line in ipairs(response_lines) do - response_lines[i] = original_first_line_indentation .. line - end - end - api.nvim_buf_set_lines(code_bufnr, start_line - 1, finish_line, true, response_lines) - finish_line = start_line + #response_lines - 1 - end - - ---@type AvanteLLMStopCallback - local function on_stop(stop_opts) - if stop_opts.error then - -- NOTE: in Ubuntu 22.04+ you will see this ignorable error from ~/.local/share/nvim/lazy/avante.nvim/lua/avante/llm.lua `on_error = function(err)`, check to avoid showing this error. - if type(stop_opts.error) == "table" and stop_opts.error.exit == nil and stop_opts.error.stderr == "{}" then - return - end - Utils.error( - "Error occurred while processing the response: " .. vim.inspect(stop_opts.error), - { once = true, title = "Avante" } - ) - return - end - self.prompt_input:stop_spinner() - vim.defer_fn(function() self:close_editing_input() end, 0) - Utils.debug("full response:", full_response) - end - - local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr }) - local file_ext = api.nvim_buf_get_name(code_bufnr):match("^.+%.(.+)$") - - local mentions = Utils.extract_mentions(input) - input = mentions.new_content - local project_context = mentions.enable_project_context and RepoMap.get_repo_map(file_ext) or nil - - local diagnostics = Utils.get_current_selection_diagnostics(code_bufnr, self.selection) - - ---@type AvanteSelectedCode | nil - local selected_code = nil - - if self.selection then - selected_code = { - content = self.selection.content, - file_type = self.selection.filetype, - path = self.selection.filepath, - } - end - - Llm.stream({ - ask = true, - project_context = vim.json.encode(project_context), - diagnostics = vim.json.encode(diagnostics), - selected_files = { { content = code_content, file_type = filetype, path = "" } }, - code_lang = filetype, - selected_code = selected_code, - instructions = input, - mode = "editing", - on_start = on_start, - on_chunk = on_chunk, - on_stop = on_stop, + self.selected_code_extmark_id = + api.nvim_buf_set_extmark(self.code_bufnr, SELECTED_CODE_NAMESPACE, start_row, start_col, { + hl_group = "Visual", + hl_mode = "combine", + end_row = end_row, + end_col = end_col, + priority = PRIORITY, }) - end local prompt_input = PromptInput:new({ - submit_callback = submit_input, + default_value = request, + submit_callback = function(input) self:submit_input(input) end, cancel_callback = function() self:close_editing_input() end, win_opts = { border = Config.windows.edit.border, @@ -285,6 +318,13 @@ end function Selection:setup_autocmds() Selection.did_setup = true + + api.nvim_create_autocmd("User", { + group = self.augroup, + pattern = "AvanteEditSubmitted", + callback = function(ev) self:submit_input(ev.data.request) end, + }) + api.nvim_create_autocmd({ "ModeChanged" }, { group = self.augroup, pattern = { "n:v", "n:V", "n:" }, -- Entering Visual mode from Normal mode diff --git a/lua/avante/selection_result.lua b/lua/avante/selection_result.lua index 051670c..d928fce 100644 --- a/lua/avante/selection_result.lua +++ b/lua/avante/selection_result.lua @@ -7,6 +7,8 @@ local SelectionResult = {} SelectionResult.__index = SelectionResult -- Create a selection content and range +---@param filepath string Filepath of the selected content +---@param filetype string Filetype of the selected content ---@param content string Selected content ---@param range avante.Range Selection range function SelectionResult:new(filepath, filetype, content, range) diff --git a/lua/avante/ui/prompt_input.lua b/lua/avante/ui/prompt_input.lua index c630c0d..82684d7 100644 --- a/lua/avante/ui/prompt_input.lua +++ b/lua/avante/ui/prompt_input.lua @@ -114,6 +114,8 @@ function PromptInput:open() self.winid = winid api.nvim_set_option_value("wrap", false, { win = winid }) + api.nvim_set_option_value("winblend", 5, { win = winid }) + api.nvim_set_option_value("winhighlight", "FloatBorder:NormalFloat", { win = winid }) api.nvim_set_option_value("cursorline", true, { win = winid }) api.nvim_set_option_value("modifiable", true, { buf = bufnr }) @@ -200,6 +202,7 @@ function PromptInput:show_shortcuts_hints() } self.shortcuts_hints_winid = api.nvim_open_win(buf, false, opts) + api.nvim_set_option_value("winblend", 10, { win = self.shortcuts_hints_winid }) end function PromptInput:close_shortcuts_hints() diff --git a/plugin/avante.lua b/plugin/avante.lua index 77b9c7b..177b726 100644 --- a/plugin/avante.lua +++ b/plugin/avante.lua @@ -46,7 +46,7 @@ end ---@param n string ---@param c vim.api.keyset.user_command.callback ---@param o vim.api.keyset.user_command.opts -local cmd = function(n, c, o) +local function cmd(n, c, o) o = vim.tbl_extend("force", { nargs = 0 }, o or {}) api.nvim_create_user_command("Avante" .. n, c, o) end @@ -102,8 +102,8 @@ end, { }) cmd( "Edit", - function(opts) require("avante.api").edit(vim.trim(opts.args)) end, - { desc = "avante: edit selected block", nargs = "*" } + function(opts) require("avante.api").edit(vim.trim(opts.args), opts.line1, opts.line2) end, + { desc = "avante: edit selected block", nargs = "*", range = 2 } ) cmd("Refresh", function() require("avante.api").refresh() end, { desc = "avante: refresh windows" }) cmd("Focus", function() require("avante.api").focus() end, { desc = "avante: switch focus windows" })