From d0d644da9387464f8568f79609133612f0c45c49 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 12:25:00 -0600 Subject: [PATCH 1/9] fix(commands): account for missing last arg in wrap_cursor --- lua/python/commands.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/python/commands.lua b/lua/python/commands.lua index 66700b8..4fae837 100644 --- a/lua/python/commands.lua +++ b/lua/python/commands.lua @@ -199,7 +199,12 @@ local subcommand_tbl = { end if command == "wrap_cursor" then - ts_cmd.ts_wrap_at_cursor(args[#args]) + -- Account for if the last argument is the command or an actual arg + local wrap_arg = args[#args] + if wrap_arg == "wrap_cursor" then + wrap_arg = "" + end + ts_cmd.ts_wrap_at_cursor(wrap_arg) return end end, From d120010d69088d2d2f6cd97037833062347a59b4 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 12:28:25 -0600 Subject: [PATCH 2/9] chore: adjust Python command description --- lua/python/commands.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/python/commands.lua b/lua/python/commands.lua index 4fae837..2e59e89 100644 --- a/lua/python/commands.lua +++ b/lua/python/commands.lua @@ -294,7 +294,7 @@ function PythonCommands.load_commands() -- NOTE: the options will vary, based on your use case. vim.api.nvim_create_user_command("Python", python_cmd, { nargs = "+", - desc = "My awesome command with subcommand completions", + desc = "Python.nvim commands", complete = function(arg_lead, cmdline, _) -- Get the subcommand. local subcmd_key, subcmd_arg_lead = cmdline:match("^['<,'>]*Python[!]*%s(%S+)%s(.*)$") From 53297a2fae807ecc5bd99758cfb4f005c6f642c4 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 14:02:58 -0600 Subject: [PATCH 3/9] fix: support range for `:Python` for treesitter wrap_cursor --- lua/python/commands.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/python/commands.lua b/lua/python/commands.lua index 2e59e89..63aa580 100644 --- a/lua/python/commands.lua +++ b/lua/python/commands.lua @@ -315,6 +315,7 @@ function PythonCommands.load_commands() end end, bang = true, -- If you want to support ! modifiers + range = true, -- Support some visual command }) end From c7fc80a9ab3317849684e530771b6140712e5b06 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 14:15:22 -0600 Subject: [PATCH 4/9] doc: update vim doc --- doc/python.txt | 12 ++++++------ doc/tags | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/python.txt b/doc/python.txt index 15c4545..b3f46c1 100644 --- a/doc/python.txt +++ b/doc/python.txt @@ -295,8 +295,8 @@ Parameters ~ Class ~ {PythonStateVEnv} Fields ~ -{python_interpreter} `(string | nil)` -{venv_path} `(string | nil)` +{python_interpreter} `(string)` +{venv_path} `(string)` {install_method} `(string)` {install_file} `(string)` {source} `(string)` @@ -320,14 +320,14 @@ Fields ~ {dap} `(table)` ------------------------------------------------------------------------------ - *PythonState.State()* - `PythonState.State`() + *PythonStateM.State()* + `PythonStateM.State`() Return ~ `(PythonState)` ------------------------------------------------------------------------------ - *PythonState.save()* - `PythonState.save`({new_state}) + *PythonStateM.save()* + `PythonStateM.save`({new_state}) Parameters ~ {new_state} `(PythonState)` diff --git a/doc/tags b/doc/tags index 1c8c77a..199f643 100644 --- a/doc/tags +++ b/doc/tags @@ -8,9 +8,9 @@ PythonConfig.setup() python.txt /*PythonConfig.setup()* PythonDap.prepare_debugpy() python.txt /*PythonDap.prepare_debugpy()* PythonLSPCommands.pyright_change_type_checking() python.txt /*PythonLSPCommands.pyright_change_type_checking()* PythonState python.txt /*PythonState* -PythonState.State() python.txt /*PythonState.State()* -PythonState.save() python.txt /*PythonState.save()* PythonStateDap python.txt /*PythonStateDap* +PythonStateM.State() python.txt /*PythonStateM.State()* +PythonStateM.save() python.txt /*PythonStateM.save()* PythonStateVEnv python.txt /*PythonStateVEnv* PythonTreeSitterCommands.ts_wrap_at_cursor() python.txt /*PythonTreeSitterCommands.ts_wrap_at_cursor()* PythonUI.activate_system_call_ui() python.txt /*PythonUI.activate_system_call_ui()* From 259f83be32b440830105ab34f7729f5fd6ee62ee Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 17:11:58 -0600 Subject: [PATCH 5/9] test(treesitter): add test for treesitter wrap cursor commands --- scripts/minimal_init.lua | 57 +++++++++++++++++++++---------------- tests/test_treesitter.lua | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 tests/test_treesitter.lua diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index 7045eeb..08efbc0 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -1,28 +1,37 @@ -- Add current directory to 'runtimepath' to be able to use 'lua' files vim.cmd([[let &rtp.=','.getcwd()]]) --- Set up 'mini.test' only when calling headless Neovim (like with `make test`) -if #vim.api.nvim_list_uis() == 0 then - -- Add 'mini.nvim' to 'runtimepath' to be able to use 'mini.test' - -- Assumed that 'mini.nvim' is stored in 'deps/mini.nvim' - -- - local runtime_dependencies = { - "deps/mini.nvim", - "deps/nvim-treesitter", - "deps/neotest", - "deps/neotest-python", - "deps/nvim-dap", - "deps/nvim-dap-python", - "deps/nvim-lspconfig", - "deps/LuaSnip", - } - local runtime_path = vim.fn.join(runtime_dependencies, ",") - vim.cmd("set rtp+=" .. runtime_path) +local runtime_dependencies = { + "deps/mini.nvim", + "deps/nvim-treesitter", + "deps/neotest", + "deps/neotest-python", + "deps/nvim-dap", + "deps/nvim-dap-python", + "deps/nvim-lspconfig", + "deps/LuaSnip", +} +local runtime_path = vim.fn.join(runtime_dependencies, ",") +vim.cmd("set rtp+=" .. runtime_path) - -- Set up 'mini.test' - require("luasnip.extras.fmt") - require("luasnip.nodes.absolute_indexer") - require("nvim-treesitter.locals") - require("mini.test").setup() - require("mini.doc").setup() -end +-- Set up 'mini.test' +require("luasnip.extras.fmt") +require("luasnip.nodes.absolute_indexer") +require("nvim-treesitter.locals") +require("nvim-treesitter").setup() +require("mini.test").setup() +require("mini.doc").setup() +require("nvim-treesitter.configs").setup({ + modules = { + "highlight", + }, + sync_install = false, + auto_install = true, + ignore_install = {}, + ensure_installed = { + "python" + }, + highlight = { + enable = true + } +}) diff --git a/tests/test_treesitter.lua b/tests/test_treesitter.lua new file mode 100644 index 0000000..831550b --- /dev/null +++ b/tests/test_treesitter.lua @@ -0,0 +1,59 @@ +-- Define helper aliases +local new_set = MiniTest.new_set +local expect, eq = MiniTest.expect, MiniTest.expect.equality + +-- Create (but not start) child Neovim object +local child = MiniTest.new_child_neovim() + +-- Define main test set of this file +local T = new_set({ + -- Register hooks + hooks = { + -- This will be executed before every (even nested) case + pre_case = function() + -- Restart child process with custom 'init.lua' script + child.restart({ "-u", "scripts/minimal_init.lua" }) + -- Load tested plugin + child.lua([[require('python').setup()]]) + end, + -- This will be executed one after all tests from this set are finished + post_once = child.stop, + }, +}) + + +local get_lines = function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end + +T['wrap_cursor'] = MiniTest.new_set({ + hooks = { + pre_case = function() + child.cmd("e _not_existing_new_buffer.py") + child.type_keys("cc", [["TEST"]], "", "0") + end, + } +}) + +T['wrap_cursor']['normal'] = function() + child.cmd("Python treesitter wrap_cursor test(%s)") + eq(get_lines(), {[[test("TEST")]]}) +end + +T['wrap_cursor']['visual'] = function() + child.type_keys("l", "v", "$") + child.type_keys([[:Python treesitter wrap_cursor test(%s)]]) + eq(get_lines(), {[["test(TEST")]]}) +end + +T['wrap_cursor']['visual_with_selection'] = function() + child.type_keys("l", "v", "$") + child.type_keys([[:Python treesitter wrap_cursor1]]) + eq(get_lines(), {[["print(TEST")]]}) +end + +T['wrap_cursor']['with_selection'] = function() + child.type_keys([[:Python treesitter wrap_cursor1]]) + eq(get_lines(), {[[print("TEST")]]}) +end + +-- Return test set which will be collected and execute inside `MiniTest.run()` +return T From ee72d7a9d482c8a99fb757ecb9f6d2e05bd5a32f Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 17:13:34 -0600 Subject: [PATCH 6/9] chore: run format --- scripts/minimal_init.lua | 6 +++--- tests/test_treesitter.lua | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index 08efbc0..0668bd5 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -29,9 +29,9 @@ require("nvim-treesitter.configs").setup({ auto_install = true, ignore_install = {}, ensure_installed = { - "python" + "python", }, highlight = { - enable = true - } + enable = true, + }, }) diff --git a/tests/test_treesitter.lua b/tests/test_treesitter.lua index 831550b..3ec8333 100644 --- a/tests/test_treesitter.lua +++ b/tests/test_treesitter.lua @@ -21,38 +21,39 @@ local T = new_set({ }, }) +local get_lines = function() + return child.api.nvim_buf_get_lines(0, 0, -1, true) +end -local get_lines = function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end - -T['wrap_cursor'] = MiniTest.new_set({ +T["wrap_cursor"] = MiniTest.new_set({ hooks = { pre_case = function() child.cmd("e _not_existing_new_buffer.py") child.type_keys("cc", [["TEST"]], "", "0") end, - } + }, }) -T['wrap_cursor']['normal'] = function() +T["wrap_cursor"]["normal"] = function() child.cmd("Python treesitter wrap_cursor test(%s)") - eq(get_lines(), {[[test("TEST")]]}) + eq(get_lines(), { [[test("TEST")]] }) end -T['wrap_cursor']['visual'] = function() +T["wrap_cursor"]["visual"] = function() child.type_keys("l", "v", "$") child.type_keys([[:Python treesitter wrap_cursor test(%s)]]) - eq(get_lines(), {[["test(TEST")]]}) + eq(get_lines(), { [["test(TEST")]] }) end -T['wrap_cursor']['visual_with_selection'] = function() +T["wrap_cursor"]["visual_with_selection"] = function() child.type_keys("l", "v", "$") child.type_keys([[:Python treesitter wrap_cursor1]]) - eq(get_lines(), {[["print(TEST")]]}) + eq(get_lines(), { [["print(TEST")]] }) end -T['wrap_cursor']['with_selection'] = function() +T["wrap_cursor"]["with_selection"] = function() child.type_keys([[:Python treesitter wrap_cursor1]]) - eq(get_lines(), {[[print("TEST")]]}) + eq(get_lines(), { [[print("TEST")]] }) end -- Return test set which will be collected and execute inside `MiniTest.run()` From 462695f44587af27b5f1bf4c3ff35e9bba8f9e62 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 17:25:58 -0600 Subject: [PATCH 7/9] test: text action and enumerate ts test --- tests/test_text_actions.lua | 44 +++++++++++++++++++++++++++++++++++++ tests/test_treesitter.lua | 7 ++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/test_text_actions.lua diff --git a/tests/test_text_actions.lua b/tests/test_text_actions.lua new file mode 100644 index 0000000..101fc14 --- /dev/null +++ b/tests/test_text_actions.lua @@ -0,0 +1,44 @@ +-- Define helper aliases +local new_set = MiniTest.new_set +local expect, eq = MiniTest.expect, MiniTest.expect.equality + +-- Create (but not start) child Neovim object +local child = MiniTest.new_child_neovim() + +-- Define main test set of this file +local T = new_set({ + -- Register hooks + hooks = { + -- This will be executed before every (even nested) case + pre_case = function() + -- Restart child process with custom 'init.lua' script + child.restart({ "-u", "scripts/minimal_init.lua" }) + -- Load tested plugin + child.lua([[require('python').setup()]]) + end, + -- This will be executed one after all tests from this set are finished + post_once = child.stop, + }, +}) + +local get_lines = function() + return child.api.nvim_buf_get_lines(0, 0, -1, true) +end + + +T["text_actions"] = MiniTest.new_set({ + hooks = { + pre_case = function() + child.cmd("e _not_existing_new_buffer.py") + end, + }, +}) + +T['text_actions']['insert_f_string'] = function() + child.type_keys("i", [[print("{foo}")]], "") + + eq(get_lines(), { [[print(f"{foo}")]] }) +end + +-- Return test set which will be collected and execute inside `MiniTest.run()` +return T diff --git a/tests/test_treesitter.lua b/tests/test_treesitter.lua index 3ec8333..bc456f9 100644 --- a/tests/test_treesitter.lua +++ b/tests/test_treesitter.lua @@ -56,5 +56,12 @@ T["wrap_cursor"]["with_selection"] = function() eq(get_lines(), { [[print("TEST")]] }) end +T["enumerate"] = function() + child.cmd("e _not_existing_new_buffer.py") + child.type_keys("cc", "for i in [1, 2, 3]:", "", "0") + child.type_keys([[:Python treesitter toggle_enumerate]]) + eq(get_lines(), { "for idx, i in enumerate([1, 2, 3]):" }) +end + -- Return test set which will be collected and execute inside `MiniTest.run()` return T From 053308d8a8b88868ff2cb8b387e93e6d6d4df70e Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 17:26:22 -0600 Subject: [PATCH 8/9] chore: lint fix test_text_actions --- tests/test_text_actions.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_text_actions.lua b/tests/test_text_actions.lua index 101fc14..ae125ad 100644 --- a/tests/test_text_actions.lua +++ b/tests/test_text_actions.lua @@ -25,7 +25,6 @@ local get_lines = function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end - T["text_actions"] = MiniTest.new_set({ hooks = { pre_case = function() @@ -34,7 +33,7 @@ T["text_actions"] = MiniTest.new_set({ }, }) -T['text_actions']['insert_f_string'] = function() +T["text_actions"]["insert_f_string"] = function() child.type_keys("i", [[print("{foo}")]], "") eq(get_lines(), { [[print(f"{foo}")]] }) From 7ac4d81a1f655c47242fadb86c32b4abd0760fb0 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Sat, 30 Aug 2025 17:36:41 -0600 Subject: [PATCH 9/9] test: check for treesitter grammar before running tests --- scripts/minimal_init.lua | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index 0668bd5..9f7de55 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -21,17 +21,47 @@ require("nvim-treesitter.locals") require("nvim-treesitter").setup() require("mini.test").setup() require("mini.doc").setup() -require("nvim-treesitter.configs").setup({ +local ts_configs = require("nvim-treesitter.configs").setup({ modules = { "highlight", }, sync_install = false, auto_install = true, ignore_install = {}, - ensure_installed = { - "python", - }, + ensure_installed = {}, highlight = { enable = true, }, }) + +-- Clean path for use in a prefix comparison +---@param input string +---@return string +local function clean_path(input) + local pth = vim.fn.fnamemodify(input, ":p") + if vim.fn.has("win32") == 1 then + pth = pth:gsub("/", "\\") + end + return pth +end + +local function ts_is_installed(lang) + local matched_parsers = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true) or {} + local configs = require("nvim-treesitter.configs") + local install_dir = configs.get_parser_install_dir() + if not install_dir then + return false + end + install_dir = clean_path(install_dir) + for _, path in ipairs(matched_parsers) do + local abspath = clean_path(path) + if vim.startswith(abspath, install_dir) then + return true + end + end + return false +end + +if not ts_is_installed("python") then + vim.cmd("TSInstallSync python") +end