feat: cursor planning mode (#1289)
This commit is contained in:
@@ -581,6 +581,12 @@ Avante provides a set of default providers, but users can also create their own
|
||||
|
||||
For more information, see [Custom Providers](https://github.com/yetone/avante.nvim/wiki/Custom-providers)
|
||||
|
||||
## Cursor planning mode
|
||||
|
||||
Because avante.nvim has always used Aider’s method for planning applying, but its prompts are very picky with models and require ones like claude-3.5-sonnet or gpt-4o to work properly.
|
||||
|
||||
Therefore, I have adopted Cursor’s method to implement planning applying. For details on the implementation, please refer to [cursor-planning-mode.md](./cursor-planning-mode.md)
|
||||
|
||||
## Web Search Engines
|
||||
|
||||
Avante's tools include some web search engines, currently support [tavily](https://tavily.com/), [serpapi](https://serpapi.com/), [searchapi](https://www.searchapi.io/) and google's [programmable search engine](https://developers.google.com/custom-search/v1/overview). The default is tavily, and can be changed through configuring `Config.web_search_engine.provider`:
|
||||
|
||||
@@ -27,11 +27,12 @@ struct TemplateContext {
|
||||
use_xml_format: bool,
|
||||
ask: bool,
|
||||
code_lang: String,
|
||||
selected_files: Vec<SelectedFile>,
|
||||
selected_files: Option<Vec<SelectedFile>>,
|
||||
selected_code: Option<String>,
|
||||
project_context: Option<String>,
|
||||
diagnostics: Option<String>,
|
||||
system_info: Option<String>,
|
||||
model_name: Option<String>,
|
||||
}
|
||||
|
||||
// Given the file name registered after add, the context table in Lua, resulted in a formatted
|
||||
@@ -56,6 +57,7 @@ fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult<
|
||||
project_context => context.project_context,
|
||||
diagnostics => context.diagnostics,
|
||||
system_info => context.system_info,
|
||||
model_name => context.model_name,
|
||||
})
|
||||
.map_err(LuaError::external)
|
||||
.unwrap())
|
||||
|
||||
35
cursor-planning-mode.md
Normal file
35
cursor-planning-mode.md
Normal file
@@ -0,0 +1,35 @@
|
||||
Cursor planning mode
|
||||
====================
|
||||
|
||||
Because avante.nvim has always used Aider’s method for planning applying, but its prompts are very picky with models and require ones like claude-3.5-sonnet or gpt-4o to work properly.
|
||||
|
||||
Therefore, I have adopted Cursor’s method to implement planning applying. For details on the implementation, please refer to: [🚀 Introducing Fast Apply - Replicate Cursor's Instant Apply model](https://www.reddit.com/r/LocalLLaMA/comments/1ga25gj/introducing_fast_apply_replicate_cursors_instant/)
|
||||
|
||||
So you need to first run the `FastApply` model mentioned above:
|
||||
|
||||
```bash
|
||||
ollama pull hf.co/Kortix/FastApply-7B-v1.0_GGUF:Q4_K_M
|
||||
```
|
||||
|
||||
Then enable it in avante.nvim:
|
||||
|
||||
```lua
|
||||
{
|
||||
--- ... existing configurations
|
||||
cursor_applying_provider = 'fastapply',
|
||||
behaviour = {
|
||||
--- ... existing behaviours
|
||||
enable_cursor_planning_mode = true,
|
||||
},
|
||||
vendors = {
|
||||
--- ... existing vendors
|
||||
fastapply = {
|
||||
__inherited_from = 'openai',
|
||||
api_key_name = '',
|
||||
endpoint = 'http://localhost:11434/v1',
|
||||
model = 'hf.co/Kortix/FastApply-7B-v1.0_GGUF:Q4_K_M',
|
||||
},
|
||||
},
|
||||
--- ... existing configurations
|
||||
}
|
||||
```
|
||||
@@ -21,6 +21,7 @@ M._defaults = {
|
||||
-- currently designating it as `copilot` provider is dangerous because: https://github.com/yetone/avante.nvim/issues/1048
|
||||
-- Of course, you can reduce the request frequency by increasing `suggestion.debounce`.
|
||||
auto_suggestions_provider = "claude",
|
||||
cursor_applying_provider = nil,
|
||||
---@alias Tokenizer "tiktoken" | "hf"
|
||||
-- Used for counting tokens and encoding text.
|
||||
-- By default, we will use tiktoken.
|
||||
@@ -248,6 +249,7 @@ M._defaults = {
|
||||
support_paste_from_clipboard = false,
|
||||
minimize_diff = true,
|
||||
enable_token_counting = true,
|
||||
enable_cursor_planning_mode = false,
|
||||
},
|
||||
history = {
|
||||
max_tokens = 4096,
|
||||
|
||||
@@ -7,7 +7,7 @@ local curl = require("plenary.curl")
|
||||
local Utils = require("avante.utils")
|
||||
local Config = require("avante.config")
|
||||
local Path = require("avante.path")
|
||||
local P = require("avante.providers")
|
||||
local Providers = require("avante.providers")
|
||||
local LLMTools = require("avante.llm_tools")
|
||||
|
||||
---@class avante.LLM
|
||||
@@ -22,16 +22,16 @@ local group = api.nvim_create_augroup("avante_llm", { clear = true })
|
||||
---@param opts GeneratePromptsOptions
|
||||
---@return AvantePromptOptions
|
||||
M.generate_prompts = function(opts)
|
||||
local Provider = opts.provider or P[Config.provider]
|
||||
local provider = opts.provider or Providers[Config.provider]
|
||||
local mode = opts.mode or "planning"
|
||||
---@type AvanteProviderFunctor | AvanteBedrockProviderFunctor
|
||||
local _, request_body = P.parse_config(Provider)
|
||||
local _, request_body = Providers.parse_config(provider)
|
||||
local max_tokens = request_body.max_tokens or 4096
|
||||
|
||||
-- Check if the instructions contains an image path
|
||||
local image_paths = {}
|
||||
local instructions = opts.instructions
|
||||
if opts.instructions:match("image: ") then
|
||||
if instructions and instructions:match("image: ") then
|
||||
local lines = vim.split(opts.instructions, "\n")
|
||||
for i, line in ipairs(lines) do
|
||||
if line:match("^image: ") then
|
||||
@@ -49,7 +49,7 @@ M.generate_prompts = function(opts)
|
||||
local system_info = Utils.get_system_info()
|
||||
|
||||
local template_opts = {
|
||||
use_xml_format = Provider.use_xml_format,
|
||||
use_xml_format = provider.use_xml_format,
|
||||
ask = opts.ask, -- TODO: add mode without ask instruction
|
||||
code_lang = opts.code_lang,
|
||||
selected_files = opts.selected_files,
|
||||
@@ -57,6 +57,7 @@ M.generate_prompts = function(opts)
|
||||
project_context = opts.project_context,
|
||||
diagnostics = opts.diagnostics,
|
||||
system_info = system_info,
|
||||
model_name = provider.model or "unknown",
|
||||
}
|
||||
|
||||
local system_prompt = Path.prompts.render_mode(mode, template_opts)
|
||||
@@ -74,15 +75,17 @@ M.generate_prompts = function(opts)
|
||||
if diagnostics ~= "" then table.insert(messages, { role = "user", content = diagnostics }) end
|
||||
end
|
||||
|
||||
if #opts.selected_files > 0 or opts.selected_code ~= nil then
|
||||
if (opts.selected_files and #opts.selected_files > 0 or false) or opts.selected_code ~= nil then
|
||||
local code_context = Path.prompts.render_file("_context.avanterules", template_opts)
|
||||
if code_context ~= "" then table.insert(messages, { role = "user", content = code_context }) end
|
||||
end
|
||||
|
||||
if opts.use_xml_format then
|
||||
table.insert(messages, { role = "user", content = string.format("<question>%s</question>", instructions) })
|
||||
else
|
||||
table.insert(messages, { role = "user", content = string.format("QUESTION:\n%s", instructions) })
|
||||
if instructions then
|
||||
if opts.use_xml_format then
|
||||
table.insert(messages, { role = "user", content = string.format("<question>%s</question>", instructions) })
|
||||
else
|
||||
table.insert(messages, { role = "user", content = string.format("QUESTION:\n%s", instructions) })
|
||||
end
|
||||
end
|
||||
|
||||
local remaining_tokens = max_tokens - Utils.tokens.calculate_tokens(system_prompt)
|
||||
@@ -110,6 +113,22 @@ M.generate_prompts = function(opts)
|
||||
if #messages > 0 and messages[1].role == "assistant" then table.remove(messages, 1) end
|
||||
end
|
||||
|
||||
if opts.mode == "cursor-applying" then
|
||||
local user_prompt = [[
|
||||
Merge all changes from the <update> snippet into the <code> below.
|
||||
- Preserve the code's structure, order, comments, and indentation exactly.
|
||||
- Output only the updated code, enclosed within <updated-code> and </updated-code> tags.
|
||||
- Do not include any additional text, explanations, placeholders, ellipses, or code fences.
|
||||
|
||||
]]
|
||||
user_prompt = user_prompt .. string.format("<code>\n%s\n</code>\n", opts.original_code)
|
||||
for _, snippet in ipairs(opts.update_snippets) do
|
||||
user_prompt = user_prompt .. string.format("<update>\n%s\n</update>\n", snippet)
|
||||
end
|
||||
user_prompt = user_prompt .. "Provide the complete updated code."
|
||||
table.insert(messages, { role = "user", content = user_prompt })
|
||||
end
|
||||
|
||||
---@type AvantePromptOptions
|
||||
return {
|
||||
system_prompt = system_prompt,
|
||||
@@ -133,7 +152,7 @@ end
|
||||
|
||||
---@param opts StreamOptions
|
||||
M._stream = function(opts)
|
||||
local Provider = opts.provider or P[Config.provider]
|
||||
local provider = opts.provider or Providers[Config.provider]
|
||||
|
||||
local prompt_opts = M.generate_prompts(opts)
|
||||
|
||||
@@ -166,7 +185,7 @@ M._stream = function(opts)
|
||||
}
|
||||
|
||||
---@type AvanteCurlOutput
|
||||
local spec = Provider.parse_curl_args(Provider, prompt_opts)
|
||||
local spec = provider.parse_curl_args(provider, prompt_opts)
|
||||
|
||||
local resp_ctx = {}
|
||||
|
||||
@@ -178,11 +197,11 @@ M._stream = function(opts)
|
||||
return
|
||||
end
|
||||
local data_match = line:match("^data: (.+)$")
|
||||
if data_match then Provider.parse_response(resp_ctx, data_match, current_event_state, handler_opts) end
|
||||
if data_match then provider.parse_response(resp_ctx, data_match, current_event_state, handler_opts) end
|
||||
end
|
||||
|
||||
local function parse_response_without_stream(data)
|
||||
Provider.parse_response_without_stream(data, current_event_state, handler_opts)
|
||||
provider.parse_response_without_stream(data, current_event_state, handler_opts)
|
||||
end
|
||||
|
||||
local completed = false
|
||||
@@ -214,17 +233,17 @@ M._stream = function(opts)
|
||||
end
|
||||
if not data then return end
|
||||
vim.schedule(function()
|
||||
if Config[Config.provider] == nil and Provider.parse_stream_data ~= nil then
|
||||
if Provider.parse_response ~= nil then
|
||||
if Config[Config.provider] == nil and provider.parse_stream_data ~= nil then
|
||||
if provider.parse_response ~= nil then
|
||||
Utils.warn(
|
||||
"parse_stream_data and parse_response are mutually exclusive, and thus parse_response will be ignored. Make sure that you handle the incoming data correctly.",
|
||||
{ once = true }
|
||||
)
|
||||
end
|
||||
Provider.parse_stream_data(data, handler_opts)
|
||||
provider.parse_stream_data(data, handler_opts)
|
||||
else
|
||||
if Provider.parse_stream_data ~= nil then
|
||||
Provider.parse_stream_data(data, handler_opts)
|
||||
if provider.parse_stream_data ~= nil then
|
||||
provider.parse_stream_data(data, handler_opts)
|
||||
else
|
||||
parse_stream_data(data)
|
||||
end
|
||||
@@ -259,8 +278,8 @@ M._stream = function(opts)
|
||||
active_job = nil
|
||||
cleanup()
|
||||
if result.status >= 400 then
|
||||
if Provider.on_error then
|
||||
Provider.on_error(result)
|
||||
if provider.on_error then
|
||||
provider.on_error(result)
|
||||
else
|
||||
Utils.error("API request failed with status " .. result.status, { once = true, title = "Avante" })
|
||||
end
|
||||
@@ -388,7 +407,7 @@ M._dual_boost_stream = function(opts, Provider1, Provider2)
|
||||
if not success then Utils.error("Failed to start dual_boost streams: " .. tostring(err)) end
|
||||
end
|
||||
|
||||
---@alias LlmMode "planning" | "editing" | "suggesting"
|
||||
---@alias LlmMode "planning" | "editing" | "suggesting" | "cursor-planning" | "cursor-applying"
|
||||
---
|
||||
---@class SelectedFiles
|
||||
---@field path string
|
||||
@@ -408,11 +427,13 @@ end
|
||||
---
|
||||
---@class GeneratePromptsOptions: TemplateOptions
|
||||
---@field ask boolean
|
||||
---@field instructions string
|
||||
---@field instructions? string
|
||||
---@field mode LlmMode
|
||||
---@field provider AvanteProviderFunctor | AvanteBedrockProviderFunctor | nil
|
||||
---@field tools? AvanteLLMTool[]
|
||||
---@field tool_histories? AvanteLLMToolHistory[]
|
||||
---@field original_code? string
|
||||
---@field update_snippets? string[]
|
||||
---
|
||||
---@class AvanteLLMToolHistory
|
||||
---@field tool_result? AvanteLLMToolResult
|
||||
@@ -450,7 +471,11 @@ M.stream = function(opts)
|
||||
end)
|
||||
end
|
||||
if Config.dual_boost.enabled and opts.mode == "planning" then
|
||||
M._dual_boost_stream(opts, P[Config.dual_boost.first_provider], P[Config.dual_boost.second_provider])
|
||||
M._dual_boost_stream(
|
||||
opts,
|
||||
Providers[Config.dual_boost.first_provider],
|
||||
Providers[Config.dual_boost.second_provider]
|
||||
)
|
||||
else
|
||||
M._stream(opts)
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ local DressingState = { winid = nil, input_winid = nil, input_bufnr = nil }
|
||||
---@alias AvanteMessagesParser fun(opts: AvantePromptOptions): AvanteChatMessage[]
|
||||
---
|
||||
---@class AvanteCurlOutput: {url: string, proxy: string, insecure: boolean, body: table<string, any> | string, headers: table<string, string>, rawArgs: string[] | nil}
|
||||
---@alias AvanteCurlArgsParser fun(opts: AvanteProvider | AvanteProviderFunctor | AvanteBedrockProviderFunctor, prompt_opts: AvantePromptOptions): AvanteCurlOutput
|
||||
---@alias AvanteCurlArgsParser fun(provider: AvanteProvider | AvanteProviderFunctor | AvanteBedrockProviderFunctor, prompt_opts: AvantePromptOptions): AvanteCurlOutput
|
||||
---
|
||||
---@class ResponseParser
|
||||
---@field on_start AvanteLLMStartCallback
|
||||
@@ -364,11 +364,19 @@ M.setup = function()
|
||||
---@type AvanteProviderFunctor | AvanteBedrockProviderFunctor
|
||||
local provider = M[Config.provider]
|
||||
local auto_suggestions_provider = M[Config.auto_suggestions_provider]
|
||||
|
||||
E.setup({ provider = provider })
|
||||
|
||||
if auto_suggestions_provider and auto_suggestions_provider ~= provider then
|
||||
E.setup({ provider = auto_suggestions_provider })
|
||||
end
|
||||
|
||||
if Config.cursor_applying_provider then
|
||||
local cursor_applying_provider = M[Config.cursor_applying_provider]
|
||||
if cursor_applying_provider and cursor_applying_provider ~= provider then
|
||||
E.setup({ provider = cursor_applying_provider })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param provider Provider
|
||||
|
||||
@@ -232,7 +232,7 @@ function Selection:create_editing_input()
|
||||
cancel_callback = function() self:close_editing_input() end,
|
||||
win_opts = {
|
||||
border = Config.windows.edit.border,
|
||||
title = { { "edit selected block", "FloatTitle" } },
|
||||
title = { { "Edit selected block", "FloatTitle" } },
|
||||
},
|
||||
start_insert = Config.windows.edit.start_insert,
|
||||
})
|
||||
|
||||
@@ -520,6 +520,52 @@ end
|
||||
---@field start_line_in_response_buf integer
|
||||
---@field end_line_in_response_buf integer
|
||||
---@field filepath string
|
||||
---
|
||||
---@param response_content string
|
||||
---@return table<string, AvanteCodeSnippet[]>
|
||||
local function extract_cursor_planning_code_snippets_map(response_content)
|
||||
local snippets = {}
|
||||
local current_snippet = {}
|
||||
local in_code_block = false
|
||||
local lang, filepath, start_line_in_response_buf
|
||||
|
||||
local lines = vim.split(response_content, "\n")
|
||||
|
||||
for idx, line in ipairs(lines) do
|
||||
if line:match("^%s*```") then
|
||||
if in_code_block then
|
||||
in_code_block = false
|
||||
table.insert(snippets, {
|
||||
range = { 0, 0 },
|
||||
content = table.concat(current_snippet, "\n"),
|
||||
lang = lang,
|
||||
filepath = filepath,
|
||||
start_line_in_response_buf = start_line_in_response_buf,
|
||||
end_line_in_response_buf = idx,
|
||||
})
|
||||
else
|
||||
in_code_block = true
|
||||
start_line_in_response_buf = idx
|
||||
local lang_ = line:match("^%s*```(%w+)")
|
||||
lang = lang_ or "unknown"
|
||||
local filepath_ = line:match("^%s*```%w+:(.+)$")
|
||||
filepath = filepath_ or ""
|
||||
-- local line_ = line:gsub(".*(:.+)$", "")
|
||||
-- lines[idx] = line_
|
||||
end
|
||||
elseif in_code_block then
|
||||
table.insert(current_snippet, line)
|
||||
end
|
||||
end
|
||||
|
||||
local snippets_map = {}
|
||||
for _, snippet in ipairs(snippets) do
|
||||
snippets_map[snippet.filepath] = snippets_map[snippet.filepath] or {}
|
||||
table.insert(snippets_map[snippet.filepath], snippet)
|
||||
end
|
||||
|
||||
return snippets_map
|
||||
end
|
||||
|
||||
---@param response_content string
|
||||
---@return table<string, AvanteCodeSnippet[]>
|
||||
@@ -714,7 +760,10 @@ local function parse_codeblocks(buf)
|
||||
if in_codeblock and not lang_ then
|
||||
table.insert(codeblocks, { start_line = start_line, end_line = i - 1, lang = lang })
|
||||
in_codeblock = false
|
||||
elseif lang_ and lines[i - 1]:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)") then
|
||||
elseif
|
||||
lang_ and Config.behaviour.enable_cursor_planning_mode
|
||||
or lines[i - 1]:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)")
|
||||
then
|
||||
lang = lang_
|
||||
start_line = i - 1
|
||||
in_codeblock = true
|
||||
@@ -787,8 +836,12 @@ end
|
||||
---@param current_cursor boolean
|
||||
function Sidebar:apply(current_cursor)
|
||||
local response, response_start_line = self:get_content_between_separators()
|
||||
local all_snippets_map = extract_code_snippets_map(response)
|
||||
all_snippets_map = ensure_snippets_no_overlap(all_snippets_map)
|
||||
local all_snippets_map = Config.behaviour.enable_cursor_planning_mode
|
||||
and extract_cursor_planning_code_snippets_map(response)
|
||||
or extract_code_snippets_map(response)
|
||||
if not Config.behaviour.enable_cursor_planning_mode then
|
||||
all_snippets_map = ensure_snippets_no_overlap(all_snippets_map)
|
||||
end
|
||||
local selected_snippets_map = {}
|
||||
if current_cursor then
|
||||
if self.result_container and self.result_container.winid then
|
||||
@@ -809,6 +862,231 @@ function Sidebar:apply(current_cursor)
|
||||
selected_snippets_map = all_snippets_map
|
||||
end
|
||||
|
||||
if Config.behaviour.enable_cursor_planning_mode then
|
||||
for filepath, snippets in pairs(selected_snippets_map) do
|
||||
local original_code_lines = Utils.read_file_from_buf_or_disk(filepath)
|
||||
if not original_code_lines then
|
||||
Utils.error("Failed to read file: " .. filepath)
|
||||
return
|
||||
end
|
||||
local formated_snippets = vim.iter(snippets):map(function(snippet) return snippet.content end):totable()
|
||||
local original_code = table.concat(original_code_lines, "\n")
|
||||
local resp_content = ""
|
||||
local filetype = Utils.get_filetype(filepath)
|
||||
local cursor_applying_provider = Provider[Config.cursor_applying_provider]
|
||||
if not cursor_applying_provider then
|
||||
Utils.error("Failed to find cursor_applying_provider provider: " .. Config.cursor_applying_provider, {
|
||||
once = true,
|
||||
title = "Avante",
|
||||
})
|
||||
end
|
||||
local bufnr = Utils.get_or_create_buffer_with_filepath(filepath)
|
||||
local path_ = PPath:new(filepath)
|
||||
path_:parent():mkdir({ parents = true, exists_ok = true })
|
||||
|
||||
local ns_id = api.nvim_create_namespace("avante_live_diff")
|
||||
|
||||
local function clear_highlights() api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) end
|
||||
|
||||
local last_processed_line = 0
|
||||
|
||||
-- Create loading indicator float window
|
||||
local loading_buf = api.nvim_create_buf(false, true)
|
||||
local loading_win = nil
|
||||
local spinner_frames = { "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" }
|
||||
local spinner_idx = 1
|
||||
local loading_timer = nil
|
||||
|
||||
local function update_loading_indicator()
|
||||
if not loading_win or not api.nvim_win_is_valid(loading_win) then return end
|
||||
spinner_idx = (spinner_idx % #spinner_frames) + 1
|
||||
local text = spinner_frames[spinner_idx] .. " Applying changes..."
|
||||
api.nvim_buf_set_lines(loading_buf, 0, -1, false, { text })
|
||||
end
|
||||
|
||||
local function create_loading_window()
|
||||
local winid = self.input_container.winid
|
||||
local win_height = api.nvim_win_get_height(winid)
|
||||
local win_width = api.nvim_win_get_width(winid)
|
||||
|
||||
-- Calculate position for center of window
|
||||
local width = 30
|
||||
local height = 1
|
||||
local row = win_height - height - 1
|
||||
local col = win_width - width
|
||||
|
||||
local opts = {
|
||||
relative = "win",
|
||||
win = winid,
|
||||
width = width,
|
||||
height = height,
|
||||
row = row,
|
||||
col = col,
|
||||
anchor = "NW",
|
||||
style = "minimal",
|
||||
border = "none",
|
||||
focusable = false,
|
||||
zindex = 101,
|
||||
}
|
||||
|
||||
loading_win = api.nvim_open_win(loading_buf, false, opts)
|
||||
|
||||
-- Start timer to update spinner
|
||||
loading_timer = vim.loop.new_timer()
|
||||
if loading_timer then loading_timer:start(0, 100, vim.schedule_wrap(update_loading_indicator)) end
|
||||
end
|
||||
|
||||
local function close_loading_window()
|
||||
if loading_timer then
|
||||
loading_timer:stop()
|
||||
loading_timer:close()
|
||||
loading_timer = nil
|
||||
end
|
||||
if loading_win and api.nvim_win_is_valid(loading_win) then
|
||||
api.nvim_win_close(loading_win, true)
|
||||
loading_win = nil
|
||||
end
|
||||
end
|
||||
|
||||
clear_highlights()
|
||||
create_loading_window()
|
||||
|
||||
Llm.stream({
|
||||
ask = true,
|
||||
provider = cursor_applying_provider,
|
||||
code_lang = filetype,
|
||||
mode = "cursor-applying",
|
||||
original_code = original_code,
|
||||
update_snippets = formated_snippets,
|
||||
on_start = function(_) end,
|
||||
on_chunk = function(chunk)
|
||||
if not chunk then return end
|
||||
|
||||
resp_content = resp_content .. chunk
|
||||
|
||||
local clean_content = resp_content:gsub("<updated%-code>\n*", ""):gsub("</updated%-code>\n*", "")
|
||||
clean_content = clean_content:gsub(".*```%w+\n", ""):gsub("\n```\n.*", "")
|
||||
local resp_lines = vim.split(clean_content, "\n")
|
||||
|
||||
local complete_lines_count = #resp_lines - 1
|
||||
if complete_lines_count <= last_processed_line then return end
|
||||
|
||||
local original_lines = vim.list_slice(original_code_lines, 1, complete_lines_count)
|
||||
local resp_lines_to_process = vim.list_slice(resp_lines, 1, complete_lines_count)
|
||||
|
||||
local resp_lines_content = table.concat(resp_lines_to_process, "\n")
|
||||
local original_lines_content = table.concat(original_lines, "\n")
|
||||
|
||||
---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
|
||||
local patch = vim.diff(original_lines_content, resp_lines_content, { ---@type integer[][]
|
||||
algorithm = "histogram",
|
||||
result_type = "indices",
|
||||
ctxlen = vim.o.scrolloff,
|
||||
})
|
||||
|
||||
clear_highlights()
|
||||
|
||||
for _, hunk in ipairs(patch) do
|
||||
local start_a, count_a, start_b, count_b = unpack(hunk)
|
||||
|
||||
for i = start_a, start_a + count_a - 1 do
|
||||
api.nvim_buf_add_highlight(bufnr, ns_id, Highlights.CURRENT, i - 1, 0, -1)
|
||||
end
|
||||
|
||||
local new_lines = vim.list_slice(resp_lines_to_process, start_b, start_b + count_b - 1)
|
||||
local virt_lines = vim
|
||||
.iter(new_lines)
|
||||
:map(function(line) return { { line, Highlights.INCOMING } } end)
|
||||
:totable()
|
||||
api.nvim_buf_set_extmark(bufnr, ns_id, math.max(0, start_a + count_a - 2), 0, {
|
||||
virt_lines = virt_lines,
|
||||
virt_text_pos = "overlay",
|
||||
hl_mode = "combine",
|
||||
})
|
||||
end
|
||||
|
||||
last_processed_line = complete_lines_count
|
||||
|
||||
local winid = Utils.get_winid(bufnr)
|
||||
|
||||
--- goto window winid
|
||||
api.nvim_set_current_win(winid)
|
||||
--- goto the last line
|
||||
api.nvim_win_set_cursor(winid, { last_processed_line, 0 })
|
||||
vim.cmd("normal! zz")
|
||||
end,
|
||||
on_stop = function(stop_opts)
|
||||
clear_highlights()
|
||||
close_loading_window()
|
||||
|
||||
if stop_opts.error ~= nil then
|
||||
Utils.error(string.format("applying failed: %s", vim.inspect(stop_opts.error)))
|
||||
return
|
||||
end
|
||||
|
||||
resp_content = resp_content:gsub("<updated%-code>\n*", ""):gsub("</updated%-code>\n*", "")
|
||||
|
||||
resp_content = resp_content:gsub(".*```%w+\n", ""):gsub("\n```\n.*", "")
|
||||
local resp_lines = vim.split(resp_content, "\n")
|
||||
local original_lines = vim.list_slice(original_code_lines, 1, #resp_lines)
|
||||
local resp_lines_content = table.concat(resp_lines, "\n")
|
||||
local original_lines_content = table.concat(original_lines, "\n")
|
||||
|
||||
if resp_lines_content == original_lines_content then return end
|
||||
|
||||
---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
|
||||
local patch = vim.diff(original_lines_content, resp_lines_content, { ---@type integer[][]
|
||||
algorithm = "histogram",
|
||||
result_type = "indices",
|
||||
ctxlen = vim.o.scrolloff,
|
||||
})
|
||||
|
||||
local new_lines = {}
|
||||
local prev_start_a = 1
|
||||
for _, hunk in ipairs(patch) do
|
||||
local start_a, count_a, start_b, count_b = unpack(hunk)
|
||||
vim.list_extend(new_lines, vim.list_slice(original_lines, prev_start_a, start_a - 1))
|
||||
prev_start_a = start_a + count_a
|
||||
table.insert(new_lines, "<<<<<<< HEAD")
|
||||
vim.list_extend(new_lines, vim.list_slice(original_lines, start_a, start_a + count_a - 1))
|
||||
table.insert(new_lines, "=======")
|
||||
vim.list_extend(new_lines, vim.list_slice(resp_lines, start_b, start_b + count_b - 1))
|
||||
table.insert(new_lines, ">>>>>>> Snippet")
|
||||
end
|
||||
|
||||
api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
|
||||
|
||||
local process = function(winid)
|
||||
api.nvim_set_current_win(winid)
|
||||
api.nvim_feedkeys(api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
|
||||
Diff.add_visited_buffer(bufnr)
|
||||
Diff.process(bufnr)
|
||||
api.nvim_win_set_cursor(winid, { 1, 0 })
|
||||
vim.defer_fn(function()
|
||||
Diff.find_next(Config.windows.ask.focus_on_apply)
|
||||
vim.cmd("normal! zz")
|
||||
end, 100)
|
||||
end
|
||||
|
||||
local winid = Utils.get_winid(bufnr)
|
||||
if winid then
|
||||
process(winid)
|
||||
else
|
||||
api.nvim_create_autocmd("BufWinEnter", {
|
||||
buffer = bufnr,
|
||||
once = true,
|
||||
callback = function()
|
||||
local winid_ = Utils.get_winid(bufnr)
|
||||
if winid_ then process(winid_) end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
vim.defer_fn(function()
|
||||
api.nvim_set_current_win(self.code.winid)
|
||||
for filepath, snippets in pairs(selected_snippets_map) do
|
||||
@@ -1801,7 +2079,7 @@ function Sidebar:create_input_container(opts)
|
||||
code_lang = filetype,
|
||||
selected_code = selected_code_content,
|
||||
instructions = request,
|
||||
mode = "planning",
|
||||
mode = Config.behaviour.enable_cursor_planning_mode and "cursor-planning" or "planning",
|
||||
tools = tools,
|
||||
}
|
||||
end
|
||||
|
||||
1
lua/avante/templates/cursor-applying.avanterules
Normal file
1
lua/avante/templates/cursor-applying.avanterules
Normal file
@@ -0,0 +1 @@
|
||||
You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated.
|
||||
41
lua/avante/templates/cursor-planning.avanterules
Normal file
41
lua/avante/templates/cursor-planning.avanterules
Normal file
@@ -0,0 +1,41 @@
|
||||
You are an intelligent programmer, powered by {{ model_name }}. You are happy to help answer any questions that the user has (usually they will be about coding).
|
||||
|
||||
1. When the user is asking for edits to their code, please output a simplified version of the code block that highlights the changes necessary and adds comments to indicate where unchanged code has been skipped. For example:
|
||||
```language:path/to/file
|
||||
// ... existing code ...
|
||||
{% raw -%}
|
||||
{{ edit_1 }}
|
||||
{%- endraw %}
|
||||
// ... existing code ...
|
||||
{% raw -%}
|
||||
{{ edit_2 }}
|
||||
{%- endraw %}
|
||||
// ... existing code ...
|
||||
```
|
||||
The user can see the entire file, so they prefer to only read the updates to the code. Often this will mean that the start/end of the file will be skipped, but that's okay! Rewrite the entire file only if specifically requested. Always provide a brief explanation of the updates, unless the user specifically requests only the code.
|
||||
|
||||
These edit codeblocks are also read by a less intelligent language model, colloquially called the apply model, to update the file. To help specify the edit to the apply model, you will be very careful when generating the codeblock to not introduce ambiguity. You will specify all unchanged regions (code and comments) of the file with "// … existing code …" comment markers. This will ensure the apply model will not delete existing unchanged code or comments when editing the file. You will not mention the apply model.
|
||||
|
||||
2. Do not lie or make up facts.
|
||||
|
||||
3. If a user messages you in a foreign language, please respond in that language.
|
||||
|
||||
4. Format your response in markdown.
|
||||
|
||||
5. When writing out new code blocks, please specify the language ID after the initial backticks, like so:
|
||||
```python
|
||||
{% raw -%}
|
||||
{{ code }}
|
||||
{%- endraw %}
|
||||
```
|
||||
|
||||
6. When writing out code blocks for an existing file, please also specify the file path after the initial backticks and restate the method / class your codeblock belongs to, like so:
|
||||
```language:some/other/file
|
||||
function AIChatHistory() {
|
||||
...
|
||||
{% raw -%}
|
||||
{{ code }}
|
||||
{%- endraw %}
|
||||
...
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user