TreeSJ

repository·main·Indexed 23 days ago

https://github.com/wansmer/treesj

A Neovim plugin that uses Tree-Sitter to intelligently split or join blocks of code such as arrays, objects, and statements. It serves as a Lua-based alternative to splitjoin.vim, providing a Lua API and Vim commands (:TSJToggle, :TSJSplit, :TSJJoin) to manipulate code structures based on language-specific presets.

Tokens
6.6K
Snippets
18
Records
20
Agent score
30%

What's inside TreeSJ

  1. Configure Basic Node Presets

    main

    TreeSJ uses a hierarchical configuration for nodes. A node configuration is composed of three main tables: both, join, and split.

    • both: Contains settings applied to both split and join modes. This is the base configuration.
    • join: Contains settings used specifically when joining nodes (e.g., adding spaces in brackets or inserting separators like ;). These settings override both if keys overlap.
    • split: Contains settings used specifically when splitting nodes (e.g., indentation rules). These settings override both if keys overlap.

    Key Configuration Options

    both (Base Settings)

    • no_format_with: A list of node types (e.g., {'comment'}) that, if present as descendants, prevent the node from being formatted.
    • separator: The string used to separate elements (e.g., ',').
    • last_separator: Boolean indicating if the last element should have a separator.
    • format_empty_node: Boolean; if true, handles empty brackets or tags.
    • recursive: Boolean; if true, nested configured nodes will also use their presets.
    • enable: Boolean or function function(tsnode: TSNode): boolean. If false, the node won't be split or joined.
    • format_tree: Function function(tsj: TreeSJ): void for custom tree formatting.
    • format_resulted_lines: Function function(lines: string[], tsn?: TSNode): string[] to transform resulting lines.
    • fallback: Function function(node: TSNode): void to pass control to an external script.
    • omit: A list of node types or functions to merge with the previous node without a new line.
    • non_bracket_node: Boolean or table { left = 'text', right = 'text' } for nodes that don't use brackets (like then ... end).
    • shrink_node: Table { from = string, to = string } to process only a specific range within a node.
    • disable: Boolean; if true, the node is completely removed from the language preset.
    • target_nodes: A list of node types. If not empty, TreeSJ redirects to the found child instead of using split/join settings.

    join (Join Mode Only)

    • space_in_brackets: Boolean; adds space in framing brackets.
    • space_separator: Boolean; inserts space between nodes.
    • force_insert: A string (e.g., ;) to act as an instruction separator.
    • no_insert_if: A list of node types where force_insert should be omitted.

    split (Split Mode Only)

    • recursive: Boolean; whether nested nodes process their presets.
    • last_indent: 'normal' (matches first line) or 'inner' (matches inner nodes).
    • inner_indent: 'normal' or 'inner'.
    local node_type = {
      both = {
        no_format_with = { 'comment' },
        separator = '',
        last_separator = false,
        format_empty_node = true,
        recursive = true,
        recursive_ignore = {},
        enable = true,
        format_tree = nil,
        format_resulted_lines = nil,
        fallback = nil,
        omit = {},
        non_bracket_node = false,
        shrink_node = nil,
      },
      join = {
        space_in_brackets = false,
        space_separator = true,
        force_insert = '',
        no_insert_if = {},
      },
      split = {
        recursive = false,
        last_indent = 'normal',
        inner_indent = 'inner',
      },
      disable = false,
      target_nodes = {},
    }
  2. How TreeSJ detects and processes nodes

    main

    TreeSJ works by detecting the node under the cursor and looking for a matching configuration in its presets. If the current node is not configured, it traverses up the tree to the parent node until a configured node is found.

    There are two types of presets:

    1. Preset for self: If the node itself is configured, it is formatted directly.
    2. Reference for nested nodes/fields: If the node is configured with target_nodes, TreeSJ will search through the descendants of the current node for the first configured target.

    Example of Reference behavior: If you are on a variable_declarator that isn't configured, but its parent lexical_declaration has { target_nodes = { 'array', 'object' } }, TreeSJ will look inside the declaration to find the first array or object to split.

  3. Understand the TreeSJ class and lifecycle

    main

    A TreeSJ instance is created once a node for formatting is found. Every child and descendant of that node is also an instance of the TreeSJ class. These instances allow you to manipulate formatting behavior dynamically.

    TreeSJ Lifecycle

    1. Checking the found Node: Validates for syntax errors, ensures no descendants are in preset[mode].no_format_with, and verifies preset[mode].enable is true.
    2. Creating root TreeSJ: An instance is created based on the validated node.
    3. Build tree: Iterates through children. If preset[mode].recursive is true, it builds sub-trees for configured children.
    4. Separator handling: Inserts or removes separators if specified in the preset.
    5. Run preset[mode].format_tree: Executes the custom function defined in the preset.
    6. Mode-based preparation: Handles spacing, indenting, and preset.join.force_insert to prepare the replacement string list.
    7. Run preset[mode].format_resulted_lines: Executes the custom function from the preset to finalize the lines.
  4. Install TreeSJ

    main

    TreeSJ is a Neovim plugin for splitting and joining blocks of code (arrays, hashes, etc.) using Tree-Sitter.

    Requirements

    • Neovim 0.9+
    • nvim-treesitter (optional, but recommended for parser support)

    Installation with lazy.nvim

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

    Installation with packer.nvim

    use({
      'Wansmer/treesj',
      requires = { 'nvim-treesitter/nvim-treesitter' },
      config = function()
        require('treesj').setup({--[[ your config ]])
      end,
    })
    return {
      'Wansmer/treesj',
      keys = { '<space>m', '<space>j', '<space>s' },
      dependencies = { 'nvim-treesitter/nvim-treesitter' },
      config = function()
        require('treesj').setup({--[[ your config ]])
      end,
    }
  5. Configure language presets

    main

    To add support for a language or customize how specific nodes are handled, add a langs table to your setup() call.

    You can define presets for specific node types (like array or object) or use target_nodes to create a reference that searches for specific descendants.

    Example Configuration:

    local langs = {
      javascript = {
        array = {--[[ preset ]]},
        object = {--[[ preset ]]},
        ['function'] = { target_nodes = {--[[ targets ]]}}
      },
    }
    
    require('treesj').setup({ langs = langs })

    To find the exact names of nodes in your language, use nvim-treesitter/playground to inspect the syntax tree.

    local langs = {
      javascript = {
        array = {--[[ preset ]]},
        object = {--[[ preset ]]},
        ['function'] = { target_nodes = {--[[ targets ]]}}
      },
    }
  6. Configure TreeSJ settings

    main

    Use require('treesj').setup() to configure the plugin behavior.

    Key options include:

    • use_default_keymaps: (boolean) Enables default mappings: <space>m (toggle), <space>j (join), <space>s (split).
    • check_syntax_error: (boolean) If true, nodes with syntax errors will not be formatted.
    • max_join_length: (number) If the line after joining exceeds this value, the node will not be formatted.
    • cursor_behavior: (string) Controls where the cursor goes after the action. Options: 'hold' (stays on current text), 'start' (jumps to first symbol), 'end' (jumps to last symbol).
    • notify: (boolean) Whether to notify about possible problems.
    • dot_repeat: (boolean) Enables support for the . command to repeat the last action.
    • on_error: (function) Callback for error handling: func(err_text, level, ...).
    • langs: (table) Language-specific presets.
    local tsj = require('treesj')
    
    local langs = {--[[ configuration for languages ]]}
    
    tsj.setup({
      use_default_keymaps = true,
      check_syntax_error = true,
      max_join_length = 120,
      cursor_behavior = 'hold',
      notify = true,
      dot_repeat = true,
      on_error = nil,
      -- langs = {}, 
    })
  7. Use Language Preset Utilities to Configure Nodes

    main

    TreeSJ provides utility functions to quickly set up common node types within a language configuration. Instead of defining every field manually, you can use these presets:

    • set_default_preset(override): The standard default.
    • set_preset_for_list(override): For list-like nodes (e.g., arrays).
    • set_preset_for_dict(override): For dictionary-like nodes (e.g., objects).
    • set_preset_for_statement(override): For statement-like nodes.
    • set_preset_for_args(override): For argument-like nodes.
    • set_preset_for_non_bracket(override): For nodes without brackets.

    Each function accepts an override table to customize the preset.

    Example: Configuring JavaScript and Lua

    local lang_utils = require('treesj.langs.utils')
    
    local langs = {
      javascript = {
        object = lang_utils.set_preset_for_dict(),
        array = lang_utils.set_preset_for_list(),
        formal_parameters = lang_utils.set_preset_for_args(),
        arguments = lang_utils.set_preset_for_args(),
        statement_block = lang_utils.set_preset_for_statement({
          join = {
            no_insert_if = {
              'function_declaration',
              'try_statement',
              'if_statement',
            },
          },
        }),
      },
      lua = {
        table_constructor = lang_utils.set_preset_for_dict(),
        arguments = lang_utils.set_preset_for_args(),
        parameters = lang_utils.set_preset_for_args(),
      },
    }
  8. Merge Language Presets

    main

    If two languages share a similar structure (for example, css and scss), you can use merge_preset to reuse an existing language's configuration and only override or add specific nodes. This prevents duplication and ensures consistency.

    Use treesj.langs.utils.merge_preset(base_preset, overrides) to achieve this.

    local tsj_utils = require('treesj.langs.utils')
    local css = require('treesj.langs.css')
    
    local langs = {
      scss = tsj_utils.merge_preset(css, {
        --[[ 
          Here you can override existing nodes
          or add language-specific nodes
        ]]
      })
    }
  9. Configure advanced node behavior with functions

    main

    While most nodes can be configured declaratively, you can use functions for certain options to modify values, text, or child order dynamically. These functions receive instances of TSNode, a TreeSJ instance, or an array of rows.

    enable option

    Accepts a boolean or a function. The function receives a TSNode and must return a boolean to determine if the node should be enabled for processing.

    -- Example: Disable 'import_spec' if it is already inside an 'import_spec_list'
    import_spec = {
      enable = function(tsn)
        return tsn:parent():type() ~= 'import_spec_list'
      end
    }

    fallback option

    Passes control to a third-party script or command. When used, TreeSJ finds the node but does not process it. The function receives the TSNode instance.

    -- Example: Pass control to Splitjoin.vim for Ruby classes
    class = {
      fallback = function(_)
        vim.cmd('SplitjoinSplit')
      end
    }
    import_spec = {
      enable = function(tsn)
        return tsn:parent():type() ~= 'import_spec_list'
      end
    }
  10. Redirect node searches with `target_nodes`

    main

    The target_nodes option allows you to redirect the search for a configured node deeper into the tree. It acts as a dictionary where:

    • Key: A node type or a field name (field names have highest priority).
    • Value: The name of another configured node whose preset should be used.
    -- Example: Redirecting a Rust 'match_arm' field 'value' to use a 'value' preset
    match_arm = {
      target_nodes = { 'value' },
    },
    value = lang_utils.set_preset_for_statement({
      -- ... configuration for the redirected node
    })
    match_arm = {
      target_nodes = { 'value' },
    }, 
    value = lang_utils.set_preset_for_statement({
      split = {
        format_tree = function(tsj)
          if tsj:type() ~= 'block' then
            tsj:wrap({ left = '{', right = '}' })
          end
        end,
      },
      -- ...
    })
  11. Update TreeSJ text and content

    main

    To change the content of a node, use update_text.

    Note on Recursive Mode: If recursive mode is active, update_text might require you to update the text of the children directly instead of the parent, as the parent's text is often glued from its children.

    ---@param new_text string|string[]
    function TreeSJ:update_text(new_text)
    end

    Example of conditional text updating in a format_tree function:

    {
      format_tree = function(tsj) 
        if tsj:type() ~= 'statement_block' then
          local body = tsj:child(2)
          if body:will_be_formatted() then
            local set_return
            if body:has_preset('split') then
              set_return = body:child(1)
            else
              set_return = body:child(1):child(1)
            end
            set_return:update_text('return ' .. set_return:text())
          else
            body:update_text('return ' .. body:text())
          end
        end
      end,
    }
    {
      format_tree = function(tsj)
        if tsj:type() ~= 'statement_block' then
          -- ...
          local body = tsj:child(2)
          if body:will_be_formatted() then
            local set_return
            if body:has_preset('split') then
              set_return = body:child(1)
            else
              set_return = body:child(1):child(1)
            end
            set_return:update_text('return ' .. set_return:text())
          else
            body:update_text('return ' .. body:text())
          end
          -- ...
        end
      end,
    }
  12. Manipulate tree structure with `format_tree`

    main

    The format_tree option accepts a function that receives a TreeSJ root node. This allows you to manually add, remove, or update elements within the tree during the split or join process.

    -- Example: Python import_from_statement (adding parentheses manually)
    import_from_statement = lang_utils.set_preset_for_args({
      both = {
        omit = { lang_utils.omit.if_second, 'import', ' (' },
      },
      split = {
        last_separator = true,
        format_tree = function(tsj)
          if not tsj:has_children({ '(', ')' }) then
            tsj:create_child({ text = ' (' }, 4)
            tsj:create_child({ text = ')' }, #tsj:children() + 1)
            local penult = tsj:child(-2)
            penult:update_text(penult:text() .. ',')
          end
        end,
      },
      join = {
        format_tree = function(tsj)
          tsj:remove_child({ '(', ')' })
        end,
      },
    })
    import_from_statement = lang_utils.set_preset_for_args({
      both = {
        omit = { lang_utils.omit.if_second, 'import', ' (' },
      },
      split = {
        last_separator = true,
        format_tree = function(tsj)
          if not tsj:has_children({ '(', ')' }) then
            tsj:create_child({ text = ' (' }, 4)
            tsj:create_child({ text = ')' }, #tsj:children() + 1)
            local penult = tsj:child(-2)
            penult:update_text(penult:text() .. ',')
          end
        end,
      },
      join = {
        format_tree = function(tsj)
          tsj:remove_child({ '(', ')' })
        end,
      },
    })