From c256211674f7a1f79bca01be0fd2f23d0269d2c6 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Mon, 8 Sep 2025 09:05:31 -0600 Subject: [PATCH 1/3] fix(f-strings): do not inject string on regex or `.format()` string Try to find if either the string is "regex" or is a "format" type string and ignore. --- lua/python/treesitter/commands.lua | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lua/python/treesitter/commands.lua b/lua/python/treesitter/commands.lua index 7afcc32..4df4f73 100644 --- a/lua/python/treesitter/commands.lua +++ b/lua/python/treesitter/commands.lua @@ -209,6 +209,26 @@ function PythonTreeSitterCommands.ts_wrap_at_cursor(subtitute_option) end) end +--- +---@param node TSNode the current ts node we are checking for parents +---@return string callText check if this node has a "call" type node 3 parents up +--- this is used for checking on "".format() calls for strings. +local function checkForFStringCallParent(node) + local callStatus, callText = pcall(function() + local callNode = node:parent():parent():parent() + if callNode then + local text = getNodeText(callNode) + return text + end + return "" + end) -- Get potential function call on string for .format() + + if not callStatus then + callText = "" + end + return callText +end + function PythonTreeSitterCommands.pythonFStr() local maxCharacters = 200 -- safeguard to prevent converting invalid code local node = getNodeAtCursor() @@ -217,6 +237,8 @@ function PythonTreeSitterCommands.pythonFStr() end local strNode + local callText = checkForFStringCallParent(node) + if node:type() == "string" then strNode = node elseif node:type():find("^string_") then @@ -239,10 +261,12 @@ function PythonTreeSitterCommands.pythonFStr() return end -- safeguard on converting invalid code - local isFString = text:find("^r?f") -- rf -> raw-formatted-string + local isFormatString = callText:find([[^.*["']%.format%(]]) + local isRString = text:find("^r") + local isFString = text:find("^r?f") -- rf -> raw-formatted-string local hasBraces = text:find("{.-[^%d,%s].-}") -- nonRegex-braces, see #12 and #15 - if not isFString and hasBraces then + if (not isFString and not isFormatString and not isRString) and hasBraces then text = "f" .. text replaceNodeText(strNode, text) elseif isFString and not hasBraces then From 457093c6433ddef3688a659a95cf02425212c468 Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Mon, 8 Sep 2025 09:19:07 -0600 Subject: [PATCH 2/3] test(f-string): add tests for inserts and not insterting fstrings --- lua/python/treesitter/commands.lua | 4 ++-- tests/test_treesitter.lua | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lua/python/treesitter/commands.lua b/lua/python/treesitter/commands.lua index 4df4f73..e879bbb 100644 --- a/lua/python/treesitter/commands.lua +++ b/lua/python/treesitter/commands.lua @@ -209,7 +209,7 @@ function PythonTreeSitterCommands.ts_wrap_at_cursor(subtitute_option) end) end ---- +--- ---@param node TSNode the current ts node we are checking for parents ---@return string callText check if this node has a "call" type node 3 parents up --- this is used for checking on "".format() calls for strings. @@ -263,7 +263,7 @@ function PythonTreeSitterCommands.pythonFStr() local isFormatString = callText:find([[^.*["']%.format%(]]) local isRString = text:find("^r") - local isFString = text:find("^r?f") -- rf -> raw-formatted-string + local isFString = text:find("^r?f") -- rf -> raw-formatted-string local hasBraces = text:find("{.-[^%d,%s].-}") -- nonRegex-braces, see #12 and #15 if (not isFString and not isFormatString and not isRString) and hasBraces then diff --git a/tests/test_treesitter.lua b/tests/test_treesitter.lua index bc456f9..e8135ec 100644 --- a/tests/test_treesitter.lua +++ b/tests/test_treesitter.lua @@ -63,5 +63,31 @@ T["enumerate"] = function() eq(get_lines(), { "for idx, i in enumerate([1, 2, 3]):" }) end +T["f-string"] = MiniTest.new_set({ + hooks = { + pre_case = function() + child.cmd("e _not_existing_new_buffer.py") + child.type_keys("cc", [["TEST"]], "", "0") + end, + }, +}) + +T["f-string"]["insert f string"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [["{foo}"]], "", "hh", "i", "") + eq(get_lines(), { [[f"{foo}"]] }) +end + +T["f-string"]["skip on r"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [[r"{foo}"]], "", "hh", "i", "") + eq(get_lines(), { [[r"{foo}"]] }) +end + +T["f-string"]["skip on format"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [["{foo}".format()]], "", "0lll", "i", "") + eq(get_lines(), { [["{foo}".format()]] }) +end -- Return test set which will be collected and execute inside `MiniTest.run()` return T From dce0476001c1e627266bdfc534330a5be08faebd Mon Sep 17 00:00:00 2001 From: Joshua Cold Date: Mon, 8 Sep 2025 09:25:43 -0600 Subject: [PATCH 3/3] test: remove duplicate and move text actions from ts tests to own file --- tests/test_text_actions.lua | 22 +++++++++++++++++----- tests/test_treesitter.lua | 26 -------------------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/tests/test_text_actions.lua b/tests/test_text_actions.lua index eb4604e..7cf481b 100644 --- a/tests/test_text_actions.lua +++ b/tests/test_text_actions.lua @@ -25,19 +25,31 @@ local get_lines = function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end -T["text_actions"] = MiniTest.new_set({ - n_retry = 3, +T["f-string"] = MiniTest.new_set({ hooks = { pre_case = function() child.cmd("e _not_existing_new_buffer.py") + child.type_keys("cc", [["TEST"]], "", "0") end, }, }) -T["text_actions"]["insert_f_string"] = function() - child.type_keys("i", [[print("{foo}")]], "") +T["f-string"]["insert f string"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [["{foo}"]], "", "hh", "i", "") + eq(get_lines(), { [[f"{foo}"]] }) +end + +T["f-string"]["skip on r"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [[r"{foo}"]], "", "hh", "i", "") + eq(get_lines(), { [[r"{foo}"]] }) +end - eq(get_lines(), { [[print(f"{foo}")]] }) +T["f-string"]["skip on format"] = function() + child.cmd("e! _not_existing_new_buffer.py") + child.type_keys("cc", [["{foo}".format()]], "", "0lll", "i", "") + eq(get_lines(), { [["{foo}".format()]] }) end -- Return test set which will be collected and execute inside `MiniTest.run()` diff --git a/tests/test_treesitter.lua b/tests/test_treesitter.lua index e8135ec..bc456f9 100644 --- a/tests/test_treesitter.lua +++ b/tests/test_treesitter.lua @@ -63,31 +63,5 @@ T["enumerate"] = function() eq(get_lines(), { "for idx, i in enumerate([1, 2, 3]):" }) end -T["f-string"] = MiniTest.new_set({ - hooks = { - pre_case = function() - child.cmd("e _not_existing_new_buffer.py") - child.type_keys("cc", [["TEST"]], "", "0") - end, - }, -}) - -T["f-string"]["insert f string"] = function() - child.cmd("e! _not_existing_new_buffer.py") - child.type_keys("cc", [["{foo}"]], "", "hh", "i", "") - eq(get_lines(), { [[f"{foo}"]] }) -end - -T["f-string"]["skip on r"] = function() - child.cmd("e! _not_existing_new_buffer.py") - child.type_keys("cc", [[r"{foo}"]], "", "hh", "i", "") - eq(get_lines(), { [[r"{foo}"]] }) -end - -T["f-string"]["skip on format"] = function() - child.cmd("e! _not_existing_new_buffer.py") - child.type_keys("cc", [["{foo}".format()]], "", "0lll", "i", "") - eq(get_lines(), { [["{foo}".format()]] }) -end -- Return test set which will be collected and execute inside `MiniTest.run()` return T