Files
avante.nvim/lua/avante/api.lua
Dmitry Torokhov 7d60b51324 refactor(sidebar): move sub-windows into a table
Maintaining secondary table of window IDs is cumbersome and is prone to
getting out of sync with the true state of the sidebar. In preparation
for removal of winids table move all containers (sub-windows of the
sidebar) into "containers" table.

The change is mostly mechanical rename with following exceptions:

 - Sidebar:reifresh_winids() and other places where the code scanned
   entire Sidebar object looking for tables with specific fields, such
   as "winid", or "mount" needed to be adjusted for the new structure

 - Sidebar:new() and Sidebar:reset() have been adjusted to make better
   use of the new sub-table.
2025-07-21 13:31:07 +08:00

315 lines
9.9 KiB
Lua

local Config = require("avante.config")
local Utils = require("avante.utils")
local PromptInput = require("avante.ui.prompt_input")
---@class avante.ApiToggle
---@operator call(): boolean
---@field debug ToggleBind.wrap
---@field hint ToggleBind.wrap
---@class avante.Api
---@field toggle avante.ApiToggle
local M = {}
---@param target_provider avante.SelectorProvider
function M.switch_selector_provider(target_provider)
require("avante.config").override({
selector = {
provider = target_provider,
},
})
end
---@param target_provider avante.InputProvider
function M.switch_input_provider(target_provider)
require("avante.config").override({
input = {
provider = target_provider,
},
})
end
---@param target avante.ProviderName
function M.switch_provider(target) require("avante.providers").refresh(target) end
---@param path string
local function to_windows_path(path)
local winpath = path:gsub("/", "\\")
if winpath:match("^%a:") then winpath = winpath:sub(1, 2):upper() .. winpath:sub(3) end
winpath = winpath:gsub("\\$", "")
return winpath
end
---@param opts? {source: boolean}
function M.build(opts)
opts = opts or { source = true }
local dirname = Utils.trim(string.sub(debug.getinfo(1).source, 2, #"/init.lua" * -1), { suffix = "/" })
local git_root = vim.fs.find(".git", { path = dirname, upward = true })[1]
local build_directory = git_root and vim.fn.fnamemodify(git_root, ":h") or (dirname .. "/../../")
if opts.source and not vim.fn.executable("cargo") then
error("Building avante.nvim requires cargo to be installed.", 2)
end
---@type string[]
local cmd
local os_name = Utils.get_os_name()
if vim.tbl_contains({ "linux", "darwin" }, os_name) then
cmd = {
"sh",
"-c",
string.format("make BUILD_FROM_SOURCE=%s -C %s", opts.source == true and "true" or "false", build_directory),
}
elseif os_name == "windows" then
build_directory = to_windows_path(build_directory)
cmd = {
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
string.format("%s\\Build.ps1", build_directory),
"-WorkingDirectory",
build_directory,
"-BuildFromSource",
string.format("%s", opts.source == true and "true" or "false"),
}
else
error("Unsupported operating system: " .. os_name, 2)
end
---@type integer
local pid
local exit_code = { 0 }
local ok, job_or_err = pcall(vim.system, cmd, { text = true }, function(obj)
local stderr = obj.stderr and vim.split(obj.stderr, "\n") or {}
local stdout = obj.stdout and vim.split(obj.stdout, "\n") or {}
if vim.tbl_contains(exit_code, obj.code) then
local output = stdout
if #output == 0 then
table.insert(output, "")
Utils.debug("build output:", output)
else
Utils.debug("build error:", stderr)
end
end
end)
if not ok then Utils.error("Failed to build the command: " .. cmd .. "\n" .. job_or_err, { once = true }) end
pid = job_or_err.pid
return pid
end
---@class AskOptions
---@field question? string optional questions
---@field win? table<string, any> windows options similar to |nvim_open_win()|
---@field ask? boolean
---@field floating? boolean whether to open a floating input to enter the question
---@field new_chat? boolean whether to open a new chat
---@field without_selection? boolean whether to open a new chat without selection
---@param opts? AskOptions
function M.ask(opts)
opts = opts or {}
if type(opts) == "string" then
Utils.warn("passing 'ask' as string is deprecated, do {question = '...'} instead", { once = true })
opts = { question = opts }
end
local has_question = opts.question ~= nil and opts.question ~= ""
local new_chat = opts.new_chat == true
if Utils.is_sidebar_buffer(0) and not has_question and not new_chat then
require("avante").close_sidebar()
return false
end
opts = vim.tbl_extend("force", { selection = Utils.get_visual_selection_and_range() }, opts)
---@param input string | nil
local function ask(input)
if input == nil or input == "" then input = opts.question end
local sidebar = require("avante").get()
if sidebar and sidebar:is_open() and sidebar.code.bufnr ~= vim.api.nvim_get_current_buf() then
sidebar:close({ goto_code_win = false })
end
require("avante").open_sidebar(opts)
if new_chat then sidebar:new_chat() end
if opts.without_selection then
sidebar.code.selection = nil
sidebar.file_selector:reset()
if sidebar.containers.selected_files then sidebar.containers.selected_files:unmount() end
end
if input == nil or input == "" then return true end
vim.api.nvim_exec_autocmds("User", { pattern = "AvanteInputSubmitted", data = { request = input } })
return true
end
if opts.floating == true or (Config.windows.ask.floating == true and not has_question and opts.floating == nil) then
local prompt_input = PromptInput:new({
submit_callback = function(input) ask(input) end,
close_on_submit = true,
win_opts = {
border = Config.windows.ask.border,
title = { { "Avante Ask", "FloatTitle" } },
},
start_insert = Config.windows.ask.start_insert,
default_value = opts.question,
})
prompt_input:open()
return true
end
return ask()
end
---@param request? string
---@param line1? integer
---@param line2? integer
function M.edit(request, line1, line2)
local _, selection = require("avante").get()
if not selection then return end
selection:create_editing_input(request, line1, line2)
if request ~= nil and request ~= "" then
vim.api.nvim_exec_autocmds("User", { pattern = "AvanteEditSubmitted", data = { request = request } })
end
end
---@return avante.Suggestion | nil
function M.get_suggestion()
local _, _, suggestion = require("avante").get()
return suggestion
end
---@param opts? AskOptions
function M.refresh(opts)
opts = opts or {}
local sidebar = require("avante").get()
if not sidebar then return end
if not sidebar:is_open() then return end
local curbuf = vim.api.nvim_get_current_buf()
local focused = sidebar.containers.result.bufnr == curbuf or sidebar.containers.input.bufnr == curbuf
if focused or not sidebar:is_open() then return end
local listed = vim.api.nvim_get_option_value("buflisted", { buf = curbuf })
if Utils.is_sidebar_buffer(curbuf) or not listed then return end
local curwin = vim.api.nvim_get_current_win()
sidebar:close()
sidebar.code.winid = curwin
sidebar.code.bufnr = curbuf
sidebar:render(opts)
end
---@param opts? AskOptions
function M.focus(opts)
opts = opts or {}
local sidebar = require("avante").get()
if not sidebar then return end
local curbuf = vim.api.nvim_get_current_buf()
local curwin = vim.api.nvim_get_current_win()
if sidebar:is_open() then
if curbuf == sidebar.containers.input.bufnr then
if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end
elseif curbuf == sidebar.containers.result.bufnr then
if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end
else
if sidebar.containers.input.winid and sidebar.containers.input.winid ~= curwin then
vim.api.nvim_set_current_win(sidebar.containers.input.winid)
end
end
else
if sidebar.code.winid then vim.api.nvim_set_current_win(sidebar.code.winid) end
---@cast opts SidebarOpenOptions
sidebar:open(opts)
if sidebar.containers.input.winid then vim.api.nvim_set_current_win(sidebar.containers.input.winid) end
end
end
function M.select_model() require("avante.model_selector").open() end
function M.select_history()
local buf = vim.api.nvim_get_current_buf()
require("avante.history_selector").open(buf, function(filename)
vim.api.nvim_buf_call(buf, function()
if not require("avante").is_sidebar_open() then require("avante").open_sidebar({}) end
local Path = require("avante.path")
Path.history.save_latest_filename(buf, filename)
local sidebar = require("avante").get()
sidebar:update_content_with_history()
sidebar:create_todos_container()
sidebar:initialize_token_count()
vim.schedule(function() sidebar:focus_input() end)
end)
end)
end
function M.add_buffer_files()
local sidebar = require("avante").get()
if not sidebar then
require("avante.api").ask()
sidebar = require("avante").get()
end
if not sidebar:is_open() then sidebar:open({}) end
sidebar.file_selector:add_buffer_files()
end
function M.add_selected_file(filepath)
local rel_path = Utils.uniform_path(filepath)
local sidebar = require("avante").get()
if not sidebar then
require("avante.api").ask()
sidebar = require("avante").get()
end
if not sidebar:is_open() then sidebar:open({}) end
sidebar.file_selector:add_selected_file(rel_path)
end
function M.remove_selected_file(filepath)
---@diagnostic disable-next-line: undefined-field
local stat = vim.loop.fs_stat(filepath)
local files
if stat and stat.type == "directory" then
files = Utils.scan_directory({ directory = filepath, add_dirs = true })
else
files = { filepath }
end
local sidebar = require("avante").get()
if not sidebar then
require("avante.api").ask()
sidebar = require("avante").get()
end
if not sidebar:is_open() then sidebar:open({}) end
for _, file in ipairs(files) do
local rel_path = Utils.uniform_path(file)
sidebar.file_selector:remove_selected_file(rel_path)
end
end
function M.stop() require("avante.llm").cancel_inflight_request() end
return setmetatable(M, {
__index = function(t, k)
local module = require("avante")
---@class AvailableApi: ApiCaller
---@field api? boolean
local has = module[k]
if type(has) ~= "table" or not has.api then
Utils.warn(k .. " is not a valid avante's API method", { once = true })
return
end
t[k] = has
return t[k]
end,
}) --[[@as avante.Api]]