Navigate between sibling nodes in your code using Tree-sitter. Context-aware navigation that keeps you at the right level of abstraction.
General-demo.mov
- Context-aware navigation: Jumps between meaningful code units (statements, properties, array elements, etc.)
- Block-loop (separate keybinding): Cycle through a block's structural boundaries (start → branches → end → back to start)
- Visual mode block selection: Select entire blocks with block-loop in visual mode
- Multi-language support: Works with TypeScript, JavaScript, JSX, TSX, Lua, Java, C, C#, Python, and more
- Smart boundary detection: Prevents navigation from jumping out of context
- Method chain navigation: Seamlessly navigate through method chains like
obj.foo().bar().baz() - If-else chain navigation: Jump between if/else-if/else clauses
- JSX/TSX support: Navigate between JSX elements and attributes
- Count support: Use
3<C-j>to jump 3 siblings forward
Jump between nodes at the same nesting level. When your cursor is on a statement, property, or element, pressing the navigation key moves you to the next/previous sibling.
Supported contexts:
- Statements (variable declarations, if/for/while, return, etc.)
- Object properties and type properties
- Array elements
- Function parameters and arguments
- Import specifiers
- JSX elements and attributes
- Method chains
- If-else-if chains
- Generic type parameters
- Union type members
- And more!
A complementary feature with its own keybinding. When triggered, it cycles through a block's structural boundaries instead of jumping to siblings.
Screen.Recording.2026-01-04.at.3.00.00.mov
Supported constructs:
const/let/vardeclarations → cycles between keyword and closing}/)if/else if/elseblocks → cycles through all branches and closing}for/whileloops → cycles between keyword and closing}switchstatements → cycles throughswitch, eachcase/default, and closing}functiondeclarations → cycles between keyword and closing}type/interfacedeclarations → cycles between keyword and closing}- Method chains → cycles between each method in the chain
sibling-jump.nvim works with any language that has Tree-sitter support. The following languages have been tested:
- TypeScript (.ts)
- TSX (.tsx)
- JavaScript (.js)
- JSX (.jsx)
- Lua (.lua)
- Java (.java)
- C (.c)
- C++ (.cpp)
- C# (.cs)
- Python (.py)
The plugin should work with most languages out of the box. If you encounter issues with a specific language, please open an issue with a minimal example.
Using lazy.nvim
{
"subev/sibling-jump.nvim",
config = function()
require("sibling_jump").setup({
next_key = "<C-j>", -- Jump to next sibling (default)
prev_key = "<C-k>", -- Jump to previous sibling (default)
block_loop_key = "<C-l>", -- Cycle through block boundaries (optional)
center_on_jump = false, -- Center screen after jump (default: false)
})
end,
}Using packer.nvim
use {
"subev/sibling-jump.nvim",
config = function()
require("sibling_jump").setup({
next_key = "<C-j>",
prev_key = "<C-k>",
block_loop_key = "<C-l>", -- optional
})
end,
}Using vim-plug
Plug 'subev/sibling-jump.nvim'
" In your init.vim or after/plugin/sibling-jump.lua:
lua << EOF
require("sibling_jump").setup({
next_key = "<C-j>",
prev_key = "<C-k>",
block_loop_key = "<C-l>", -- optional
})
EOFOnce installed, use your configured keybindings:
<C-j>- Jump to next sibling<C-k>- Jump to previous sibling3<C-j>- Jump 3 siblings forward (works with any count)<C-l>- Cycle through block boundaries (ifblock_loop_keyconfigured)Vthen<C-l>- Select entire block in visual mode
Navigate object properties:
const obj = {
foo: 1, // <C-j> →
bar: 2, // <C-j> →
baz: 3, // cursor here
};Navigate array elements:
const arr = [
element1, // <C-j> →
element2, // <C-j> →
element3, // cursor here
];Navigate statements:
const x = 1; // <C-j> →
const y = 2; // <C-j> →
return x + y; // cursor hereNavigate method chains:
obj
.foo() // <C-j> →
.bar() // <C-j> →
.baz(); // cursor hereNavigate if-else chains:
if (condition1) {
// <C-j> →
// ...
} else if (condition2) {
// <C-j> →
// ...
} else {
// cursor here
// ...
}Navigate JSX elements:
<>
<Header /> // <C-j> →
<Content /> // <C-j> →
<Footer /> // cursor here
</>Cycle through a const declaration:
const config = {
// cursor on "const", <C-j> →
foo: 1,
bar: 2,
}; // ← lands here, <C-j> cycles back to "const"Cycle through if-else blocks:
if (condition1) {
// cursor on "if", <C-j> →
// ...
} else if (cond2) {
// ← <C-j> →
// ...
} else {
// ← <C-j> →
// ...
} // ← lands here, <C-j> cycles back to "if"Cycle through a switch statement:
switch (
value // cursor on "switch", <C-j> →
) {
case 1: // ← <C-j> →
break;
case 2: // ← <C-j> →
break;
default: // ← <C-j> →
break;
} // ← lands here, <C-j> cycles back to "switch"Cycle through a for loop:
for (let i = 0; i < 10; i++) {
// cursor on "for", <C-j> →
console.log(i);
} // ← lands here, <C-j> cycles backVisual mode progressive selection:
In visual mode, block-loop progressively extends the selection with each keypress:
if (condition1) { // v to start visual, <C-l> →
// ...
} else if (cond2) { // ← selection extends here, <C-l> →
// ...
} else { // ← selection extends here, <C-l> →
// ...
} // ← selection extends here, <C-l> wraps backThis lets you precisely control how much of the block to select - useful for selecting just the if-else-if portion without the final else, for example.
The setup() function accepts the following options:
require("sibling_jump").setup({
-- Key to jump to next sibling (default: "<C-j>")
next_key = "<C-j>",
-- Key to jump to previous sibling (default: "<C-k>")
prev_key = "<C-k>",
-- Key to cycle through block boundaries (default: nil = disabled)
-- When set, enables block-loop feature in both normal and visual modes
block_loop_key = "<C-l>",
-- Whether to center screen after each jump (default: false)
center_on_jump = false,
-- Separate center setting for block-loop (default: uses center_on_jump value)
block_loop_center_on_jump = false,
-- Optional: Restrict keymaps to specific filetypes (default: nil = global keymaps)
-- When set, creates buffer-local keymaps only for these filetypes
filetypes = { "typescript", "javascript", "typescriptreact", "javascriptreact" },
})To avoid keymap conflicts and improve performance, restrict the plugin to TS/JS files:
{
"subev/sibling-jump.nvim",
ft = { "typescript", "javascript", "typescriptreact", "javascriptreact" },
config = function()
require("sibling_jump").setup({
next_key = "<C-j>",
prev_key = "<C-k>",
center_on_jump = true,
filetypes = { "typescript", "javascript", "typescriptreact", "javascriptreact" },
})
end,
}This configuration:
- Lazy loads the plugin only when opening TS/JS files (
ftparameter) - Creates buffer-local keymaps only for TS/JS files (
filetypesoption) - Keymaps won't interfere with other filetypes
You can manually enable/disable sibling-jump for any buffer using these commands:
:SiblingJumpBufferEnable " Enable for current buffer
:SiblingJumpBufferDisable " Disable for current buffer
:SiblingJumpBufferToggle " Toggle on/off for current buffer
:SiblingJumpBufferStatus " Check if enabled for current bufferUse cases:
- Testing the plugin in non-TS/JS files (Python, Lua, etc.)
- Temporarily enabling for a specific file without changing config
- Quick experiments with the plugin in different languages
Example:
" Open a Python file
:e script.py
" Enable sibling-jump manually
:SiblingJumpBufferEnable
" Now <C-j> and <C-k> work in this buffer!- Neovim >= 0.9.0 (requires Tree-sitter support)
- Tree-sitter parser for your language (automatically installed for most languages)
Primary support:
- TypeScript / JavaScript
- TSX / JSX
Partial support:
- Python
- Lua
- Other languages with Tree-sitter parsers (may work, but not extensively tested)
sibling-jump uses Neovim's Tree-sitter integration to understand your code's structure. Instead of jumping by lines or words, it jumps between meaningful syntactic units.
Sibling Navigation (<C-j>/<C-k>):
- Finds the Tree-sitter node at your cursor
- Identifies the appropriate "navigation context" (e.g., are you in an object, array, statement block?)
- Finds the next/previous sibling node in that context
- Jumps to it, staying within the same level of abstraction
Block-Loop (<C-l> if configured):
- Detects the block construct you're on (
const,if,for,switch, etc.) - Collects all structural boundary positions (start, branches, end)
- Cycles through them in order, wrapping from end back to start
The plugin includes a comprehensive test suite with tests covering all supported navigation scenarios.
Run tests:
cd /path/to/sibling-jump.nvim
bash tests/test_runner.shAll tests pass with Tree-sitter support for TypeScript/JavaScript/JSX/TSX.
sibling-jump stays within your current context. When you are in a function you are jumping only inside of its top level statements/expressions. when you're in an object, it jumps between properties. When you're in an array, it jumps between elements. When you're in an if-else chain, it treats the entire chain as one navigable unit.
treewalker.nvim is great for full AST traversal (4 directions, moving between nesting levels), but sibling-jump focuses on "just working" horizontally - staying at the same level of abstraction without accidentally jumping out of your current block. The following shouldn't be possible with sibling-jump.nvim
sibling-jump also has a block-loop feature that cycles through a construct's boundaries (if → else if → else → closing brace). In visual mode, it selects the entire block - useful for quickly selecting an if-else chain, function, or declaration for deletion/yanking.
-
syntax-tree-surfer - Publicly archived. Had visual selection and swap features that inspired many Tree-sitter navigation plugins.
-
nvim-treehopper - Leap-like approach with label-based jumps to annotated nodes, rather than direct next/prev movements.
-
tree-climber.nvim - Fine-grained AST node navigation. Gives more literal syntax tree access vs sibling-jump's context-aware approach.
-
nvim-treesitter-textobjects - Node-type-specific movements and swaps. sibling-jump is node-type agnostic.
For more Tree-sitter motion plugins, see awesome-neovim#motion.
Contributions are welcome! Please feel free to submit issues or pull requests.
See ROADMAP.md for planned features and future direction.
MIT
Developed by @subev
You can develop this plugin directly in your lazy.nvim installation directory.
For AI-assisted development, see .ai/instructions.md for comprehensive project context, architecture details, and development guidelines.