Treesj
Neovim plugin for splitting/joining blocks of code
Install / Use
/learn @Wansmer/TreesjREADME
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
- Neovim 0.9+
- nvim-treesitter (optional: if you install parsers with
nvim-treesitter)
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
phpandphp_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
