From 717fb7504966f3af415c7904819d487671e02e23 Mon Sep 17 00:00:00 2001 From: e3oroush Date: Sat, 6 Dec 2025 23:03:42 +0100 Subject: [PATCH 1/4] changes q to kiro-cli --- lua/askCode/agents/amazonq.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/askCode/agents/amazonq.lua b/lua/askCode/agents/amazonq.lua index 49d0e84..88cd3e7 100644 --- a/lua/askCode/agents/amazonq.lua +++ b/lua/askCode/agents/amazonq.lua @@ -15,7 +15,7 @@ end function M.prepare_command(prompt) -- Escape the prompt to ensure it's safely passed to the shell. local escaped_prompt = vim.fn.shellescape(prompt) - return string.format("echo %s | q chat --no-interactive 2>&1", escaped_prompt) + return string.format("echo %s | kiro-cli chat --no-interactive 2>&1", escaped_prompt) end --- Parses the response from the AmazonQ CLI. From 7c97edf30a406344ed66ef37a8f19b2d3c2d66ec Mon Sep 17 00:00:00 2001 From: e3oroush Date: Sun, 7 Dec 2025 12:16:48 +0100 Subject: [PATCH 2/4] feat: Add configurable window types for display --- README.md | 3 +- lua/askCode/config.lua | 1 + lua/askCode/init.lua | 10 +++--- lua/askCode/ui.lua | 70 +++++++++++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9f6dabe..33ed14d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ require("askCode").setup({ debug = false, -- Enable debug mode (default: false) quit_key = "q", -- Key to quit floating windows (default: "q") output_format = "json", -- Output format (default: "json") - window = { -- Floating window configuration + window = { -- Configuration for the display window + type = "float", -- Type of window: "float", "vertical", or "horizontal" (default: "float") width_ratio = 0.7, -- Window width as ratio of screen width (default: 0.7) height_ratio = 0.7, -- Window height as ratio of screen height (default: 0.7) max_width = 240, -- Maximum window width in columns (default: 240) diff --git a/lua/askCode/config.lua b/lua/askCode/config.lua index c7e4a0e..d0163e1 100644 --- a/lua/askCode/config.lua +++ b/lua/askCode/config.lua @@ -14,6 +14,7 @@ M.default = { quit_key = "q", output_format = "json", window = { + type = "float", -- float, vertical, horizontal width_ratio = 0.7, height_ratio = 0.7, max_width = 240, diff --git a/lua/askCode/init.lua b/lua/askCode/init.lua index 4a9e232..18b9da5 100644 --- a/lua/askCode/init.lua +++ b/lua/askCode/init.lua @@ -83,7 +83,7 @@ function M.ask(question, mode) end local command = agent.prepare_command(full_prompt) - state.win_id, state.buf_id = ui.show_in_float("Loading...", on_close) + state.win_id, state.buf_id = ui.show_window("Loading...", on_close) local response_lines = {} local on_stdout = function(_, data) @@ -114,7 +114,7 @@ function M.ask(question, mode) utils.append_file(state.history_file, agent_response) state.display_content = "AGENT: " .. response - ui.update_float(state.win_id, state.buf_id, state.display_content) + ui.update_window(state.win_id, state.buf_id, state.display_content) end runner.run_command( @@ -174,7 +174,7 @@ function M.follow_up(question) local new_display_part = string.format("\n\n---\n\nUSER: %s\n\nAGENT: %s", question, response) state.display_content = state.display_content .. new_display_part - ui.update_float(state.win_id, state.buf_id, state.display_content, cursor_position) + ui.update_window(state.win_id, state.buf_id, state.display_content, cursor_position) end runner.run_command({ "sh", "-c", command }, on_stdout, { on_exit = on_exit, stdout_buffered = true }) @@ -224,7 +224,7 @@ function M.ask_replace(question, mode) local command = agent.prepare_command(full_prompt) state.display_content = "Press Q to apply replacement, q to cancel\n\n" - state.win_id, state.buf_id = ui.show_in_float(state.display_content, on_close, true, on_apply) + state.win_id, state.buf_id = ui.show_window(state.display_content, on_close, true, on_apply) local response_lines = {} local on_stdout = function(_, data) @@ -245,7 +245,7 @@ function M.ask_replace(question, mode) end state.display_content = state.display_content .. response - ui.update_float(state.win_id, state.buf_id, state.display_content, nil, true) + ui.update_window(state.win_id, state.buf_id, state.display_content, nil, true) end runner.run_command({ "sh", "-c", command }, on_stdout, { on_exit = on_exit, stdout_buffered = true }) diff --git a/lua/askCode/ui.lua b/lua/askCode/ui.lua index 06e3085..0510cad 100644 --- a/lua/askCode/ui.lua +++ b/lua/askCode/ui.lua @@ -1,38 +1,60 @@ local config = require("askCode.config") local M = {} ---- Creates a floating window to show content. +--- Creates a window to show content. --- @param content string The content to display. --- @param on_close function A callback to execute when the window is closed. --- @param editable boolean|nil Whether the window should be editable (default: false). --- @param on_apply function|nil Callback for apply action (Q key) - receives edited content. --- @return number, number The window ID and buffer ID. -function M.show_in_float(content, on_close, editable, on_apply) +function M.show_window(content, on_close, editable, on_apply) local window_config = config.current_config.window + local window_type = window_config.type or "float" - local width = math.floor(vim.o.columns * window_config.width_ratio) - if width > window_config.max_width then - width = window_config.max_width - end - - local height = math.floor(vim.o.lines * window_config.height_ratio) - if height > window_config.max_height then - height = window_config.max_height - end - - local win_opts = { - relative = "cursor", - width = width, - height = height, - row = 1, - col = 0, - style = "minimal", - border = "rounded", - } local buf_id = vim.api.nvim_create_buf(false, true) vim.api.nvim_set_option_value("filetype", "markdown", { buf = buf_id }) - local win_id = vim.api.nvim_open_win(buf_id, true, win_opts) + local win_id + if window_type == "vertical" then + vim.api.nvim_command("vsplit") + win_id = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win_id, buf_id) + local width = math.floor(vim.o.columns * window_config.width_ratio) + if width > window_config.max_width then + width = window_config.max_width + end + vim.api.nvim_win_set_width(win_id, width) + elseif window_type == "horizontal" then + vim.api.nvim_command("split") + win_id = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win_id, buf_id) + local height = math.floor(vim.o.lines * window_config.height_ratio) + if height > window_config.max_height then + height = window_config.max_height + end + vim.api.nvim_win_set_height(win_id, height) + else -- float is the default + local width = math.floor(vim.o.columns * window_config.width_ratio) + if width > window_config.max_width then + width = window_config.max_width + end + + local height = math.floor(vim.o.lines * window_config.height_ratio) + if height > window_config.max_height then + height = window_config.max_height + end + + local win_opts = { + relative = "cursor", + width = width, + height = height, + row = 1, + col = 0, + style = "minimal", + border = "rounded", + } + win_id = vim.api.nvim_open_win(buf_id, true, win_opts) + end -- Prepare content with instructions if editable -- Set content @@ -77,13 +99,13 @@ function M.show_in_float(content, on_close, editable, on_apply) return win_id, buf_id end ---- Updates the content of a floating window. +--- Updates the content of a window. --- @param win_id number The ID of the window to update. --- @param buf_id number The ID of the buffer to update. --- @param content string The new content. --- @param replacement? boolean if the buffer is for replacement command --- @param cursor_line number|nil Optional line number to position cursor (defaults to end). -function M.update_float(win_id, buf_id, content, cursor_line, replacement) +function M.update_window(win_id, buf_id, content, cursor_line, replacement) vim.schedule(function() if not (buf_id and vim.api.nvim_buf_is_valid(buf_id)) then return From c7a520bfc5a098879c96e0b51c45989c9e76fd43 Mon Sep 17 00:00:00 2001 From: e3oroush Date: Sun, 7 Dec 2025 12:54:37 +0100 Subject: [PATCH 3/4] updates unit tests --- tests/test_amazonq.lua | 4 ++-- tests/test_ask_code.lua | 18 +++++++++--------- tests/test_config.lua | 2 ++ tests/test_ui.lua | 22 +++++++++++----------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/tests/test_amazonq.lua b/tests/test_amazonq.lua index cd5267c..5c2b60f 100644 --- a/tests/test_amazonq.lua +++ b/tests/test_amazonq.lua @@ -32,12 +32,12 @@ T["prepare_command"] = new_set() T["prepare_command"]["should format command correctly"] = function() local command = child.lua_get([[Agent.prepare_command("test prompt")]]) - eq(command, "echo 'test prompt' | q chat --no-interactive 2>&1") + eq(command, "echo 'test prompt' | kiro-cli chat --no-interactive 2>&1") end T["prepare_command"]["should escape special characters"] = function() local command = child.lua_get([[Agent.prepare_command("test 'quoted' prompt")]]) - eq(command, "echo 'test '\\''quoted'\\'' prompt' | q chat --no-interactive 2>&1") + eq(command, "echo 'test '\\''quoted'\\'' prompt' | kiro-cli chat --no-interactive 2>&1") end -- Test parse_response function diff --git a/tests/test_ask_code.lua b/tests/test_ask_code.lua index efb7ee1..674aaeb 100644 --- a/tests/test_ask_code.lua +++ b/tests/test_ask_code.lua @@ -20,15 +20,15 @@ local T = new_set({ config = require('askCode.config') -- Mock UI functions - _G.show_in_float_calls = {} - ui.show_in_float = function(content, on_close) - table.insert(_G.show_in_float_calls, {content = content}) + _G.show_window_calls = {} + ui.show_window = function(content, on_close) + table.insert(_G.show_window_calls, {content = content}) return 1, 1 -- return mock win_id and buf_id end - _G.update_float_calls = {} - ui.update_float = function(win_id, buf_id, content) - table.insert(_G.update_float_calls, {win_id = win_id, buf_id = buf_id, content = content}) + _G.update_window_calls = {} + ui.update_window = function(win_id, buf_id, content) + table.insert(_G.update_window_calls, {win_id = win_id, buf_id = buf_id, content = content}) end ]]) end, @@ -63,7 +63,7 @@ T["ask"]["should show plain text response in new window"] = function() child.lua("vim.loop.sleep(100)") -- Wait for async operations -- 3. Assert the expected outcome - local update_calls = child.lua_get("_G.update_float_calls") + local update_calls = child.lua_get("_G.update_window_calls") eq(#update_calls, 1) eq(update_calls[1].content, "AGENT: mocked response") end @@ -92,7 +92,7 @@ T["ask"]["should parse and show JSON response in new window"] = function() child.lua("vim.loop.sleep(100)") -- Wait for async operations -- 3. Assert the expected outcome - local update_calls = child.lua_get("_G.update_float_calls") + local update_calls = child.lua_get("_G.update_window_calls") eq(#update_calls, 1) eq(update_calls[1].content, "AGENT: parsed content") end @@ -130,7 +130,7 @@ T["follow_up"]["should update window with conversation"] = function() child.lua("vim.loop.sleep(100)") -- Wait for async operations -- 3. Assert the expected outcome - local update_calls = child.lua_get("_G.update_float_calls") + local update_calls = child.lua_get("_G.update_window_calls") eq(#update_calls, 2) -- One for initial ask, one for follow-up local expected_content = "AGENT: initial response\n\n---\n\nUSER: follow-up question\n\nAGENT: follow-up response" eq(update_calls[2].content, expected_content) diff --git a/tests/test_config.lua b/tests/test_config.lua index 53339a3..adf48b6 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -33,6 +33,7 @@ T["config"]["default value"] = function() quit_key = "q", output_format = "json", window = { + type = "float", width_ratio = 0.7, height_ratio = 0.7, max_width = 240, @@ -49,6 +50,7 @@ T["config"]["custom value"] = function() quit_key = "q", output_format = "json", window = { + type = "float", width_ratio = 0.7, height_ratio = 0.7, max_width = 240, diff --git a/tests/test_ui.lua b/tests/test_ui.lua index afbfbb7..8669bdd 100644 --- a/tests/test_ui.lua +++ b/tests/test_ui.lua @@ -23,13 +23,13 @@ local T = new_set({ }, }) --- Test set for show_in_float -T["show_in_float()"] = new_set() +-- Test set for show_window +T["show_window()"] = new_set() -T["show_in_float()"]["creates a floating window with content"] = function() +T["show_window()"]["creates a floating window with content"] = function() -- Execute the function child.lua([[ - win_id, buf_id = ui.show_in_float("hello\nworld") + win_id, buf_id = ui.show_window("hello\nworld") ]]) local win_id = child.lua_get("win_id") local buf_id = child.lua_get("buf_id") @@ -43,13 +43,13 @@ T["show_in_float()"]["creates a floating window with content"] = function() eq(buffer_content, { "hello", "world" }) end -T["show_in_float()"]["calls on_close when window is closed"] = function() +T["show_window()"]["calls on_close when window is closed"] = function() child.lua([[ _G.close_called = false local on_close = function() _G.close_called = true end - win_id, buf_id = ui.show_in_float("test", on_close) + win_id, buf_id = ui.show_window("test", on_close) ]]) -- Close the window by sending 'q' @@ -61,19 +61,19 @@ T["show_in_float()"]["calls on_close when window is closed"] = function() end --- Test set for update_float -T["update_float()"] = new_set() +-- Test set for update_window +T["update_window()"] = new_set() -T["update_float()"]["updates the content of a floating window"] = function() +T["update_window()"]["updates the content of a floating window"] = function() -- Create a window first child.lua([[ - win_id, buf_id = ui.show_in_float("initial content") + win_id, buf_id = ui.show_window("initial content") ]]) local win_id = child.lua_get("win_id") local buf_id = child.lua_get("buf_id") -- Update the window - child.lua("ui.update_float(" .. win_id .. ", " .. buf_id .. ", [[new\ncontent]])") + child.lua("ui.update_window(" .. win_id .. ", " .. buf_id .. ", [[new\ncontent]])") child.lua("vim.loop.sleep(50)") -- Wait for vim.schedule -- Verify buffer content From 39d848e91398c0a983971c86bfba5c3f1cc01787 Mon Sep 17 00:00:00 2001 From: e3oroush Date: Sun, 7 Dec 2025 13:17:43 +0100 Subject: [PATCH 4/4] feat(config, cli): Introduce runtime configuration management Implement a comprehensive configuration management system allowing runtime inspection and modification of settings. This change includes: * Adding get, set, reset_config, and get_all_keys functions in lua/askCode/config.lua to handle dynamic configuration access, including automatic type conversion for common values. * Exposing get_config and set_config in lua/askCode/init.lua for programmatic interaction. * Introducing the :AskCodeConfig Neovim user command in plugin/askCode.lua with tab completion, enabling users to inspect and modify configuration values directly. * Updating README.md to document the new configuration management features and command usage. * Adding extensive unit tests for the new get, set, and reset_config functionalities in tests/test_config.lua. * Updating repository owner links from e3oroush to realEbi in README.md. --- README.md | 36 +++++++++++++++++-- lua/askCode/config.lua | 79 ++++++++++++++++++++++++++++++++++++++---- lua/askCode/init.lua | 15 ++++++++ plugin/askCode.lua | 31 +++++++++++++++++ tests/test_config.lua | 66 +++++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 33ed14d..cf50964 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Install `askCode.nvim` using your favorite package manager. ```lua { - "e3oroush/askCode", + "realEbi/askCode", config = function() require("askCode").setup({ -- Your configuration here @@ -41,7 +41,7 @@ Install `askCode.nvim` using your favorite package manager. ```lua use { - "e3oroush/askCode", + "realEbi/askCode", config = function() require("askCode").setup({ -- Your configuration here @@ -90,6 +90,36 @@ The `:AskCode` command automatically detects whether to start a new conversation **Note**: Code replacement only works in visual mode with selected text. +### Configuration Management + +You can get and set configuration values at runtime using the `:AskCodeConfig` command: + +**Get a configuration value:** +```vim +:AskCodeConfig agent +:AskCodeConfig window.type +``` + +**Set a configuration value:** +```vim +:AskCodeConfig agent amazonq +:AskCodeConfig window.type vertical +:AskCodeConfig window.width_ratio 0.8 +:AskCodeConfig debug true +``` + +The command supports tab completion for all available configuration keys. Values are automatically converted to the appropriate type (booleans, numbers, strings). + +**Programmatic access:** +```lua +-- Get config +local agent = require("askCode").get_config("agent") +local all_config = require("askCode").get_config() + +-- Set config +require("askCode").set_config("window.type", "horizontal") +``` + ### Keybinding Examples You can map the commands to keybindings for easier access: @@ -111,7 +141,7 @@ Contributions are welcome! To get started with development: 1. **Clone the repository**: ```sh - git clone https://github.com/e3oroush/askCode.git askCode.nvim + git clone https://github.com/realEbi/askCode.git askCode.nvim cd askCode.nvim ``` diff --git a/lua/askCode/config.lua b/lua/askCode/config.lua index d0163e1..642547d 100644 --- a/lua/askCode/config.lua +++ b/lua/askCode/config.lua @@ -27,14 +27,81 @@ M.default = { ---@return Config function M.merge_with_default(changes) changes = changes or {} - - -- merge basic settings - ---@type Config - local config = vim.tbl_deep_extend("force", M.default, changes) - M.current_config = config + M.current_config = vim.tbl_deep_extend("keep", vim.deepcopy(changes), M.current_config) return M.current_config end -M.current_config = M.default +M.current_config = vim.deepcopy(M.default) + +--- gets a config value +---@param key? string dot separated key +---@return any +function M.get(key) + if not key then + return M.current_config + end + local parts = vim.split(key, "%.") + local current = M.current_config + for _, part in ipairs(parts) do + if type(current) ~= "table" or current[part] == nil then + return nil + end + current = current[part] + end + return current +end + +--- sets a config value +---@param key string dot separated key +---@param value any +---@return any new value +function M.set(key, value) + local parts = vim.split(key, "%.") + if type(value) == "string" then + if value == "true" then + value = true + elseif value == "false" then + value = false + elseif tonumber(value) then + value = tonumber(value) + end + end + + local t = {} + local current = t + for i, part in ipairs(parts) do + if i == #parts then + current[part] = value + else + current[part] = {} + current = current[part] + end + end + M.merge_with_default(t) + return M.get(key) +end + +--- resets config to default +function M.reset_config() + M.current_config = vim.deepcopy(M.default) +end + +--- gets all config keys as dot-separated strings +---@return string[] +function M.get_all_keys() + local keys = {} + local function traverse(tbl, prefix) + for key, value in pairs(tbl) do + local full_key = prefix == "" and key or prefix .. "." .. key + if type(value) == "table" then + traverse(value, full_key) + else + table.insert(keys, full_key) + end + end + end + traverse(M.default, "") + return keys +end return M diff --git a/lua/askCode/init.lua b/lua/askCode/init.lua index 18b9da5..351d473 100644 --- a/lua/askCode/init.lua +++ b/lua/askCode/init.lua @@ -19,6 +19,21 @@ function M.setup(cfg) config.merge_with_default(cfg) end +--- Gets a config value +---@param key? string dot separated key (e.g., "window.type"). If nil, returns entire config +---@return any +function M.get_config(key) + return config.get(key) +end + +--- Sets a config value +---@param key string dot separated key (e.g., "window.type") +---@param value any the value to set +---@return any the new value +function M.set_config(key, value) + return config.set(key, value) +end + --- Constructs a prompt for the AI assistant based on question, mode, and conversation history --- @param question string The user's question or request --- @param mode string The current editor mode (visual, normal, etc.) diff --git a/plugin/askCode.lua b/plugin/askCode.lua index 1b3f1cf..5933ca3 100644 --- a/plugin/askCode.lua +++ b/plugin/askCode.lua @@ -38,6 +38,37 @@ vim.api.nvim_create_user_command("AskCodeReplace", function(opts) end end, { range = true, nargs = "?" }) +vim.api.nvim_create_user_command("AskCodeConfig", function(opts) + local args = vim.split(opts.args, "%s+") + local key = args[1] + local value = args[2] + + if not key or key == "" then + vim.notify("Usage: AskCodeConfig [value]", vim.log.levels.ERROR) + return + end + + if value then + local new_value = require("askCode").set_config(key, value) + vim.notify(string.format("Set %s = %s", key, vim.inspect(new_value)), vim.log.levels.INFO) + else + local current_value = require("askCode").get_config(key) + vim.notify(string.format("%s = %s", key, vim.inspect(current_value)), vim.log.levels.INFO) + end +end, { + nargs = "+", + complete = function(arg_lead, cmd_line, cursor_pos) + local args = vim.split(cmd_line, "%s+") + if #args <= 2 then + local keys = require("askCode.config").get_all_keys() + return vim.tbl_filter(function(key) + return vim.startswith(key, arg_lead) + end, keys) + end + return {} + end, +}) + vim.keymap.set( "v", "(AskCodeExplain)", diff --git a/tests/test_config.lua b/tests/test_config.lua index adf48b6..4c8a0b7 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -59,4 +59,70 @@ T["config"]["custom value"] = function() }) end +T["get()"] = new_set() + +T["get()"]["returns entire config when no key provided"] = function() + local config = child.lua_get([[require('askCode.config').get()]]) + eq(config.agent, "gemini") + eq(config.debug, false) +end + +T["get()"]["returns top-level value"] = function() + eq(child.lua_get([[require('askCode.config').get('agent')]]), "gemini") + eq(child.lua_get([[require('askCode.config').get('debug')]]), false) +end + +T["get()"]["returns nested value with dot notation"] = function() + eq(child.lua_get([[require('askCode.config').get('window.type')]]), "float") + eq(child.lua_get([[require('askCode.config').get('window.width_ratio')]]), 0.7) +end + +T["get()"]["returns nil for non-existent key"] = function() + eq(child.lua_get([[require('askCode.config').get('nonexistent')]]), vim.NIL) + eq(child.lua_get([[require('askCode.config').get('window.nonexistent')]]), vim.NIL) +end + +T["set()"] = new_set() + +T["set()"]["sets top-level value"] = function() + child.lua([[require('askCode.config').set('agent', 'amazonq')]]) + eq(child.lua_get([[require('askCode.config').get('agent')]]), "amazonq") +end + +T["set()"]["sets nested value with dot notation"] = function() + child.lua([[require('askCode.config').set('window.type', 'vertical')]]) + eq(child.lua_get([[require('askCode.config').get('window.type')]]), "vertical") +end + +T["set()"]["converts string 'true' to boolean"] = function() + child.lua([[require('askCode.config').set('debug', 'true')]]) + eq(child.lua_get([[require('askCode.config').get('debug')]]), true) +end + +T["set()"]["converts string 'false' to boolean"] = function() + child.lua([[require('askCode.config').set('debug', 'false')]]) + eq(child.lua_get([[require('askCode.config').get('debug')]]), false) +end + +T["set()"]["converts numeric string to number"] = function() + child.lua([[require('askCode.config').set('window.max_width', '300')]]) + eq(child.lua_get([[require('askCode.config').get('window.max_width')]]), 300) +end + +T["set()"]["returns the new value"] = function() + local result = child.lua_get([[require('askCode.config').set('agent', 'gemini')]]) + eq(result, "gemini") +end + +T["reset_config()"] = new_set() + +T["reset_config()"]["resets config to default values"] = function() + child.lua([[require('askCode.config').set('agent', 'amazonq')]]) + child.lua([[require('askCode.config').set('window.type', 'vertical')]]) + child.lua([[require('askCode.config').reset_config()]]) + + eq(child.lua_get([[require('askCode.config').get('agent')]]), "gemini") + eq(child.lua_get([[require('askCode.config').get('window.type')]]), "float") +end + return T