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.
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:
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-lang basis. Check out the implementations under lua/filetypes/
to see how!
(*) | Ruby | js/ts/tsx/jsx | Lua | Python | PHP | Rust | JSON | HTML | YAML | R | |
---|---|---|---|---|---|---|---|---|---|---|---|
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() |
✅ | ✅ |
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!