kylechui / nvim-surround

Add/change/delete surrounding delimiter pairs with ease. Written with :heart: in Lua.
MIT License
3.18k stars 62 forks source link

delete surrounding function scope / leave function body #114

Closed k-times-c closed 2 years ago

k-times-c commented 2 years ago

Hello @kylechui. Thank you for the recreation of this plugin! The code is really neat, relatively easy to follow. It would be awesome if you could do simple refactoring with this plugin. Per the title, remove the surrounding scope of a function using treesitter.

e.g.

foo = function()
  print("bar") |
  --            ^
  -- cursor at end of line
end

pressing something like ds, f

print("bar")

or

vim.keymap.set("n", "<space><space>", function() return "hello world" end)
--                                                                 ^
--                                                       cursor here

after ds f

vim.keymap.set("n", "<space><space>", return "hello world" )

getting rid of return is a bit more intelligent (perhaps a little out of scope of the what I'm thinking) but would be a bonus

Not sure how one could do this, or whether this is even possible, I'd been open helping with this!

kylechui commented 2 years ago

See #101, #113, #60.

kylechui commented 2 years ago

@k-times-c I know this is a very requested feature, so I've been busy trying to get things to work out/to provide an API for users to define their own custom way to modify existing surrounds. Thanks for the comment on the code! I've been trying to keep it simple to follow, so other people can more easily look at the code and contribute/give guesses as to where their issues stem from. I intend on writing "developer docs" that give a brief overview of the structure/execution flow of the code. If you want to test out the latest stuff, branch pattern-matching has started the process, allowing for users to provide their own Lua pattern to find/delete/change existing surrounds. I plan on redoing parts of the API to make it accept functions, with Lua patterns being a subset of its functionality.

If you'd like to help out, I would appreciate you checking out #101, in particular weighing in on proposed user interfaces (since I would like to make config as simple/intuitive as possible), as well as just giving feedback on whatever goes on there. With respect to this exact feature, it is "possible" in a very basic form by using Lua patterns, albeit limited in accuracy when you're in nested functions.

TL;DR Tree-sitter/general function support is up next :)

kylechui commented 2 years ago

Update: @k-times-c If you do decide to use the new branch, this setup (say, in ftplugin/lua.lua) will probably work for most simple cases:

Edit: Only works on functions of the form local [func_name] = function() [contents] end. You can try editing the patterns in order to make it match other types of functions as well.

require("nvim-surround").buffer_setup({
    delimiters = {
        ["F"] = {
            add = function()
                local func_name = vim.fn.input("Enter the function name: ")
                if #func_name > 0 then
                    return { { "local " .. func_name .. " = function() " }, { " end" } }
                end
                return { { "function() " }, { " end" } }
            end,
            find = "local [%w_]+ = function%b().-end",
            delete = "^(local [%w_]+ = function%b())().-(end)()$",
            change = {
                target = "^local ([%w_]+)() = function%b().-()()$",
                replacement = function()
                    return { { vim.fn.input("Enter the function name: ") }, { "" } }
                end,
            },
        },
    },
})
kylechui commented 2 years ago

Oh yeah I think the general consensus so far is that f is going to be used for "function calls" of the form [function_name]([arguments]), e.g. dsf on print("Hello world") should yield "Hello world". Obviously you can override it if you so please, but that's how the default is going to be set for user configuration. In the sample configuration I provided above I was using the F key for regular functions.

kylechui commented 2 years ago

@k-times-c Update 2: You can now use Tree-sitter to avoid nested issues, but there's no "jumping" to the nearest one, unfortunately:

require("nvim-surround").buffer_setup({
    delimiters = {
        ["p"] = { -- Bonus surround I find quite useful, for adding debug print statements via `yssp`
            add = { "vim.pretty_print(", ")" },
            find = "vim%.pretty_print%b()",
            delete = "^(vim%.pretty_print%()().-(%))()$",
        },
        ["F"] = {
            add = { "function() ", " end" },
            find = function()
                local ts_utils = require("nvim-treesitter.ts_utils")

                local root = ts_utils.get_node_at_cursor()
                while root and root:type() ~= "function_definition" do
                    root = root:parent()
                end
                if root then
                    local first_lnum, first_col, last_lnum, last_col = root:range()
                    return {
                        first_pos = { first_lnum + 1, first_col + 1 },
                        last_pos = { last_lnum + 1, last_col },
                    }
                end
            end,
            delete = "^(function%b())().-(end)()$",
        },
    },
})
k-times-c commented 2 years ago

