From b21d2632d3990f162aee57f9151864cd2fd77cac Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 17 Feb 2025 18:49:29 +0800 Subject: [PATCH] feat: cursor planning mode (#1289) --- README.md | 6 + crates/avante-templates/src/lib.rs | 4 +- cursor-planning-mode.md | 35 +++ lua/avante/config.lua | 2 + lua/avante/llm.lua | 73 +++-- lua/avante/providers/init.lua | 10 +- lua/avante/selection.lua | 2 +- lua/avante/sidebar.lua | 286 +++++++++++++++++- .../templates/cursor-applying.avanterules | 1 + .../templates/cursor-planning.avanterules | 41 +++ 10 files changed, 429 insertions(+), 31 deletions(-) create mode 100644 cursor-planning-mode.md create mode 100644 lua/avante/templates/cursor-applying.avanterules create mode 100644 lua/avante/templates/cursor-planning.avanterules diff --git a/README.md b/README.md index c7f3363..0ab734e 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/crates/avante-templates/src/lib.rs b/crates/avante-templates/src/lib.rs index ece86c4..8a4d23f 100644 --- a/crates/avante-templates/src/lib.rs +++ b/crates/avante-templates/src/lib.rs @@ -27,11 +27,12 @@ struct TemplateContext { use_xml_format: bool, ask: bool, code_lang: String, - selected_files: Vec, + selected_files: Option>, selected_code: Option, project_context: Option, diagnostics: Option, system_info: Option, + model_name: Option, } // 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()) diff --git a/cursor-planning-mode.md b/cursor-planning-mode.md new file mode 100644 index 0000000..2357467 --- /dev/null +++ b/cursor-planning-mode.md @@ -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 +} +``` diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 2bcbcc6..620cbfa 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -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, diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index 064cb10..f75d6b2 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -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("%s", 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("%s", 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 snippet into the below. +- Preserve the code's structure, order, comments, and indentation exactly. +- Output only the updated code, enclosed within and tags. +- Do not include any additional text, explanations, placeholders, ellipses, or code fences. + +]] + user_prompt = user_prompt .. string.format("\n%s\n\n", opts.original_code) + for _, snippet in ipairs(opts.update_snippets) do + user_prompt = user_prompt .. string.format("\n%s\n\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 diff --git a/lua/avante/providers/init.lua b/lua/avante/providers/init.lua index 91aa37d..3ee362c 100644 --- a/lua/avante/providers/init.lua +++ b/lua/avante/providers/init.lua @@ -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, headers: table, 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 diff --git a/lua/avante/selection.lua b/lua/avante/selection.lua index db68127..f2f842e 100644 --- a/lua/avante/selection.lua +++ b/lua/avante/selection.lua @@ -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, }) diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index 02a25d3..8b28f1c 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -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 +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 @@ -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("\n*", ""):gsub("\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("\n*", ""):gsub("\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("", 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 diff --git a/lua/avante/templates/cursor-applying.avanterules b/lua/avante/templates/cursor-applying.avanterules new file mode 100644 index 0000000..c8f61c4 --- /dev/null +++ b/lua/avante/templates/cursor-applying.avanterules @@ -0,0 +1 @@ +You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated. diff --git a/lua/avante/templates/cursor-planning.avanterules b/lua/avante/templates/cursor-planning.avanterules new file mode 100644 index 0000000..3c1de35 --- /dev/null +++ b/lua/avante/templates/cursor-planning.avanterules @@ -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 %} + ... +} +```