diff --git a/lua/avante/path.lua b/lua/avante/path.lua index a7b5053..794d02e 100644 --- a/lua/avante/path.lua +++ b/lua/avante/path.lua @@ -31,7 +31,17 @@ local History = {} function History.get_history_dir(bufnr) local dirname = generate_project_dirname_in_storage(bufnr) local history_dir = Path:new(Config.history.storage_path):joinpath(dirname):joinpath("history") - if not history_dir:exists() then history_dir:mkdir({ parents = true }) end + if not history_dir:exists() then + history_dir:mkdir({ parents = true }) + + local metadata_filepath = history_dir:joinpath("metadata.json") + local metadata = { + project_root = Utils.root.get({ + buf = bufnr, + }), + } + metadata_filepath:write(vim.json.encode(metadata), "w") + end return history_dir end @@ -105,13 +115,14 @@ end function History.save_latest_filename(bufnr, filename) local metadata_filepath = History.get_metadata_filepath(bufnr) - local metadata - if not metadata_filepath:exists() then - metadata = {} - else + local metadata = {} + if metadata_filepath:exists() then local metadata_content = metadata_filepath:read() metadata = vim.json.decode(metadata_content) end + if metadata.project_root == nil then metadata.project_root = Utils.root.get({ + buf = bufnr, + }) end metadata.latest_filename = filename metadata_filepath:write(vim.json.encode(metadata), "w") end @@ -187,6 +198,56 @@ end P.history = History +---@return table[] List of projects with their information +function P.list_projects() + local projects_dir = Path:new(Config.history.storage_path):joinpath("projects") + if not projects_dir:exists() then return {} end + + local projects = {} + local dirs = Scan.scan_dir(tostring(projects_dir), { depth = 1, add_dirs = true, only_dirs = true }) + + for _, dir_path in ipairs(dirs) do + local project_dir = Path:new(dir_path) + local history_dir = project_dir:joinpath("history") + + local metadata_file = history_dir:joinpath("metadata.json") + local project_root = "" + if metadata_file:exists() then + local content = metadata_file:read() + if content then + local metadata = vim.json.decode(content) + if metadata and metadata.project_root then project_root = metadata.project_root end + end + end + + -- Skip if project_root is empty + if project_root == "" then goto continue end + + -- Count history files + local history_count = 0 + if history_dir:exists() then + local history_files = vim.fn.glob(tostring(history_dir:joinpath("*.json")), true, true) + for _, file in ipairs(history_files) do + if not file:match("metadata.json") then history_count = history_count + 1 end + end + end + + table.insert(projects, { + name = filepath_to_filename(project_dir), + root = project_root, + history_count = history_count, + directory = tostring(project_dir), + }) + + ::continue:: + end + + -- Sort by history count (most active projects first) + table.sort(projects, function(a, b) return a.history_count > b.history_count end) + + return projects +end + -- Prompt path local Prompt = {} diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index ae164d1..a3cf7c5 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -1607,6 +1607,44 @@ function M.uuid() end) end +---Parse command arguments (fargs) into a structured format +---@param fargs string[] Command arguments +---@param options? {collect_remaining?: boolean, boolean_keys?: string[]} Options for parsing +---@return table parsed_args Key-value pairs from arguments +---@return string|nil remaining_text Concatenated remaining arguments (if collect_remaining is true) +function M.parse_args(fargs, options) + options = options or {} + local parsed_args = {} + local remaining_parts = {} + local boolean_keys = options.boolean_keys or {} + + -- Create a lookup table for boolean keys for faster access + local boolean_keys_lookup = {} + for _, key in ipairs(boolean_keys) do + boolean_keys_lookup[key] = true + end + + for _, arg in ipairs(fargs) do + local key, value = arg:match("([%w_]+)=(.+)") + + if key and value then + -- Convert "true"/"false" string values to boolean for specified keys + if boolean_keys_lookup[key] or value == "true" or value == "false" then + parsed_args[key] = (value == "true") + else + parsed_args[key] = value + end + elseif options.collect_remaining then + table.insert(remaining_parts, arg) + end + end + + -- Return the parsed arguments and optionally the concatenated remaining text + if options.collect_remaining and #remaining_parts > 0 then return parsed_args, table.concat(remaining_parts, " ") end + + return parsed_args +end + ---@param tool_use AvanteLLMToolUse function M.tool_use_to_xml(tool_use) local tool_use_json = vim.json.encode({ diff --git a/plugin/avante.lua b/plugin/avante.lua index 96db02d..161d25a 100644 --- a/plugin/avante.lua +++ b/plugin/avante.lua @@ -16,6 +16,7 @@ vim.g.avante = 1 local Clipboard = require("avante.clipboard") local Config = require("avante.config") local Utils = require("avante.utils") +local P = require("avante.path") local api = vim.api if Config.support_paste_image() then @@ -51,52 +52,62 @@ local function cmd(n, c, o) api.nvim_create_user_command("Avante" .. n, c, o) end +local function ask_complete(prefix, _, _) + local candidates = {} ---@type string[] + vim.list_extend( + candidates, + ---@param x string + vim.tbl_map(function(x) return "position=" .. x end, { "left", "right", "top", "bottom" }) + ) + vim.list_extend( + candidates, + ---@param x string + vim.tbl_map(function(x) return "project_root=" .. x.root end, P.list_projects()) + ) + return vim.tbl_filter(function(candidate) return vim.startswith(candidate, prefix) end, candidates) +end + cmd("Ask", function(opts) ---@type AskOptions local args = { question = nil, win = {} } - local q_parts = {} - local q_ask = nil - for _, arg in ipairs(opts.fargs) do - local value = arg:match("position=(%w+)") - local ask = arg:match("ask=(%w+)") - if ask ~= nil then - q_ask = ask == "true" - elseif value then - args.win.position = value - else - table.insert(q_parts, arg) - end - end - require("avante.api").ask( - vim.tbl_deep_extend("force", args, { ask = q_ask, question = #q_parts > 0 and table.concat(q_parts, " ") or nil }) - ) + + local parsed_args, question = Utils.parse_args(opts.fargs, { + collect_remaining = true, + boolean_keys = { "ask" }, + }) + + if parsed_args.position then args.win.position = parsed_args.position end + + require("avante.api").ask(vim.tbl_deep_extend("force", args, { + ask = parsed_args.ask, + project_root = parsed_args.project_root, + question = question or nil, + })) end, { desc = "avante: ask AI for code suggestions", nargs = "*", - complete = function(_, _, _) - local candidates = {} ---@type string[] - vim.list_extend( - candidates, - ---@param x string - vim.tbl_map(function(x) return "position=" .. x end, { "left", "right", "top", "bottom" }) - ) - vim.list_extend(candidates, vim.tbl_map(function(x) return "ask=" .. x end, { "true", "false" })) - return candidates - end, + complete = ask_complete, }) -cmd("Chat", function() require("avante.api").ask({ ask = false }) end, { desc = "avante: chat with the codebase" }) -cmd( - "ChatNew", - function() require("avante.api").ask({ ask = false, new_chat = true }) end, - { desc = "avante: create new chat" } -) +cmd("Chat", function(opts) + local args = Utils.parse_args(opts.fargs) + args.ask = false + + require("avante.api").ask(args) +end, { + desc = "avante: chat with the codebase", + nargs = "*", + complete = ask_complete, +}) +cmd("ChatNew", function(opts) + local args = Utils.parse_args(opts.fargs) + args.ask = false + args.new_chat = true + require("avante.api").ask(args) +end, { desc = "avante: create new chat", nargs = "*", complete = ask_complete }) cmd("Toggle", function() require("avante").toggle() end, { desc = "avante: toggle AI panel" }) cmd("Build", function(opts) - local args = {} - for _, arg in ipairs(opts.fargs) do - local key, value = arg:match("(%w+)=(%w+)") - if key and value then args[key] = value == "true" end - end + local args = Utils.parse_args(opts.fargs) + if args.source == nil then args.source = false end require("avante.api").build(args) @@ -149,11 +160,10 @@ cmd("Clear", function(opts) end sidebar:clear_history() elseif arg == "cache" then - local P = require("avante.path") local history_path = P.history_path:absolute() local cache_path = P.cache_path:absolute() local prompt = string.format("Recursively delete %s and %s?", history_path, cache_path) - if vim.fn.confirm(prompt, "&Yes\n&No", 2) == 1 then require("avante.path").clear() end + if vim.fn.confirm(prompt, "&Yes\n&No", 2) == 1 then P.clear() end else Utils.error("Invalid argument. Valid arguments: 'history', 'memory', 'cache'") return