Thank you. This is awesome. I'll definitely test this out when I get the chance. Utilizing tree-sitter in the way you wrote it above was what I had in mind, but wasn't sure what you needed to return to accomplish that so thank you. Generally speaking, I think lua patterns are necessary. I tree-sitter is the future of neovim. Utilizing/interacting with it is what really what differentiates, and really gives a reason for rewriting tpope iconic, mature vim-surround plugin.

Did something change in the api implementation within #101, or was this always a possible?

there's no "jumping" to the nearest one

Just sharing my thoughts on this:

Assuming you're referring to "lookahead" behavior? (e.g: hit the lookahead behavior ci", or in terms of this plugin, or ysiq)?

My opinion that the "parenting" scope behavior you coded above is intuitive, default behavior.

Regarding the implementing of "lookahead"/"lookbehind" behavior in the future. You may already be acutely aware of, nvim-treesitter-textobjects has "lookahead," as well as "lookbehind" behavior.

Perhaps you/I could implement, or leverage the text-objects code within nvim-surround?

Just another though on "lookhead" behavior --> "explicit lookahead/lookbehind" would be awesome. I feel like that falls "out-of-scope" for this plugin, since it has

Also does this In the less likely case you are referring to the jump-list?

I think this is segue into another functionality that might be awesome to interoperate with other plugins like .

Bonus surround I find quite useful, for adding debug print statements via yssp

thanks for sharing this as well! So not sure if you're aware of the plugin/feature, but the plugin ThePrimeagen/refactoring.nvim has a debugging feature that provides this functionality to add (print statements)[https://github.com/ThePrimeagen/refactoring.nvim#debug-features]. The plugin is lacking operator pending maps, per this issue. I was looking into adding that functionality based on your operator-pending implementation. But an idea I had about this is having nvim-surround potentially delegate more complex refactoring task. e.g: when printin, you could leverage refactoring.nvim defaults for determining the right correct print function for a given language! or even use lsp to ensure that a function rename is consistent across the project..

Hopefully all that makes sense (i'll re-edit / add to this once I have time)

kylechui commented 2 years ago

Did something change in the api implementation within #101, or was this always possible?

Jumping to "nearest" surround has always been possible, but the problem is that if the user specifies the selections to be modified, nvim-surround "blindly" follows those instructions. In other words, the burden of specifying "jump behavior/lookahead/lookbehind" now rests on the user (this is not about the jumplist, I just use "jumping" as an umbrella term for lookahead/lookbehind; see the wiki page for more info). The built-in methods still support jumping (text-objects, aliasing, pattern-matching), but since we're overriding the defaults by using Tree-sitter, people would need to specify it on their own. I'm still working through refactoring/cleaning up code organization though.

For the record, ysiq isn't possible unless you have iq as a text-object defined by a different plugin.

Thanks for the tip on nvim-treesitter-textobjects! I'll need to think about whether I want to build Treesitter support into the plugin natively, instead of as a "second-class" way to customize (as you need to write the function yourself). I'll think about a way where you just input a Tree-sitter node type like function_call and it just finds the nearest one or something like that.

As for refactoring.nvim, I've certainly heard of it but as a student I feel I've never done any complex coding to warrant using such a plugin. If it exposes functions to help you determine print statement types, then that would certainly work! You could just use those functions inside nvim-surround's add key for delimiters. One seemingly similar case that I have right now is for C++ programming, where the inclusion of std:: is wanted if and only if the line using namespace std; does not appear in the file (this heuristic could definitely be improved by leveraging LSP):

-- This would be put in ftplugin/cpp.lua
-- It has a few issues that I'm not particularly happy with, e.g. it won't delete std::cout ... std::endl
-- Think of it more as a "proof of concept" for now
require("nvim-surround").buffer_setup({
    delimiters = {
        ["p"] = {
            add = function()
                for _, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do
                    if line == "using namespace std;" then
                        return { { "cout << " }, { " << endl;" } }
                    end
                end
                return { { "std::cout << " }, { " << std::endl;" } }
            end,
            find = "cout << .- << endl;",
            delete = "^(cout << )().-( << endl;)()$",
        },
    },
})

Renaming functions "globally" via LSP would definitely be way more complex of an undertaking, as right now the plugin just modifies text, with little to no understanding of how the underlying code structure works. If you want to open an issue regarding this, e.g. if you rename the function definition as opposed to a call, then that would try and utilize LSP/etc. for renaming several functions. This would definitely be far out though, as there's still a large portion of the current code that I'm personally dissatisfied with.

FWIW, I was inspired by TJ's LuaSnips video to create this sort of "dynamic" surround. My Tree-sitter/LSP skills are definitely lacking though haha