A framework for running functions on Tree-sitter nodes, and updating the buffer with the result.
Lazy.nvim
:
{
'ckolkey/ts-node-action',
dependencies = { 'nvim-treesitter' },
opts = {},
},
packer
:
use({
'ckolkey/ts-node-action',
requires = { 'nvim-treesitter' },
config = function()
require("ts-node-action").setup({})
end
})
Note: It's not required to call require("ts-node-action").setup()
to initialize the plugin,
but a table can be passed into the setup function to specify new actions for nodes or additional filetypes.
Bind require("ts-node-action").node_action
to something. This is left up to the user.
For example, this would bind the function to K
:
vim.keymap.set({ "n" }, "K", require("ts-node-action").node_action, { desc = "Trigger Node Action" })
If tpope/vim-repeat
is installed, calling node_action()
is dot-repeatable.
If setup()
is called, user commands :NodeAction
and :NodeActionDebug
are defined.
See available_actions()
below for how to set this up with LSP Code Actions.
The setup()
function accepts a table that conforms to the following schema:
{
['*'] = { -- Global table is checked for all filetypes
["node_type"] = fn,
...
},
filetype = {
["node_type"] = fn,
...
},
...
}
filetype
should be the value ofvim.o.filetype
, or'*'
for the global tablenode_type
should be the value ofvim.treesitter.get_node_at_cursor()
A definition on the filetype
table will take precedence over the *
(global) table.
To define multiple actions for a node type, structure your node_type
value as a table of tables, like so:
["node_type"] = {
{ function_one, name = "Action One" },
{ function_two, name = "Action Two" },
}
vim.ui.select
will use the value of name
to when prompting you on which action to perform.
All node actions should be a function that takes one argument: the tree-sitter node under the cursor.
You can read more about their API via :help tsnode
This function can return one or two values:
-
The first being the text to replace the node with. The replacement text can be either a
"string"
or{ "table", "of", "strings" }
. With a table of strings, each string will be on it's own line. -
The second (optional) returned value is a table of options. Supported keys are:
cursor
,callback
, andformat
Here's how that can look.
{
cursor = { row = 0, col = 0 },
callback = function() ... end,
format = true
}
If the cursor
key is present with an empty table value, the cursor will be moved to the start of the line where the
current node is (row = 0
col = 0
relative to node start_row
and start_col
).
If callback
is present, it will simply get called without arguments after the buffer has been updated, and after the
cursor has been positioned.
Boolean value. If true
, will run =
operator on new buffer text. Requires indentexpr
to be set.
Here's a simplified example of how a node-action function gets called:
local action = node_actions[vim.o.filetype][node:type()]
local replacement, opts = action(node)
replace_node(node, replacement, opts or {})
require("ts-node-action").node_action()
Main function for plugin. Should be assigned by user, and when called will attempt to run the assigned function for the
node your cursor is currently on.
require("ts-node-action").debug()
Prints some helpful information about the current node, as well as the loaded node actions for all filetypes
require("ts-node-action").available_actions()
Exposes the function assigned to the node your cursor is currently on, as well as its name. This is mainly designed for null-ls
integration, which might look something like this:
require "null-ls".register({
name = "more_actions",
method = { require "null-ls".methods.CODE_ACTION },
filetypes = { "_all" },
generator = {
fn = require("ts-node-action").available_actions
}
})
This will present the available node action(s) for the node under your cursor alongside your lsp
/null-ls
code actions.
require("ts-node-action.helpers").node_text(node)
@node: tsnode
@return: string
Returns the text of the specified node.
require("ts-node-action.helpers").multiline_node(node)
@node: tsnode
@return: boolean
Returns true if node spans multiple lines, and false if it's a single line.
require("ts-node-action.helpers").padded_node_text(node, padding)
@node: tsnode
@padding: table
@return: string
For formatting unnamed tsnodes. For example, if you pass in an unnamed node representing the text ,
, you could pass in
a padding
table (below) to add a trailing whitespace to ,
nodes.
{ [","] = "%s " }
Nodes not specified in table are returned unchanged.
Cycle Case
require("ts-node-action.actions").cycle_case(formats)
@param formats table|nil
formats
param can be a table of strings specifying the different formats to cycle through. By default it's { "snake_case", "pascal_case", "screaming_snake_case", "camel_case" }
.
A table can also be used in place of a string to implement a custom formatter. Every format is a table that implements the following interface:
- pattern (string)
- apply (function)
- standardize (function)
A Lua pattern (string) that matches the format
A function that takes a table of standardized strings as it's argument, and returns a string in the format
A function that takes a string in this format, and returns a table of strings, all lower case, no special chars. ie:
standardize("ts_node_action") -> { "ts", "node", "action" }
standardize("tsNodeAction") -> { "ts", "node", "action" }
standardize("TsNodeAction") -> { "ts", "node", "action" }
standardize("TS_NODE_ACTION") -> { "ts", "node", "action" }
NOTE: The order of formats can be important, as some identifiers are the same for multiple formats. Take the string 'action' for example. This is a match for both snake_case and camel_case. It's therefore important to place a format between those two so we can correcly change the string.
Builtin actions are all higher-order functions so they can easily have options overridden on a per-filetype basis. Check out the implementations under lua/filetypes/
to see how!
(*) | Ruby | js/ts/tsx/jsx | Lua | Python | PHP | Rust | JSON | HTML | |
---|---|---|---|---|---|---|---|---|---|
toggle_boolean() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
cycle_case() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
cycle_quotes() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||
toggle_multiline() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
toggle_operator() |
✅ | ✅ | ✅ | ✅ | ✅ | ||||
toggle_int_readability() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||
toggle_block() |
✅ | ||||||||
if/else <-> ternery | ✅ | ||||||||
if block/postfix | ✅ | ||||||||
toggle_hash_style() |
✅ | ||||||||
conceal_string() |
✅ | ✅ |
If you come up with something that would be a good fit, pull requests for node actions are welcome!