SkillAgentSearch skills...

Treesj

Neovim plugin for splitting/joining blocks of code

Install / Use

/learn @Wansmer/Treesj
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

TreeSJ

Neovim plugin for splitting/joining blocks of code like arrays, hashes, statements, objects, dictionaries, etc.

Written in Lua, using Tree-Sitter.

Inspired by and partly repeats the functionality of splitjoin.vim.

<!-- panvimdoc-ignore-start -->

https://github.com/Wansmer/treesj/assets/46977173/4277455b-81fd-4e99-9af7-43c77dbf542b

<!--toc:start--> <!--toc:end--> <!-- panvimdoc-ignore-end -->

Features

  • Can be called from anywhere in the block: No need to move cursor to specified place to split/join block of code;
  • Make cursor sticky: The cursor follows the text on which it was called;
  • Autodetect mode: Toggle-mode present. Split or join blocks by same key mapping;
  • Do it recursively: Expand or collapse all nested nodes? Yes, you can;
  • Recognize nested languages: Filetype doesn't matter, detect language with treesitter;
  • Repeat formatting with dot: . support for each action.
  • Smart: Different behavior depending on the context.

Requirements

Installation

With lazy.nvim:

return {
  'Wansmer/treesj',
  keys = { '<space>m', '<space>j', '<space>s' },
  dependencies = { 'nvim-treesitter/nvim-treesitter' }, -- if you install parsers with `nvim-treesitter`
  config = function()
    require('treesj').setup({--[[ your config ]]})
  end,
}

With packer.nvim:

use({
  'Wansmer/treesj',
  requires = { 'nvim-treesitter/nvim-treesitter' }, -- if you install parsers with `nvim-treesitter`
  config = function()
    require('treesj').setup({--[[ your config ]]})
  end,
})

Settings

Default configuration:

local tsj = require('treesj')

local langs = {--[[ configuration for languages ]]}

tsj.setup({
  ---@type boolean Use default keymaps (<space>m - toggle, <space>j - join, <space>s - split)
  use_default_keymaps = true,
  ---@type boolean Node with syntax error will not be formatted
  check_syntax_error = true,
  ---If line after join will be longer than max value,
  ---@type number If line after join will be longer than max value, node will not be formatted
  max_join_length = 120,
  ---Cursor behavior:
  ---hold - cursor follows the node/place on which it was called
  ---start - cursor jumps to the first symbol of the node being formatted
  ---end - cursor jumps to the last symbol of the node being formatted
  ---@type 'hold'|'start'|'end'
  cursor_behavior = 'hold',
  ---@type boolean Notify about possible problems or not
  notify = true,
  ---@type boolean Use `dot` for repeat action
  dot_repeat = true,
  ---@type nil|function Callback for treesj error handler. func (err_text, level, ...other_text)
  on_error = nil,
  ---@type table Presets for languages
  -- langs = {}, -- See the default presets in lua/treesj/langs
})

Commands

TreeSJ provide user commands:

  • :TSJToggle - toggle node under cursor (split if one-line and join if multiline);
  • :TSJSplit - split node under cursor;
  • :TSJJoin - join node under cursor;

Similar with lua:

:lua require('treesj').toggle()
:lua require('treesj').split()
:lua require('treesj').join()

In the lua version, you can optionally pass a preset that will overwrite the default preset values. It should contain split or join keys. Key both will be ignored.

E.g.:

-- For default preset
vim.keymap.set('n', '<leader>m', require('treesj').toggle)
-- For extending default preset with `recursive = true`
vim.keymap.set('n', '<leader>M', function()
    require('treesj').toggle({ split = { recursive = true } })
end)

How plugin works

When you run the plugin, TreeSJ detects the node under the cursor, recognizes the language, and looks for it in the presets. If the current node is not configured, TreeSJ checks the parent node, and so on, until a configured node is found.

Presets for node can be two types:

  • With preset for self - if this type is found, the node will be formatted;
  • With referens for nested nodes or fields - in this case, search will be continued among this node descendants;

Example:

"|" - meaning cursor

// with preset for self
const arr = [ 1, |2, 3 ];
                 |
    first node is 'number' - not configured,
    parent node is 'array' - configured and will be split

// with referens
cons|t arr = [ 1, 2, 3 ];
    |
  first node is 'variable_declarator' - not configured,
  parent node is 'lexical_declaration' - configured and has reference
  { target_nodes = { 'array', 'object' } },
  first configured nested node is 'array' and array will be splitted

