github github
stars 348
issues 12
subscribers 7
forks 20



19 days ago

TS Node Action

A framework for running functions on Tree-sitter nodes, and updating the buffer with the result.

cycle case


condition formatting


operator flipflop

split join blocks



     dependencies = { 'nvim-treesitter' },
     opts = {},


     requires = { 'nvim-treesitter' },
     config = function()

[!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:

    { "n" },
    { 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 table
  • node_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.

Multiple Actions for a Node Type

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" },
} will use the value of name to when prompting you on which action to perform.

If you want to bypass 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,

Writing your own Node Actions

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>



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.


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

null-ls Integration

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")
  sources = {

This will present the available node action(s) for the node under your cursor alongside your lsp/null-ls code actions.


@node: tsnode
@return: string

Returns the text of the specified 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.

Builtin Actions

@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 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
if/else <-> ternary
if block/postfix


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!