From 65e1e178f5bd71690418c949e7d152c12a870c22 Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 3 Sep 2024 14:03:59 +0800 Subject: [PATCH] feat: automatic suggestion (smart tab) (#455) --- README.md | 13 ++ lua/avante/api.lua | 8 +- lua/avante/config.lua | 7 + lua/avante/highlights.lua | 4 +- lua/avante/init.lua | 28 ++- lua/avante/llm.lua | 66 +++++-- lua/avante/selection.lua | 2 +- lua/avante/sidebar.lua | 25 +-- lua/avante/suggestion.lua | 404 ++++++++++++++++++++++++++++++++++++++ lua/avante/utils/init.lua | 78 +++++++- 10 files changed, 592 insertions(+), 43 deletions(-) create mode 100644 lua/avante/suggestion.lua diff --git a/README.md b/README.md index d978006d2..c88b95d41 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,13 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_ temperature = 0, max_tokens = 4096, }, + behaviour = { + auto_suggestions = false, -- Experimental stage + auto_set_highlight_group = true, + auto_set_keymaps = true, + auto_apply_diff_after_generation = false, + support_paste_from_clipboard = false, + }, mappings = { --- @class AvanteConflictMappings diff = { @@ -203,6 +210,12 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_ next = "]x", prev = "[x", }, + suggestion = { + accept = "", + next = "", + prev = "", + dismiss = "", + }, jump = { next = "]]", prev = "[[", diff --git a/lua/avante/api.lua b/lua/avante/api.lua index 88250bf91..5ee45fc77 100644 --- a/lua/avante/api.lua +++ b/lua/avante/api.lua @@ -45,8 +45,14 @@ M.edit = function(question) end end +---@return avante.Suggestion | nil +M.get_suggestion = function() + local _, _, suggestion = require("avante").get() + return suggestion +end + M.refresh = function() - local sidebar, _ = require("avante").get() + local sidebar = require("avante").get() if not sidebar then return end diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 286a911a7..878dc062d 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -86,6 +86,7 @@ M.defaults = { ---3. auto_set_highlight_group : Whether to automatically set the highlight group for the current line. Default to true. ---4. support_paste_from_clipboard : Whether to support pasting image from clipboard. This will be determined automatically based whether img-clip is available or not. behaviour = { + auto_suggestions = false, -- Experimental stage auto_set_highlight_group = true, auto_set_keymaps = true, auto_apply_diff_after_generation = false, @@ -116,6 +117,12 @@ M.defaults = { next = "]x", prev = "[x", }, + suggestion = { + accept = "", + next = "", + prev = "", + dismiss = "", + }, jump = { next = "]]", prev = "[[", diff --git a/lua/avante/highlights.lua b/lua/avante/highlights.lua index 4494d1e9d..b48e33682 100644 --- a/lua/avante/highlights.lua +++ b/lua/avante/highlights.lua @@ -11,6 +11,8 @@ local Highlights = { REVERSED_SUBTITLE = { name = "AvanteReversedSubtitle", fg = "#56b6c2" }, THIRD_TITLE = { name = "AvanteThirdTitle", fg = "#ABB2BF", bg = "#353B45" }, REVERSED_THIRD_TITLE = { name = "AvanteReversedThirdTitle", fg = "#353B45" }, + SUGGESTION = { name = "AvanteSuggestion", link = "Comment" }, + ANNOTATION = { name = "AvanteAnnotation", link = "Comment" }, } Highlights.conflict = { @@ -48,7 +50,7 @@ M.setup = function() end) :each(function(_, hl) if not has_set_colors(hl.name) then - api.nvim_set_hl(0, hl.name, { fg = hl.fg or nil, bg = hl.bg or nil }) + api.nvim_set_hl(0, hl.name, { fg = hl.fg or nil, bg = hl.bg or nil, link = hl.link or nil }) end end) diff --git a/lua/avante/init.lua b/lua/avante/init.lua index 967dbd0a6..97574460d 100644 --- a/lua/avante/init.lua +++ b/lua/avante/init.lua @@ -3,6 +3,7 @@ local api = vim.api local Utils = require("avante.utils") local Sidebar = require("avante.sidebar") local Selection = require("avante.selection") +local Suggestion = require("avante.suggestion") local Config = require("avante.config") local Diff = require("avante.diff") @@ -12,8 +13,10 @@ local M = { sidebars = {}, ---@type avante.Selection[] selections = {}, - ---@type {sidebar?: avante.Sidebar, selection?: avante.Selection} - current = { sidebar = nil, selection = nil }, + ---@type avante.Suggestion[] + suggestions = {}, + ---@type {sidebar?: avante.Sidebar, selection?: avante.Selection, suggestion?: avante.Suggestion} + current = { sidebar = nil, selection = nil, suggestion = nil }, } M.did_setup = false @@ -185,7 +188,7 @@ H.autocmds = function() api.nvim_create_autocmd("VimResized", { group = H.augroup, callback = function() - local sidebar, _ = M.get() + local sidebar = M.get() if not sidebar then return end @@ -234,22 +237,25 @@ H.autocmds = function() end ---@param current boolean? false to disable setting current, otherwise use this to track across tabs. ----@return avante.Sidebar, avante.Selection +---@return avante.Sidebar, avante.Selection, avante.Suggestion function M.get(current) local tab = api.nvim_get_current_tabpage() local sidebar = M.sidebars[tab] local selection = M.selections[tab] + local suggestion = M.suggestions[tab] if current ~= false then M.current.sidebar = sidebar M.current.selection = selection + M.current.suggestion = suggestion end - return sidebar, selection + return sidebar, selection, suggestion end ---@param id integer function M._init(id) local sidebar = M.sidebars[id] local selection = M.selections[id] + local suggestion = M.suggestions[id] if not sidebar then sidebar = Sidebar:new(id) @@ -259,7 +265,11 @@ function M._init(id) selection = Selection:new(id) M.selections[id] = selection end - M.current = { sidebar = sidebar, selection = selection } + if not suggestion then + suggestion = Suggestion:new(id) + M.suggestions[id] = suggestion + end + M.current = { sidebar = sidebar, selection = selection, suggestion = suggestion } return M end @@ -288,7 +298,7 @@ M.toggle.hint = H.api(Utils.toggle_wrap({ setmetatable(M.toggle, { __index = M.toggle, __call = function() - local sidebar, _ = M.get() + local sidebar = M.get() if not sidebar then M._init(api.nvim_get_current_tabpage()) M.current.sidebar:open() @@ -326,7 +336,7 @@ M.build = H.api(function() local os_name = Utils.get_os_name() if vim.tbl_contains({ "linux", "darwin" }, os_name) then - cmd = { "sh", "-c", ("make -C %s"):format(build_directory) } + cmd = { "sh", "-c", string.format("make -C %s", build_directory) } elseif os_name == "windows" then build_directory = to_windows_path(build_directory) cmd = { @@ -334,7 +344,7 @@ M.build = H.api(function() "-ExecutionPolicy", "Bypass", "-File", - ("%s\\Build.ps1"):format(build_directory), + string.format("%s\\Build.ps1", build_directory), "-WorkingDirectory", build_directory, } diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index 0f64f80c5..2623df46a 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -88,8 +88,36 @@ Your task is to modify the provided code according to the user's request. Follow Remember: Your response should contain ONLY the modified code, ready to be used as a direct replacement for the original file. ]] +local suggesting_mode_user_prompt_tpl = [[ +Your task is to suggest code modifications at the cursor position. Follow these instructions meticulously: + +1. Carefully analyze the original code, paying close attention to its structure and the cursor position. + +2. You must follow this json format when suggesting modifications: + +[ + { + "row": ${row}, + "col": ${column}, + "content": "Your suggested code here" + } +] + +3. When suggesting suggested code: + - Each element in the returned list is a COMPLETE and INDEPENDENT code snippet. + - MUST be a valid json format. Don't be lazy! + - Only return the new code to be inserted. + - Your returned code should not overlap with the original code in any way. Don't be lazy! + - Please strictly check the code around the position and ensure that the complete code after insertion is correct. Don't be lazy! + - Do not return the entire file content or any surrounding code. + - Do not include any explanations, comments, or line numbers in your response. + - Ensure the suggested code fits seamlessly with the existing code structure and indentation. + - If there are no recommended modifications, return an empty list. + +Remember: Return ONLY the suggested code snippet, without any additional formatting or explanation. +]] + local group = api.nvim_create_augroup("avante_llm", { clear = true }) -local active_job = nil ---@class StreamOptions ---@field file_content string @@ -99,7 +127,7 @@ local active_job = nil ---@field project_context string | nil ---@field memory_context string | nil ---@field full_file_contents_context string | nil ----@field mode "planning" | "editing" +---@field mode "planning" | "editing" | "suggesting" ---@field on_chunk AvanteChunkParser ---@field on_complete AvanteCompleteParser @@ -108,7 +136,13 @@ M.stream = function(opts) local mode = opts.mode or "planning" local provider = Config.provider - local user_prompt_tpl = mode == "planning" and planning_mode_user_prompt_tpl or editing_mode_user_prompt_tpl + local user_prompt_tpl = planning_mode_user_prompt_tpl + + if mode == "editing" then + user_prompt_tpl = editing_mode_user_prompt_tpl + elseif mode == "suggesting" then + user_prompt_tpl = suggesting_mode_user_prompt_tpl + end -- Check if the instructions contains an image path local image_paths = {} @@ -191,13 +225,10 @@ M.stream = function(opts) end end - if active_job then - active_job:shutdown() - active_job = nil - end - local completed = false + local active_job + active_job = curl.post(spec.url, { headers = spec.headers, proxy = spec.proxy, @@ -230,11 +261,13 @@ M.stream = function(opts) end end) end, - on_error = function(err) + on_error = function() + active_job = nil completed = true - opts.on_complete(err) + opts.on_complete(nil) end, callback = function(result) + active_job = nil if result.status >= 400 then if Provider.on_error then Provider.on_error(result) @@ -250,16 +283,21 @@ M.stream = function(opts) end end) end - active_job = nil end, }) api.nvim_create_autocmd("User", { group = group, pattern = M.CANCEL_PATTERN, + once = true, callback = function() + -- Error: cannot resume dead coroutine if active_job then - active_job:shutdown() + xpcall(function() + active_job:shutdown() + end, function(err) + return err + end) Utils.debug("LLM request cancelled", { title = "Avante" }) active_job = nil end @@ -269,4 +307,8 @@ M.stream = function(opts) return active_job end +function M.cancel_inflight_request() + api.nvim_exec_autocmds("User", { pattern = M.CANCEL_PATTERN }) +end + return M diff --git a/lua/avante/selection.lua b/lua/avante/selection.lua index 03db43b30..1d36f8a86 100644 --- a/lua/avante/selection.lua +++ b/lua/avante/selection.lua @@ -84,7 +84,7 @@ end function Selection:close_editing_input() self:close_editing_input_shortcuts_hints() - api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN }) + Llm.cancel_inflight_request() if api.nvim_get_mode().mode == "i" then vim.cmd([[stopinsert]]) end diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index e95c9273d..252683092 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -448,7 +448,7 @@ local function insert_conflict_contents(bufnr, snippets) local snippet_lines = vim.split(snippet.content, "\n") for idx, line in ipairs(snippet_lines) do - line = line:gsub("^L%d+: ", "") + line = Utils.trim_line_number(line) if idx == 1 then local indentation = Utils.get_indentation(line) need_prepend_indentation = indentation ~= original_start_line_indentation @@ -824,7 +824,7 @@ function Sidebar:on_mount() self:render_input() self:render_selected_code() - self.augroup = api.nvim_create_augroup("avante_" .. self.id .. self.result.winid, { clear = true }) + self.augroup = api.nvim_create_augroup("avante_sidebar_" .. self.id .. self.result.winid, { clear = true }) local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr }) @@ -1040,17 +1040,6 @@ function Sidebar:update_content(content, opts) return self end -local function prepend_line_number(content, start_line) - start_line = start_line or 1 - local lines = vim.split(content, "\n") - local result = {} - for i, line in ipairs(lines) do - i = i + start_line - 1 - table.insert(result, "L" .. i .. ": " .. line) - end - return table.concat(result, "\n") -end - -- Function to get current timestamp local function get_timestamp() return os.date("%Y-%m-%d %H:%M:%S") @@ -1253,14 +1242,14 @@ function Sidebar:create_input() self:update_content(content_prefix .. "🔄 **Generating response ...**\n") local content = table.concat(Utils.get_buf_lines(0, -1, self.code.bufnr), "\n") - local content_with_line_numbers = prepend_line_number(content) + local content_with_line_numbers = Utils.prepend_line_number(content) local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr }) local selected_code_content_with_line_numbers = nil if self.code.selection ~= nil then selected_code_content_with_line_numbers = - prepend_line_number(self.code.selection.content, self.code.selection.range.start.line) + Utils.prepend_line_number(self.code.selection.content, self.code.selection.range.start.line) end if request:sub(1, 1) == "/" then @@ -1289,7 +1278,7 @@ function Sidebar:create_input() Utils.error("Invalid end line number", { once = true, title = "Avante" }) return end - selected_code_content_with_line_numbers = prepend_line_number( + selected_code_content_with_line_numbers = Utils.prepend_line_number( table.concat(api.nvim_buf_get_lines(self.code.bufnr, start_line - 1, end_line, false), "\n"), start_line ) @@ -1640,12 +1629,12 @@ function Sidebar:render() end) self.result:map("n", "q", function() - api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN }) + Llm.cancel_inflight_request() self:close() end) self.result:map("n", "", function() - api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN }) + Llm.cancel_inflight_request() self:close() end) diff --git a/lua/avante/suggestion.lua b/lua/avante/suggestion.lua new file mode 100644 index 000000000..4d7f5df70 --- /dev/null +++ b/lua/avante/suggestion.lua @@ -0,0 +1,404 @@ +local Utils = require("avante.utils") +local Llm = require("avante.llm") +local Highlights = require("avante.highlights") +local Config = require("avante.config") +local api = vim.api +local fn = vim.fn + +local SUGGESTION_NS = api.nvim_create_namespace("avante_suggestion") + +---@class avante.SuggestionItem +---@field content string +---@field row number +---@field col number + +---@class avante.SuggestionContext +---@field suggestions avante.SuggestionItem[] +---@field current_suggestion_idx number +---@field prev_doc? table + +---@class avante.Suggestion +---@field id number +---@field augroup integer +---@field extmark_id integer +---@field _timer? table +---@field _contexts table +local Suggestion = {} + +---@param id number +---@return avante.Suggestion +function Suggestion:new(id) + local o = { id = id, suggestions = {} } + setmetatable(o, self) + self.__index = self + self.augroup = api.nvim_create_augroup("avante_suggestion_" .. id, { clear = true }) + self.extmark_id = 1 + self._timer = nil + self._contexts = {} + if Config.behaviour.auto_suggestions then + self:setup_mappings() + self:setup_autocmds() + end + return o +end + +function Suggestion:destroy() + self:stop_timer() + self:reset() + self:delete_autocmds() + api.nvim_del_namespace(SUGGESTION_NS) +end + +function Suggestion:setup_mappings() + if not Config.behaviour.auto_set_keymaps then + return + end + if Config.mappings.suggestion and Config.mappings.suggestion.accept then + vim.keymap.set("i", Config.mappings.suggestion.accept, function() + self:accept() + end, { + desc = "[avante] accept suggestion", + noremap = true, + silent = true, + }) + end + + if Config.mappings.suggestion and Config.mappings.suggestion.dismiss then + vim.keymap.set("i", Config.mappings.suggestion.dismiss, function() + if self:is_visible() then + self:dismiss() + end + end, { + desc = "[avante] dismiss suggestion", + noremap = true, + silent = true, + }) + end + + if Config.mappings.suggestion and Config.mappings.suggestion.next then + vim.keymap.set("i", Config.mappings.suggestion.next, function() + self:next() + end, { + desc = "[avante] next suggestion", + noremap = true, + silent = true, + }) + end + + if Config.mappings.suggestion and Config.mappings.suggestion.prev then + vim.keymap.set("i", Config.mappings.suggestion.prev, function() + self:prev() + end, { + desc = "[avante] previous suggestion", + noremap = true, + silent = true, + }) + end +end + +function Suggestion:suggest() + Utils.debug("suggesting") + + local ctx = self:ctx() + local doc = Utils.get_doc() + ctx.prev_doc = doc + + local bufnr = api.nvim_get_current_buf() + local filetype = api.nvim_get_option_value("filetype", { buf = bufnr }) + local code_content = + Utils.prepend_line_number(table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n\n") + + local full_response = "" + + Llm.stream({ + file_content = code_content, + code_lang = filetype, + instructions = vim.json.encode(doc), + mode = "suggesting", + on_chunk = function(chunk) + full_response = full_response .. chunk + end, + on_complete = function(err) + if err then + Utils.error("Error while suggesting: " .. vim.inspect(err), { once = true, title = "Avante" }) + return + end + Utils.debug("full_response: " .. vim.inspect(full_response)) + local cursor_row, cursor_col = Utils.get_cursor_pos() + if cursor_row ~= doc.position.row or cursor_col ~= doc.position.col then + return + end + local ok, suggestions = pcall(vim.json.decode, full_response) + if not ok then + Utils.error("Error while decoding suggestions: " .. full_response, { once = true, title = "Avante" }) + return + end + if not suggestions then + Utils.info("No suggestions found", { once = true, title = "Avante" }) + return + end + suggestions = vim + .iter(suggestions) + :map(function(s) + return { row = s.row, col = s.col, content = Utils.trim_all_line_numbers(s.content) } + end) + :totable() + ctx.suggestions = suggestions + ctx.current_suggestion_idx = 1 + self:show() + end, + }) +end + +function Suggestion:show() + self:hide() + + if not fn.mode():match("^[iR]") then + return + end + + local ctx = self:ctx() + local suggestion = ctx.suggestions[ctx.current_suggestion_idx] + if not suggestion then + return + end + + local cursor_row, cursor_col = Utils.get_cursor_pos() + + if suggestion.row < cursor_row then + return + end + + local bufnr = api.nvim_get_current_buf() + local row = suggestion.row + local col = suggestion.col + local content = suggestion.content + + local lines = vim.split(content, "\n") + + local extmark_col = cursor_col + + if cursor_row < row then + extmark_col = 0 + end + + local current_lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) + + if cursor_row == row then + local cursor_line_col = #current_lines[cursor_row] - 1 + if cursor_col ~= cursor_line_col then + local current_line = current_lines[cursor_row] + lines[1] = lines[1] .. current_line:sub(col + 1, -1) + end + end + + local extmark = { + id = self.extmark_id, + virt_text_win_col = col, + virt_text = { { lines[1], Highlights.SUGGESTION } }, + } + + if #lines > 1 then + extmark.virt_lines = {} + for i = 2, #lines do + extmark.virt_lines[i - 1] = { { lines[i], Highlights.SUGGESTION } } + end + end + + extmark.hl_mode = "combine" + + local buf_lines = Utils.get_buf_lines(0, -1, bufnr) + local buf_lines_count = #buf_lines + + while buf_lines_count < row do + api.nvim_buf_set_lines(bufnr, buf_lines_count, -1, false, { "" }) + buf_lines_count = buf_lines_count + 1 + end + + api.nvim_buf_set_extmark(bufnr, SUGGESTION_NS, row - 1, extmark_col, extmark) +end + +function Suggestion:is_visible() + return not not api.nvim_buf_get_extmark_by_id(0, SUGGESTION_NS, self.extmark_id, { details = false })[1] +end + +function Suggestion:hide() + api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id) +end + +function Suggestion:ctx() + local bufnr = api.nvim_get_current_buf() + local ctx = self._contexts[bufnr] + if not ctx then + ctx = { + suggestions = {}, + current_suggestion_idx = 0, + prev_doc = {}, + } + self._contexts[bufnr] = ctx + end + return ctx +end + +function Suggestion:reset() + self._timer = nil + local bufnr = api.nvim_get_current_buf() + self._contexts[bufnr] = nil +end + +function Suggestion:stop_timer() + if self._timer then + pcall(function() + fn.timer_stop(self._timer) + end) + self._timer = nil + end +end + +function Suggestion:next() + local ctx = self:ctx() + if #ctx.suggestions == 0 then + return + end + ctx.current_suggestion_idx = (ctx.current_suggestion_idx % #ctx.suggestions) + 1 + self:show() +end + +function Suggestion:prev() + local ctx = self:ctx() + if #ctx.suggestions == 0 then + return + end + ctx.current_suggestion_idx = ((ctx.current_suggestion_idx - 2 + #ctx.suggestions) % #ctx.suggestions) + 1 + self:show() +end + +function Suggestion:dismiss() + self:stop_timer() + self:hide() + self:reset() +end + +function Suggestion:accept() + -- Llm.cancel_inflight_request() + api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id) + local ctx = self:ctx() + local suggestion = ctx.suggestions and ctx.suggestions[ctx.current_suggestion_idx] or nil + if not suggestion then + if Config.mappings.suggestion and Config.mappings.suggestion.accept == "" then + api.nvim_feedkeys(api.nvim_replace_termcodes("", true, false, true), "n", true) + end + return + end + local bufnr = api.nvim_get_current_buf() + local current_lines = Utils.get_buf_lines(0, -1, bufnr) + local row = suggestion.row + local col = suggestion.col + local content = suggestion.content + local lines = vim.split(content, "\n") + local cursor_row, cursor_col = Utils.get_cursor_pos() + if row > cursor_row then + api.nvim_buf_set_lines(bufnr, row - 1, row - 1, false, { "" }) + end + local line_count = #lines + if line_count > 0 then + if cursor_row == row then + local cursor_line_col = #current_lines[cursor_row] - 1 + if cursor_col ~= cursor_line_col then + local current_line_ = current_lines[cursor_row] + lines[1] = lines[1] .. current_line_:sub(col + 1, -1) + end + end + local current_line = current_lines[row] or "" + local current_line_max_col = #current_line - 1 + local start_col = col + if start_col > current_line_max_col then + lines[1] = string.rep(" ", start_col - current_line_max_col - 1) .. lines[1] + start_col = -1 + end + api.nvim_buf_set_text(bufnr, row - 1, start_col, row - 1, -1, { lines[1] }) + if #lines > 1 then + local insert_lines = vim.list_slice(lines, 2) + api.nvim_buf_set_lines(bufnr, row, row, true, insert_lines) + end + end + + local down_count = line_count - 1 + if row > cursor_row then + down_count = down_count + 1 + end + + local cursor_keys = string.rep("", down_count) .. "" + api.nvim_feedkeys(api.nvim_replace_termcodes(cursor_keys, true, false, true), "n", false) + + self:hide() + self:reset() +end + +function Suggestion:setup_autocmds() + local last_cursor_pos = {} + + local check_for_suggestion = Utils.debounce(function() + local current_cursor_pos = api.nvim_win_get_cursor(0) + if last_cursor_pos[1] == current_cursor_pos[1] and last_cursor_pos[2] == current_cursor_pos[2] then + self:suggest() + end + end, 77) + + local function suggest_callback() + if not vim.bo.buflisted then + return + end + + if vim.bo.buftype ~= "" then + return + end + + local ctx = self:ctx() + + if ctx.prev_doc and vim.deep_equal(ctx.prev_doc, Utils.get_doc()) then + return + end + + self:hide() + last_cursor_pos = api.nvim_win_get_cursor(0) + self._timer = check_for_suggestion() + end + + api.nvim_create_autocmd("InsertEnter", { + group = self.augroup, + callback = suggest_callback, + }) + + api.nvim_create_autocmd("BufEnter", { + group = self.augroup, + callback = function() + if fn.mode():match("^[iR]") then + suggest_callback() + end + end, + }) + + api.nvim_create_autocmd("CursorMovedI", { + group = self.augroup, + callback = suggest_callback, + }) + + api.nvim_create_autocmd("InsertLeave", { + group = self.augroup, + callback = function() + last_cursor_pos = {} + self:hide() + self:reset() + end, + }) +end + +function Suggestion:delete_autocmds() + if self.augroup then + api.nvim_del_augroup_by_id(self.augroup) + end + self.augroup = nil +end + +return Suggestion diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index ca9372635..cb6c6925c 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -1,4 +1,6 @@ local api = vim.api +local fn = vim.fn +local lsp = vim.lsp ---@class avante.utils: LazyUtilCore ---@field tokens avante.utils.tokens @@ -338,7 +340,7 @@ function M.debug(msg, opts) end opts = opts or {} if opts.title then - opts.title = "lazy.nvim: " .. opts.title + opts.title = "avante.nvim: " .. opts.title end if type(msg) == "string" then M.notify(msg, opts) @@ -484,4 +486,78 @@ function M.remove_indentation(code) return code:gsub("^%s*", "") end +local function relative_path(absolute) + local relative = fn.fnamemodify(absolute, ":.") + if string.sub(relative, 0, 1) == "/" then + return fn.fnamemodify(absolute, ":t") + end + return relative +end + +function M.get_doc() + local absolute = api.nvim_buf_get_name(0) + local params = lsp.util.make_position_params(0, "utf-8") + + local position = { + row = params.position.line + 1, + col = params.position.character, + } + + local doc = { + uri = params.textDocument.uri, + version = api.nvim_buf_get_var(0, "changedtick"), + relativePath = relative_path(absolute), + insertSpaces = vim.o.expandtab, + tabSize = fn.shiftwidth(), + indentSize = fn.shiftwidth(), + position = position, + } + + return doc +end + +function M.prepend_line_number(content, start_line) + start_line = start_line or 1 + local lines = vim.split(content, "\n") + local result = {} + for i, line in ipairs(lines) do + i = i + start_line - 1 + table.insert(result, "L" .. i .. ": " .. line) + end + return table.concat(result, "\n") +end + +function M.trim_line_number(line) + return line:gsub("^L%d+: ", "") +end + +function M.trim_all_line_numbers(content) + return vim + .iter(vim.split(content, "\n")) + :map(function(line) + local new_line = M.trim_line_number(line) + return new_line + end) + :join("\n") +end + +function M.debounce(func, delay) + local timer_id = nil + + return function(...) + local args = { ... } + + if timer_id then + fn.timer_stop(timer_id) + end + + timer_id = fn.timer_start(delay, function() + func(unpack(args)) + timer_id = nil + end) + + return timer_id + end +end + return M