Configuration

Languages

By default, TreeSJ has presets for these languages:

  • Javascript;
  • Typescript;
  • Tsx;
  • Jsx;
  • Lua;
  • CSS;
  • SCSS;
  • HTML;
  • Pug;
  • Vue;
  • Svelte;
  • JSON;
  • JSONC;
  • JSON5;
  • Toml;
  • Yaml;
  • Perl;
  • PHP (both php and php_only);
  • Ruby;
  • Python;
  • Starlark;
  • Go;
  • Java;
  • Rust;
  • R;
  • C/C++;
  • Nix;
  • Kotlin;
  • Bash;
  • Zsh;
  • SQL;
  • Dart;
  • Elixir;
  • Haskell;
  • Zig;
  • Julia;
  • Terraform;
  • Typst;

For adding your favorite language, add it to langs sections in your configuration. Also, see how to implement fallback to splitjoin.vim.

It is also possible to configure fallback for any node (see Advanced node).

To find out what nodes are called in your language, analyze your code with nvim-treesitter/playground or look in the source code of the parsers.

Example:

local langs = {
  javascript = {
    array = {--[[ preset ]]},
    object = {--[[ preset ]]}
    ['function'] = { target_nodes = {--[[ targets ]]}}
  },
}

If you have completely configured your language, and it works as well as you expected, feel free to open PR and share it. (Please, read manual before PR)

Basic node

Default preset for node:

local node_type = {
  -- `both` will be merged with both presets from `split` and `join` modes tables.
  -- If you need different values for different modes, they can be overridden
  -- in mode tables unless otherwise noted.
  both = {
    ---If a node contains descendants with a type from the list, it will not be formatted
    ---@type string[]
    no_format_with = { 'comment' },
    ---Separator for arrays, objects, hash e.c.t. (usually ',')
    ---@type string
    separator = '',
    ---Set last separator or not
    ---@type boolean
    last_separator = false,
    ---If true, empty brackets, empty tags, or node which only contains nodes from 'omit' no will handling
    ---@type boolean
    format_empty_node = true,
    ---All nested configured nodes will process according to their presets
    ---@type boolean
    recursive = true,
    ---Type of configured node that must be ignored
    ---@type string[]
    recursive_ignore = {},

    --[[ Working with the options below is explained in detail in `advanced node configuration` section. ]]
    ---Set `false` if node should't be splitted or joined.
    ---@type boolean|function For function: function(tsnode: TSNode): boolean
    enable = true,
    ---@type function|nil function(tsj: TreeSJ): void
    format_tree = nil,
    ---@type function|nil function(lines: string[], tsn?: TSNode): string[]
    format_resulted_lines = nil,
    ---Passes control to an external script and terminates treesj execution.
    ---@type function|nil function(node: TSNode): void
    fallback = nil,

    --[[ The options below should be the same for both modes. ]]
    ---The text of the node will be merged with the previous one, without wrapping to a new line
    ---@type table List-like table with types 'string' (type of node) or 'function' (function(child: TreeSJ): boolean).
    omit = {},
    ---Non-bracket nodes (e.g., with 'then|()' ... 'end' instead of { ... }|< ... >|[ ... ])
    ---If value is table, should be contains follow keys: { left = 'text', right = 'text' }. Empty string uses by default
    ---@type boolean|table
    non_bracket_node = false,
    ---If you need to process only nodes in the range from / to.
    ---If `shrink_node` is present, `non_bracket_node` will be ignored
    ---Learn more in advanced node configuration
    ---@type table|nil
    shrink_node = nil,
    -- shrink_node = { from = string, to = string },
  },
  -- Use only for join. If contains field from 'both',
  -- field here have higher priority
  join = {
    ---Adding space in framing brackets or last/end element
    ---@type boolean
    space_in_brackets = false,
    ---Insert space between nodes or not
    ---@type boolean
    space_separator = true,
    ---Adds instruction separator like ';' in statement block.
    ---It's not the same as `separator`: `separator` is a separate node, `force_insert` is a last symbol of code instruction.
    ---@type string
    force_insert = '',
    ---The `force_insert` symbol will be omitted if the type of node contains in this list
    -- (e.g. function_declaration inside statement_block in JS no require instruction separator (';'))
    ---@type table List-like table with types 'string' (type of node) or 'functio
View on GitHub
GitHub Stars1.3k
CategoryDevelopment
Updated1d ago
Forks44

Languages

Lua

Security Score

100/100

Audited on Mar 31, 2026

No findings