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 langs.
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 langs
["node_type"] = fn,
...
},
lang = {
["node_type"] = fn,
...
},
...
}
lang
should be the treesitter parser lang, or '*'
for the global tablenode_type
should be the value of vim.treesitter.get_node_at_cursor()
A definition on the lang
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.
If you want to bypass vim.ui.select
and instead just want all actions to be
applied without prompting, you can pass ask = false
as an argument in the
node_type
value. Using the same example as above, it would look like this:
["node_type"] = {
{ function_one, name = "Action One" },
{ function_two, name = "Action Two" },
ask = false,
}
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
, format
, and target
.
Here's how that can look.
{
cursor = { row = 0, col = 0 },
callback = function() ... end,
format = true,
target = <tsnode>
}
cursor
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
).
callback
If callback
is present, it will simply get called without arguments after the
buffer has been updated, and after the cursor has been positioned.
format
Boolean value. If true
, will run =
operator on new buffer text. Requires
indentexpr
to be set.
target
TSNode. If present, this node will be used as the target for replacement instead of the node under your cursor.
Here's a simplified example of how a node-action function gets called:
local action = node_actions[lang][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 langs
require("ts-node-action").available_actions()
Exposes the function assigned to
the node your cursor is currently on, as well as its name
Users can set up integration with
null-ls and use it to
display available node actions by registering the builtin ts_node_action
code
action source
local null_ls = require("null-ls")
null_ls.setup({
sources = {
null_ls.builtins.code_actions.ts_node_action,
...
}
})
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").node_is_multiline(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.
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
A Lua pattern (string) that matches the format
apply
A function that takes a table of standardized strings as it's argument, and returns a string in the format
standardize
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 snakecase _and camel_case. It's therefore important to place a format between those two so we can correctly change the string.
Builtin actions are all higher-order functions so they can easily have options
overridden on a per-lang basis. Check out the implementations under
lua/filetypes/
to see how!
(*) | Ruby | js/ts/tsx/jsx | Lua | Python | PHP | Rust | C# | JSON | HTML | YAML | R | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
toggle_boolean() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
cycle_case() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||
cycle_quotes() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||||
toggle_multiline() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||||
toggle_operator() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||||
toggle_int_readability() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||||
toggle_block() |
✅ | |||||||||||
if/else <-> ternary | ✅ | ✅ | ||||||||||
if block/postfix | ✅ | |||||||||||
toggle_hash_style() |
✅ | |||||||||||
conceal_string() |
✅ | ✅ |
To run the test suite, clone the repo and run ./run_spec
. It should pull all
dependencies into spec/support/
on first run, then execute the tests.
This is still a little WIP.
If you come up with something that would be a good fit, pull requests for node actions are welcome!