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,
|
temperature = 0,
|
||||||
max_tokens = 4096,
|
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 = {
|
mappings = {
|
||||||
--- @class AvanteConflictMappings
|
--- @class AvanteConflictMappings
|
||||||
diff = {
|
diff = {
|
||||||
@ -203,6 +210,12 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_
|
|||||||
next = "]x",
|
next = "]x",
|
||||||
prev = "[x",
|
prev = "[x",
|
||||||
},
|
},
|
||||||
|
suggestion = {
|
||||||
|
accept = "<M-l>",
|
||||||
|
next = "<M-]>",
|
||||||
|
prev = "<M-[>",
|
||||||
|
dismiss = "<C-]>",
|
||||||
|
},
|
||||||
jump = {
|
jump = {
|
||||||
next = "]]",
|
next = "]]",
|
||||||
prev = "[[",
|
prev = "[[",
|
||||||
|
@ -45,8 +45,14 @@ M.edit = function(question)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return avante.Suggestion | nil
|
||||||
|
M.get_suggestion = function()
|
||||||
|
local _, _, suggestion = require("avante").get()
|
||||||
|
return suggestion
|
||||||
|
end
|
||||||
|
|
||||||
M.refresh = function()
|
M.refresh = function()
|
||||||
local sidebar, _ = require("avante").get()
|
local sidebar = require("avante").get()
|
||||||
if not sidebar then
|
if not sidebar then
|
||||||
return
|
return
|
||||||
end
|
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.
|
---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.
|
---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 = {
|
behaviour = {
|
||||||
|
auto_suggestions = false, -- Experimental stage
|
||||||
auto_set_highlight_group = true,
|
auto_set_highlight_group = true,
|
||||||
auto_set_keymaps = true,
|
auto_set_keymaps = true,
|
||||||
auto_apply_diff_after_generation = false,
|
auto_apply_diff_after_generation = false,
|
||||||
@ -116,6 +117,12 @@ M.defaults = {
|
|||||||
next = "]x",
|
next = "]x",
|
||||||
prev = "[x",
|
prev = "[x",
|
||||||
},
|
},
|
||||||
|
suggestion = {
|
||||||
|
accept = "<M-l>",
|
||||||
|
next = "<M-]>",
|
||||||
|
prev = "<M-[>",
|
||||||
|
dismiss = "<C-]>",
|
||||||
|
},
|
||||||
jump = {
|
jump = {
|
||||||
next = "]]",
|
next = "]]",
|
||||||
prev = "[[",
|
prev = "[[",
|
||||||
|
@ -11,6 +11,8 @@ local Highlights = {
|
|||||||
REVERSED_SUBTITLE = { name = "AvanteReversedSubtitle", fg = "#56b6c2" },
|
REVERSED_SUBTITLE = { name = "AvanteReversedSubtitle", fg = "#56b6c2" },
|
||||||
THIRD_TITLE = { name = "AvanteThirdTitle", fg = "#ABB2BF", bg = "#353B45" },
|
THIRD_TITLE = { name = "AvanteThirdTitle", fg = "#ABB2BF", bg = "#353B45" },
|
||||||
REVERSED_THIRD_TITLE = { name = "AvanteReversedThirdTitle", fg = "#353B45" },
|
REVERSED_THIRD_TITLE = { name = "AvanteReversedThirdTitle", fg = "#353B45" },
|
||||||
|
SUGGESTION = { name = "AvanteSuggestion", link = "Comment" },
|
||||||
|
ANNOTATION = { name = "AvanteAnnotation", link = "Comment" },
|
||||||
}
|
}
|
||||||
|
|
||||||
Highlights.conflict = {
|
Highlights.conflict = {
|
||||||
@ -48,7 +50,7 @@ M.setup = function()
|
|||||||
end)
|
end)
|
||||||
:each(function(_, hl)
|
:each(function(_, hl)
|
||||||
if not has_set_colors(hl.name) then
|
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
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ local api = vim.api
|
|||||||
local Utils = require("avante.utils")
|
local Utils = require("avante.utils")
|
||||||
local Sidebar = require("avante.sidebar")
|
local Sidebar = require("avante.sidebar")
|
||||||
local Selection = require("avante.selection")
|
local Selection = require("avante.selection")
|
||||||
|
local Suggestion = require("avante.suggestion")
|
||||||
local Config = require("avante.config")
|
local Config = require("avante.config")
|
||||||
local Diff = require("avante.diff")
|
local Diff = require("avante.diff")
|
||||||
|
|
||||||
@ -12,8 +13,10 @@ local M = {
|
|||||||
sidebars = {},
|
sidebars = {},
|
||||||
---@type avante.Selection[]
|
---@type avante.Selection[]
|
||||||
selections = {},
|
selections = {},
|
||||||
---@type {sidebar?: avante.Sidebar, selection?: avante.Selection}
|
---@type avante.Suggestion[]
|
||||||
current = { sidebar = nil, selection = nil },
|
suggestions = {},
|
||||||
|
---@type {sidebar?: avante.Sidebar, selection?: avante.Selection, suggestion?: avante.Suggestion}
|
||||||
|
current = { sidebar = nil, selection = nil, suggestion = nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
M.did_setup = false
|
M.did_setup = false
|
||||||
@ -185,7 +188,7 @@ H.autocmds = function()
|
|||||||
api.nvim_create_autocmd("VimResized", {
|
api.nvim_create_autocmd("VimResized", {
|
||||||
group = H.augroup,
|
group = H.augroup,
|
||||||
callback = function()
|
callback = function()
|
||||||
local sidebar, _ = M.get()
|
local sidebar = M.get()
|
||||||
if not sidebar then
|
if not sidebar then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -234,22 +237,25 @@ H.autocmds = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param current boolean? false to disable setting current, otherwise use this to track across tabs.
|
---@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)
|
function M.get(current)
|
||||||
local tab = api.nvim_get_current_tabpage()
|
local tab = api.nvim_get_current_tabpage()
|
||||||
local sidebar = M.sidebars[tab]
|
local sidebar = M.sidebars[tab]
|
||||||
local selection = M.selections[tab]
|
local selection = M.selections[tab]
|
||||||
|
local suggestion = M.suggestions[tab]
|
||||||
if current ~= false then
|
if current ~= false then
|
||||||
M.current.sidebar = sidebar
|
M.current.sidebar = sidebar
|
||||||
M.current.selection = selection
|
M.current.selection = selection
|
||||||
|
M.current.suggestion = suggestion
|
||||||
end
|
end
|
||||||
return sidebar, selection
|
return sidebar, selection, suggestion
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param id integer
|
---@param id integer
|
||||||
function M._init(id)
|
function M._init(id)
|
||||||
local sidebar = M.sidebars[id]
|
local sidebar = M.sidebars[id]
|
||||||
local selection = M.selections[id]
|
local selection = M.selections[id]
|
||||||
|
local suggestion = M.suggestions[id]
|
||||||
|
|
||||||
if not sidebar then
|
if not sidebar then
|
||||||
sidebar = Sidebar:new(id)
|
sidebar = Sidebar:new(id)
|
||||||
@ -259,7 +265,11 @@ function M._init(id)
|
|||||||
selection = Selection:new(id)
|
selection = Selection:new(id)
|
||||||
M.selections[id] = selection
|
M.selections[id] = selection
|
||||||
end
|
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
|
return M
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -288,7 +298,7 @@ M.toggle.hint = H.api(Utils.toggle_wrap({
|
|||||||
setmetatable(M.toggle, {
|
setmetatable(M.toggle, {
|
||||||
__index = M.toggle,
|
__index = M.toggle,
|
||||||
__call = function()
|
__call = function()
|
||||||
local sidebar, _ = M.get()
|
local sidebar = M.get()
|
||||||
if not sidebar then
|
if not sidebar then
|
||||||
M._init(api.nvim_get_current_tabpage())
|
M._init(api.nvim_get_current_tabpage())
|
||||||
M.current.sidebar:open()
|
M.current.sidebar:open()
|
||||||
@ -326,7 +336,7 @@ M.build = H.api(function()
|
|||||||
local os_name = Utils.get_os_name()
|
local os_name = Utils.get_os_name()
|
||||||
|
|
||||||
if vim.tbl_contains({ "linux", "darwin" }, os_name) then
|
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
|
elseif os_name == "windows" then
|
||||||
build_directory = to_windows_path(build_directory)
|
build_directory = to_windows_path(build_directory)
|
||||||
cmd = {
|
cmd = {
|
||||||
@ -334,7 +344,7 @@ M.build = H.api(function()
|
|||||||
"-ExecutionPolicy",
|
"-ExecutionPolicy",
|
||||||
"Bypass",
|
"Bypass",
|
||||||
"-File",
|
"-File",
|
||||||
("%s\\Build.ps1"):format(build_directory),
|
string.format("%s\\Build.ps1", build_directory),
|
||||||
"-WorkingDirectory",
|
"-WorkingDirectory",
|
||||||
build_directory,
|
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.
|
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 group = api.nvim_create_augroup("avante_llm", { clear = true })
|
||||||
local active_job = nil
|
|
||||||
|
|
||||||
---@class StreamOptions
|
---@class StreamOptions
|
||||||
---@field file_content string
|
---@field file_content string
|
||||||
@ -99,7 +127,7 @@ local active_job = nil
|
|||||||
---@field project_context string | nil
|
---@field project_context string | nil
|
||||||
---@field memory_context string | nil
|
---@field memory_context string | nil
|
||||||
---@field full_file_contents_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_chunk AvanteChunkParser
|
||||||
---@field on_complete AvanteCompleteParser
|
---@field on_complete AvanteCompleteParser
|
||||||
|
|
||||||
@ -108,7 +136,13 @@ M.stream = function(opts)
|
|||||||
local mode = opts.mode or "planning"
|
local mode = opts.mode or "planning"
|
||||||
local provider = Config.provider
|
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
|
-- Check if the instructions contains an image path
|
||||||
local image_paths = {}
|
local image_paths = {}
|
||||||
@ -191,13 +225,10 @@ M.stream = function(opts)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if active_job then
|
|
||||||
active_job:shutdown()
|
|
||||||
active_job = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local completed = false
|
local completed = false
|
||||||
|
|
||||||
|
local active_job
|
||||||
|
|
||||||
active_job = curl.post(spec.url, {
|
active_job = curl.post(spec.url, {
|
||||||
headers = spec.headers,
|
headers = spec.headers,
|
||||||
proxy = spec.proxy,
|
proxy = spec.proxy,
|
||||||
@ -230,11 +261,13 @@ M.stream = function(opts)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
on_error = function(err)
|
on_error = function()
|
||||||
|
active_job = nil
|
||||||
completed = true
|
completed = true
|
||||||
opts.on_complete(err)
|
opts.on_complete(nil)
|
||||||
end,
|
end,
|
||||||
callback = function(result)
|
callback = function(result)
|
||||||
|
active_job = nil
|
||||||
if result.status >= 400 then
|
if result.status >= 400 then
|
||||||
if Provider.on_error then
|
if Provider.on_error then
|
||||||
Provider.on_error(result)
|
Provider.on_error(result)
|
||||||
@ -250,16 +283,21 @@ M.stream = function(opts)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
active_job = nil
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
api.nvim_create_autocmd("User", {
|
api.nvim_create_autocmd("User", {
|
||||||
group = group,
|
group = group,
|
||||||
pattern = M.CANCEL_PATTERN,
|
pattern = M.CANCEL_PATTERN,
|
||||||
|
once = true,
|
||||||
callback = function()
|
callback = function()
|
||||||
|
-- Error: cannot resume dead coroutine
|
||||||
if active_job then
|
if active_job then
|
||||||
|
xpcall(function()
|
||||||
active_job:shutdown()
|
active_job:shutdown()
|
||||||
|
end, function(err)
|
||||||
|
return err
|
||||||
|
end)
|
||||||
Utils.debug("LLM request cancelled", { title = "Avante" })
|
Utils.debug("LLM request cancelled", { title = "Avante" })
|
||||||
active_job = nil
|
active_job = nil
|
||||||
end
|
end
|
||||||
@ -269,4 +307,8 @@ M.stream = function(opts)
|
|||||||
return active_job
|
return active_job
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M.cancel_inflight_request()
|
||||||
|
api.nvim_exec_autocmds("User", { pattern = M.CANCEL_PATTERN })
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
@ -84,7 +84,7 @@ end
|
|||||||
|
|
||||||
function Selection:close_editing_input()
|
function Selection:close_editing_input()
|
||||||
self:close_editing_input_shortcuts_hints()
|
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
|
if api.nvim_get_mode().mode == "i" then
|
||||||
vim.cmd([[stopinsert]])
|
vim.cmd([[stopinsert]])
|
||||||
end
|
end
|
||||||
|
@ -448,7 +448,7 @@ local function insert_conflict_contents(bufnr, snippets)
|
|||||||
local snippet_lines = vim.split(snippet.content, "\n")
|
local snippet_lines = vim.split(snippet.content, "\n")
|
||||||
|
|
||||||
for idx, line in ipairs(snippet_lines) do
|
for idx, line in ipairs(snippet_lines) do
|
||||||
line = line:gsub("^L%d+: ", "")
|
line = Utils.trim_line_number(line)
|
||||||
if idx == 1 then
|
if idx == 1 then
|
||||||
local indentation = Utils.get_indentation(line)
|
local indentation = Utils.get_indentation(line)
|
||||||
need_prepend_indentation = indentation ~= original_start_line_indentation
|
need_prepend_indentation = indentation ~= original_start_line_indentation
|
||||||
@ -824,7 +824,7 @@ function Sidebar:on_mount()
|
|||||||
self:render_input()
|
self:render_input()
|
||||||
self:render_selected_code()
|
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 })
|
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
|
||||||
|
|
||||||
@ -1040,17 +1040,6 @@ function Sidebar:update_content(content, opts)
|
|||||||
return self
|
return self
|
||||||
end
|
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
|
-- Function to get current timestamp
|
||||||
local function get_timestamp()
|
local function get_timestamp()
|
||||||
return os.date("%Y-%m-%d %H:%M:%S")
|
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")
|
self:update_content(content_prefix .. "🔄 **Generating response ...**\n")
|
||||||
|
|
||||||
local content = table.concat(Utils.get_buf_lines(0, -1, self.code.bufnr), "\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 filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
|
||||||
|
|
||||||
local selected_code_content_with_line_numbers = nil
|
local selected_code_content_with_line_numbers = nil
|
||||||
if self.code.selection ~= nil then
|
if self.code.selection ~= nil then
|
||||||
selected_code_content_with_line_numbers =
|
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
|
end
|
||||||
|
|
||||||
if request:sub(1, 1) == "/" then
|
if request:sub(1, 1) == "/" then
|
||||||
@ -1289,7 +1278,7 @@ function Sidebar:create_input()
|
|||||||
Utils.error("Invalid end line number", { once = true, title = "Avante" })
|
Utils.error("Invalid end line number", { once = true, title = "Avante" })
|
||||||
return
|
return
|
||||||
end
|
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"),
|
table.concat(api.nvim_buf_get_lines(self.code.bufnr, start_line - 1, end_line, false), "\n"),
|
||||||
start_line
|
start_line
|
||||||
)
|
)
|
||||||
@ -1640,12 +1629,12 @@ function Sidebar:render()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
self.result:map("n", "q", function()
|
self.result:map("n", "q", function()
|
||||||
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
|
Llm.cancel_inflight_request()
|
||||||
self:close()
|
self:close()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
self.result:map("n", "<Esc>", function()
|
self.result:map("n", "<Esc>", function()
|
||||||
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
|
Llm.cancel_inflight_request()
|
||||||
self:close()
|
self:close()
|
||||||
end)
|
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 api = vim.api
|
||||||
|
local fn = vim.fn
|
||||||
|
local lsp = vim.lsp
|
||||||
|
|
||||||
---@class avante.utils: LazyUtilCore
|
---@class avante.utils: LazyUtilCore
|
||||||
---@field tokens avante.utils.tokens
|
---@field tokens avante.utils.tokens
|
||||||
@ -338,7 +340,7 @@ function M.debug(msg, opts)
|
|||||||
end
|
end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.title then
|
if opts.title then
|
||||||
opts.title = "lazy.nvim: " .. opts.title
|
opts.title = "avante.nvim: " .. opts.title
|
||||||
end
|
end
|
||||||
if type(msg) == "string" then
|
if type(msg) == "string" then
|
||||||
M.notify(msg, opts)
|
M.notify(msg, opts)
|
||||||
@ -484,4 +486,78 @@ function M.remove_indentation(code)
|
|||||||
return code:gsub("^%s*", "")
|
return code:gsub("^%s*", "")
|
||||||
end
|
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
|
return M
|
||||||
|
Loading…
x
Reference in New Issue
Block a user