feat: automatic suggestion (smart tab) (#455)
This commit is contained in:
parent
962dd0a759
commit
65e1e178f5
13
README.md
13
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 = "<M-l>",
|
||||
next = "<M-]>",
|
||||
prev = "<M-[>",
|
||||
dismiss = "<C-]>",
|
||||
},
|
||||
jump = {
|
||||
next = "]]",
|
||||
prev = "[[",
|
||||
|
@ -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
|
||||
|
@ -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 = "<M-l>",
|
||||
next = "<M-]>",
|
||||
prev = "<M-[>",
|
||||
dismiss = "<C-]>",
|
||||
},
|
||||
jump = {
|
||||
next = "]]",
|
||||
prev = "[[",
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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", "<Esc>", function()
|
||||
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
|
||||
Llm.cancel_inflight_request()
|
||||
self:close()
|
||||
end)
|
||||
|
||||
|
404
lua/avante/suggestion.lua
Normal file
404
lua/avante/suggestion.lua
Normal file
@ -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 == "<Tab>" then
|
||||
api.nvim_feedkeys(api.nvim_replace_termcodes("<Tab>", 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>", down_count) .. "<End>"
|
||||
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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user