Docs for bleeding edge master can be found at https://docs.helix-editor.com/master.

See the usage section for a quick overview of the editor, keymap section for all available keybindings and the configuration section for defining custom keybindings, setting themes, etc. For everything else (e.g., how to install supported language servers), see the Helix Wiki.

Refer the FAQ for common questions.

Installing Helix

To install Helix, follow the instructions specific to your operating system. Note that:

  • To get the latest nightly version of Helix, you need to build from source.

  • To take full advantage of Helix, install the language servers for your preferred programming languages. See the wiki for instructions.

Pre-built binaries

Download pre-built binaries from the GitHub Releases page. Add the hx binary to your system's $PATH to use it from the command line, and copy the runtime directory into the config directory (for example ~/.config/helix/runtime on Linux/macOS). The runtime location can be overriden via the HELIX_RUNTIME environment variable.

Package managers

Packaging status


The following third party repositories are available:


Add the PPA for Helix:

sudo add-apt-repository ppa:maveonair/helix-editor
sudo apt update
sudo apt install helix


sudo dnf install helix

Arch Linux extra

Releases are available in the extra repository:

sudo pacman -S helix

๐Ÿ’ก When installed from the extra repository, run Helix with helix instead of hx.

For example:

helix --health

to check health

Additionally, a helix-git package is available in the AUR, which builds the master branch.


Helix is available in nixpkgs through the helix attribute, the unstable channel usually carries the latest release.

Helix is also available as a flake in the project root. Use nix develop to spin up a reproducible development shell. Outputs are cached for each push to master using Cachix. The flake is configured to automatically make use of this cache assuming the user accepts the new settings on first use.

If you are using a version of Nix without flakes enabled, install Cachix CLI and use cachix use helix to configure Nix to use cached outputs when possible.


Helix is available on Flathub:

flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix


Helix is available on Snapcraft and can be installed with:

snap install --classic helix

This will install Helix as both /snap/bin/helix and /snap/bin/hx, so make sure /snap/bin is in your PATH.


Install Helix using the Linux AppImage format. Download the official Helix AppImage from the latest releases page.

chmod +x helix-*.AppImage # change permission for executable mode
./helix-*.AppImage # run helix


Homebrew Core

brew install helix


port install helix


Install on Windows using Winget, Scoop, Chocolatey or MSYS2.


Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer. You can get App Installer from the Microsoft Store. If it's already installed, make sure it is updated with the latest version.

winget install Helix.Helix


scoop install helix


choco install helix


For 64-bit Windows 8.1 or above:

pacman -S mingw-w64-ucrt-x86_64-helix

Building from source


Clone the Helix GitHub repository into a directory of your choice. The examples in this documentation assume installation into either ~/src/ on Linux and macOS, or %userprofile%\src\ on Windows.

If you are using the musl-libc standard library instead of glibc the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly:

RUSTFLAGS="-C target-feature=-crt-static"
  1. Clone the repository:

    git clone https://github.com/helix-editor/helix
    cd helix
  2. Compile from source:

    cargo install --path helix-term --locked

    This command will create the hx executable and construct the tree-sitter grammars in the local runtime folder.

๐Ÿ’ก If you do not want to fetch or build grammars, set an environment variable HELIX_DISABLE_AUTO_GRAMMAR_BUILD

๐Ÿ’ก Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch grammars with hx --grammar fetch and compile them with hx --grammar build. This will install them in the runtime directory within the user's helix config directory (more details below).

Configuring Helix's runtime files

Linux and macOS

The runtime directory is one below the Helix source, so either export a HELIX_RUNTIME environment variable to point to that directory and add it to your ~/.bashrc or equivalent:

export HELIX_RUNTIME=~/src/helix/runtime

Or, create a symbolic link:

ln -Ts $PWD/runtime ~/.config/helix/runtime

If the above command fails to create a symbolic link because the file exists either move ~/.config/helix/runtime to a new location or delete it, then run the symlink command above again.


Either set the HELIX_RUNTIME environment variable to point to the runtime files using the Windows setting (search for Edit environment variables for your account) or use the setx command in Cmd:

setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"

๐Ÿ’ก %userprofile% resolves to your user directory like C:\Users\Your-Name\ for example.

Or, create a symlink in %appdata%\helix\ that links to the source code directory:

PowerShellNew-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"
Cmdcd %appdata%\helix
mklink /D runtime "%userprofile%\src\helix\runtime"

๐Ÿ’ก On Windows, creating a symbolic link may require running PowerShell or Cmd as an administrator.

Multiple runtime directories

When Helix finds multiple runtime directories it will search through them for files in the following order:

  1. runtime/ sibling directory to $CARGO_MANIFEST_DIR directory (this is intended for developing and testing helix only).
  2. runtime/ subdirectory of OS-dependent helix user config directory.
  4. Distribution-specific fallback directory (set at compile timeโ€”not run timeโ€” with the HELIX_DEFAULT_RUNTIME environment variable)
  5. runtime/ subdirectory of path to Helix executable.

This order also sets the priority for selecting which file will be used if multiple runtime directories have files with the same name.

Note to packagers

If you are making a package of Helix for end users, to provide a good out of the box experience, you should set the HELIX_DEFAULT_RUNTIME environment variable at build time (before invoking cargo build) to a directory which will store the final runtime files after installation. For example, say you want to package the runtime into /usr/lib/helix/runtime. The rough steps a build script could follow are:

  1. export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime
  2. cargo build --profile opt --locked --path helix-term
  3. cp -r runtime $BUILD_DIR/usr/lib/helix/
  4. cp target/opt/hx $BUILD_DIR/usr/bin/hx

This way the resulting hx binary will always look for its runtime directory in /usr/lib/helix/runtime if the user has no custom runtime in ~/.config/helix or HELIX_RUNTIME.

Validating the installation

To make sure everything is set up as expected you should run the Helix health check:

hx --health

For more information on the health check results refer to Health check.

Configure the desktop shortcut

If your desktop environment supports the XDG desktop menu you can configure Helix to show up in the application menu by copying the provided .desktop and icon files to their correct folders:

cp contrib/Helix.desktop ~/.local/share/applications
cp contrib/helix.png ~/.icons # or ~/.local/share/icons

To use another terminal than the system default, you can modify the .desktop file. For example, to use kitty:

sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop

Using Helix

For a full interactive introduction to Helix, refer to the tutor which can be accessed via the command hx --tutor or :tutor.

๐Ÿ’ก Currently, not all functionality is fully documented, please refer to the key mappings list.


In Helix, registers are storage locations for text and other data, such as the result of a search. Registers can be used to cut, copy, and paste text, similar to the clipboard in other text editors. Usage is similar to Vim, with " being used to select a register.

User-defined registers

Helix allows you to create your own named registers for storing text, for example:

  • "ay - Yank the current selection to register a.
  • "op - Paste the text in register o after the selection.

If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:

  • "hc - Store the selection in register h and then change it (delete and enter insert mode).
  • "md - Store the selection in register m and delete it.

Default registers

Commands that use registers, like yank (y), use a default register if none is specified. These registers are used as defaults:

Register characterContains
/Last search
:Last executed command
"Last yanked text
@Last recorded macro

Special registers

Some registers have special behavior when read from and written to.

Register characterWhen readWhen written
_No values are returnedAll values are discarded
#Selection indices (first selection is 1, second is 2, etc.)This register is not writable
.Contents of the current selectionsThis register is not writable
%Name of the current fileThis register is not writable
+Reads from the system clipboardJoins and yanks to the system clipboard
*Reads from the primary clipboardJoins and yanks to the primary clipboard

When yanking multiple selections to the clipboard registers, the selections are joined with newlines. Pasting from these registers will paste multiple selections if the clipboard was last yanked to by the Helix session. Otherwise the clipboard contents are pasted as one selection.


Helix includes built-in functionality similar to vim-surround. The keymappings have been inspired from vim-sandwich:

Surround demo

Key SequenceAction
ms<char> (after selecting text)Add surround characters to selection
mr<char_to_replace><new_char>Replace the closest surround characters
md<char_to_delete>Delete the closest surround characters

You can use counts to act on outer pairs.

Surround can also act on multiple selections. For example, to change every occurrence of (use) to [use]:

  1. % to select the whole file
  2. s to split the selections on a search term
  3. Input use and hit Enter
  4. mr([ to replace the parentheses with square brackets

Multiple characters are currently not supported, but planned for future release.

Selecting and manipulating text with textobjects

In Helix, textobjects are a way to select, manipulate and operate on a piece of text in a structured way. They allow you to refer to blocks of text based on their structure or purpose, such as a word, sentence, paragraph, or even a function or block of code.

Textobject demo Textobject tree-sitter demo

  • ma - Select around the object (va in Vim, <alt-a> in Kakoune)
  • mi - Select inside the object (vi in Vim, <alt-i> in Kakoune)
Key after mi or maTextobject selected
(, [, ', etc.Specified surround pairs
mThe closest surround pair
tType (or Class)

๐Ÿ’ก f, t, etc. need a tree-sitter grammar active for the current document and a special tree-sitter query file to work properly. [Only some grammars][lang-support] currently have the query file implemented. Contributions are welcome!

Navigating between functions, classes, parameters, and other elements is possible using tree-sitter and textobject queries. For example to move to the next function use ]f, to move to previous type use [t, and so on.


For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind documentation.

๐Ÿ’ก This feature relies on tree-sitter textobjects and requires the corresponding query file to work properly.

Moving the selection with syntax-aware motions

Alt-p, Alt-o, Alt-i, and Alt-n (or Alt and arrow keys) allow you to move the selection according to its location in the syntax tree. For example, many languages have the following syntax for function calls:

func(arg1, arg2, arg3);

A function call might be parsed by tree-sitter into a tree like the following.

  function: (identifier) ; func
    (arguments           ; (arg1, arg2, arg3)
      (identifier)       ; arg1
      (identifier)       ; arg2
      (identifier)))     ; arg3

Use :tree-sitter-subtree to view the syntax tree of the primary selection. In a more intuitive tree format:

      โ”‚                โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”
โ”‚identifierโ”‚      โ”‚argumentsโ”‚
โ”‚  "func"  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚        โ”‚         โ”‚
             โ”‚        โ”‚         โ”‚
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”  โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚identifierโ”‚  โ”‚identifierโ”‚  โ”‚identifierโ”‚
   โ”‚  "arg1"  โ”‚  โ”‚  "arg2"  โ”‚  โ”‚  "arg3"  โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

If you have a selection that wraps arg1 (see the tree above), and you use Alt-n, it will select the next sibling in the syntax tree: arg2.

// before
func([arg1], arg2, arg3)
// after
func(arg1, [arg2], arg3);

Similarly, Alt-o will expand the selection to the parent node, in this case, the arguments node.

func[(arg1, arg2, arg3)];

There is also some nuanced behavior that prevents you from getting stuck on a node with no sibling. When using Alt-p with a selection on arg1, the previous child node will be selected. In the event that arg1 does not have a previous sibling, the selection will move up the syntax tree and select the previous element. As a result, using Alt-p with a selection on arg1 will move the selection to the "func" identifier.


๐Ÿ’ก Mappings marked (LSP) require an active language server for the file.

๐Ÿ’ก Mappings marked (TS) require a tree-sitter grammar for the file type.

โš ๏ธ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts.

Normal mode

Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the Escape key.


NOTE: Unlike Vim, f, F, t and T are not confined to the current line.

h, LeftMove leftmove_char_left
j, DownMove downmove_visual_line_down
k, UpMove upmove_visual_line_up
l, RightMove rightmove_char_right
wMove next word startmove_next_word_start
bMove previous word startmove_prev_word_start
eMove next word endmove_next_word_end
WMove next WORD startmove_next_long_word_start
BMove previous WORD startmove_prev_long_word_start
EMove next WORD endmove_next_long_word_end
tFind 'till next charfind_till_char
fFind next charfind_next_char
TFind 'till previous chartill_prev_char
FFind previous charfind_prev_char
GGo to line number <n>goto_line
Alt-.Repeat last motion (f, t, m, [ or ])repeat_last_motion
HomeMove to the start of the linegoto_line_start
EndMove to the end of the linegoto_line_end
Ctrl-b, PageUpMove page uppage_up
Ctrl-f, PageDownMove page downpage_down
Ctrl-uMove cursor and page half page uppage_cursor_half_up
Ctrl-dMove cursor and page half page downpage_cursor_half_down
Ctrl-iJump forward on the jumplistjump_forward
Ctrl-oJump backward on the jumplistjump_backward
Ctrl-sSave the current selection to the jumplistsave_selection


rReplace with a characterreplace
RReplace with yanked textreplace_with_yanked
~Switch case of the selected textswitch_case
`Set the selected text to lower caseswitch_to_lowercase
Alt-`Set the selected text to upper caseswitch_to_uppercase
iInsert before selectioninsert_mode
aInsert after selection (append)append_mode
IInsert at the start of the lineinsert_at_line_start
AInsert at the end of the lineinsert_at_line_end
oOpen new line below selectionopen_below
OOpen new line above selectionopen_above
.Repeat last insertN/A
uUndo changeundo
URedo changeredo
Alt-uMove backward in historyearlier
Alt-UMove forward in historylater
yYank selectionyank
pPaste after selectionpaste_after
PPaste before selectionpaste_before
" <reg>Select a register to yank to or paste fromselect_register
>Indent selectionindent
<Unindent selectionunindent
=Format selection (LSP)format_selections
dDelete selectiondelete_selection
Alt-dDelete selection, without yankingdelete_selection_noyank
cChange selection (delete and enter insert mode)change_selection
Alt-cChange selection (delete and enter insert mode, without yanking)change_selection_noyank
Ctrl-aIncrement object (number) under cursorincrement
Ctrl-xDecrement object (number) under cursordecrement
QStart/stop macro recording to the selected register (experimental)record_macro
qPlay back a recorded macro from the selected register (experimental)replay_macro


|Pipe each selection through shell command, replacing with outputshell_pipe
Alt-|Pipe each selection into shell command, ignoring outputshell_pipe_to
!Run shell command, inserting output before each selectionshell_insert_output
Alt-!Run shell command, appending output after each selectionshell_append_output
$Pipe each selection into shell command, keep selections where command returned 0shell_keep_pipe

Selection manipulation

sSelect all regex matches inside selectionsselect_regex
SSplit selection into sub selections on regex matchessplit_selection
Alt-sSplit selection on newlinessplit_selection_on_newline
Alt-minusMerge selectionsmerge_selections
Alt-_Merge consecutive selectionsmerge_consecutive_selections
&Align selection in columnsalign_selections
_Trim whitespace from the selectiontrim_selections
;Collapse selection onto a single cursorcollapse_selection
Alt-;Flip selection cursor and anchorflip_selections
Alt-:Ensures the selection is in forward directionensure_selections_forward
,Keep only the primary selectionkeep_primary_selection
Alt-,Remove the primary selectionremove_primary_selection
CCopy selection onto the next line (Add cursor below)copy_selection_on_next_line
Alt-CCopy selection onto the previous line (Add cursor above)copy_selection_on_prev_line
(Rotate main selection backwardrotate_selections_backward
)Rotate main selection forwardrotate_selections_forward
Alt-(Rotate selection contents backwardrotate_selection_contents_backward
Alt-)Rotate selection contents forwardrotate_selection_contents_forward
%Select entire fileselect_all
xSelect current line, if already selected, extend to next lineextend_line_below
XExtend selection to line bounds (line-wise selection)extend_to_line_bounds
Alt-xShrink selection to line bounds (line-wise selection)shrink_to_line_bounds
JJoin lines inside selectionjoin_selections
Alt-JJoin lines inside selection and select the inserted spacejoin_selections_space
KKeep selections matching the regexkeep_selections
Alt-KRemove selections matching the regexremove_selections
Ctrl-cComment/uncomment the selectionstoggle_comments
Alt-o, Alt-upExpand selection to parent syntax node (TS)expand_selection
Alt-i, Alt-downShrink syntax tree object selection (TS)shrink_selection
Alt-p, Alt-leftSelect previous sibling node in syntax tree (TS)select_prev_sibling
Alt-n, Alt-rightSelect next sibling node in syntax tree (TS)select_next_sibling

Search commands all operate on the / register by default. To use a different register, use "<char>.

/Search for regex patternsearch
?Search for previous patternrsearch
nSelect next search matchsearch_next
NSelect previous search matchsearch_prev
*Use current selection as the search patternsearch_selection

Minor modes

These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.

vEnter select (extend) modeselect_mode
gEnter goto modeN/A
mEnter match modeN/A
:Enter command modecommand_mode
zEnter view modeN/A
ZEnter sticky view modeN/A
Ctrl-wEnter window modeN/A
SpaceEnter space modeN/A

These modes (except command mode) can be configured by remapping keys.

View mode

Accessed by typing z in normal mode.

View mode is intended for scrolling and manipulating the view without changing the selection. The "sticky" variant of this mode (accessed by typing Z in normal mode) is persistent and can be exited using the escape key. This is useful when you're simply looking over text and not actively editing it.

z, cVertically center the linealign_view_center
tAlign the line to the top of the screenalign_view_top
bAlign the line to the bottom of the screenalign_view_bottom
mAlign the line to the middle of the screen (horizontally)align_view_middle
j, downScroll the view downwardsscroll_down
k, upScroll the view upwardsscroll_up
Ctrl-f, PageDownMove page downpage_down
Ctrl-b, PageUpMove page uppage_up
Ctrl-uMove cursor and page half page uppage_cursor_half_up
Ctrl-dMove cursor and page half page downpage_cursor_half_down

Goto mode

Accessed by typing g in normal mode.

Jumps to various locations.

gGo to line number <n> else start of filegoto_file_start
eGo to the end of the filegoto_last_line
fGo to files in the selectionsgoto_file
hGo to the start of the linegoto_line_start
lGo to the end of the linegoto_line_end
sGo to first non-whitespace character of the linegoto_first_nonwhitespace
tGo to the top of the screengoto_window_top
cGo to the middle of the screengoto_window_center
bGo to the bottom of the screengoto_window_bottom
dGo to definition (LSP)goto_definition
yGo to type definition (LSP)goto_type_definition
rGo to references (LSP)goto_reference
iGo to implementation (LSP)goto_implementation
aGo to the last accessed/alternate filegoto_last_accessed_file
mGo to the last modified/alternate filegoto_last_modified_file
nGo to next buffergoto_next_buffer
pGo to previous buffergoto_previous_buffer
.Go to last modification in current filegoto_last_modification
jMove down textual (instead of visual) linemove_line_down
kMove up textual (instead of visual) linemove_line_up
wShow labels at each word and select the word that belongs to the entered labelsgoto_word

Match mode

Accessed by typing m in normal mode.

See the relevant section in Usage for an explanation about surround and textobject usage.

mGoto matching bracket (TS)match_brackets
s <char>Surround current selection with <char>surround_add
r <from><to>Replace surround character <from> with <to>surround_replace
d <char>Delete surround character <char>surround_delete
a <object>Select around textobjectselect_textobject_around
i <object>Select inside textobjectselect_textobject_inner

TODO: Mappings for selecting syntax nodes (a superset of [).

Window mode

Accessed by typing Ctrl-w in normal mode.

This layer is similar to Vim keybindings as Kakoune does not support windows.

w, Ctrl-wSwitch to next windowrotate_view
v, Ctrl-vVertical right splitvsplit
s, Ctrl-sHorizontal bottom splithsplit
fGo to files in the selections in horizontal splitsgoto_file
FGo to files in the selections in vertical splitsgoto_file
h, Ctrl-h, LeftMove to left splitjump_view_left
j, Ctrl-j, DownMove to split belowjump_view_down
k, Ctrl-k, UpMove to split abovejump_view_up
l, Ctrl-l, RightMove to right splitjump_view_right
q, Ctrl-qClose current windowwclose
o, Ctrl-oOnly keep the current window, closing all the otherswonly
HSwap window to the leftswap_view_left
JSwap window downwardsswap_view_down
KSwap window upwardsswap_view_up
LSwap window to the rightswap_view_right

Space mode

Accessed by typing Space in normal mode.

This layer is a kludge of mappings, mostly pickers.

fOpen file pickerfile_picker
FOpen file picker at current working directoryfile_picker_in_current_directory
bOpen buffer pickerbuffer_picker
jOpen jumplist pickerjumplist_picker
gOpen changed file pickerchanged_file_picker
GDebug (experimental)N/A
kShow documentation for item under cursor in a popup (LSP)hover
sOpen document symbol picker (LSP)symbol_picker
SOpen workspace symbol picker (LSP)workspace_symbol_picker
dOpen document diagnostics picker (LSP)diagnostics_picker
DOpen workspace diagnostics picker (LSP)workspace_diagnostics_picker
rRename symbol (LSP)rename_symbol
aApply code action (LSP)code_action
hSelect symbol references (LSP)select_references_to_symbol_under_cursor
'Open last fuzzy pickerlast_picker
wEnter window modeN/A
cComment/uncomment selectionstoggle_comments
CBlock comment/uncomment selectionstoggle_block_comments
Alt-cLine comment/uncomment selectionstoggle_line_comments
pPaste system clipboard after selectionspaste_clipboard_after
PPaste system clipboard before selectionspaste_clipboard_before
yYank selections to clipboardyank_to_clipboard
YYank main selection to clipboardyank_main_selection_to_clipboard
RReplace selections by clipboard contentsreplace_selections_with_clipboard
/Global search in workspace folderglobal_search
?Open command palettecommand_palette

๐Ÿ’ก Global search displays results in a fuzzy picker, use Space + ' to bring it back up after opening a file.

Displays documentation for item under cursor. Remapping currently not supported.

Ctrl-uScroll up
Ctrl-dScroll down
Completion Menu

Displays documentation for the selected completion item. Remapping currently not supported.

Shift-Tab, Ctrl-p, UpPrevious entry
Tab, Ctrl-n, DownNext entry
Signature-help Popup

Displays the signature of the selected completion item. Remapping currently not supported.

Alt-pPrevious signature
Alt-nNext signature


These mappings are in the style of vim-unimpaired.

]dGo to next diagnostic (LSP)goto_next_diag
[dGo to previous diagnostic (LSP)goto_prev_diag
]DGo to last diagnostic in document (LSP)goto_last_diag
[DGo to first diagnostic in document (LSP)goto_first_diag
]fGo to next function (TS)goto_next_function
[fGo to previous function (TS)goto_prev_function
]tGo to next type definition (TS)goto_next_class
[tGo to previous type definition (TS)goto_prev_class
]aGo to next argument/parameter (TS)goto_next_parameter
[aGo to previous argument/parameter (TS)goto_prev_parameter
]cGo to next comment (TS)goto_next_comment
[cGo to previous comment (TS)goto_prev_comment
]TGo to next test (TS)goto_next_test
[TGo to previous test (TS)goto_prev_test
]pGo to next paragraphgoto_next_paragraph
[pGo to previous paragraphgoto_prev_paragraph
]gGo to next changegoto_next_change
[gGo to previous changegoto_prev_change
]GGo to last changegoto_last_change
[GGo to first changegoto_first_change
]SpaceAdd newline belowadd_newline_below
[SpaceAdd newline aboveadd_newline_above

Insert mode

Accessed by typing i in normal mode.

Insert mode bindings are minimal by default. Helix is designed to be a modal editor, and this is reflected in the user experience and internal mechanics. Changes to the text are only saved for undos when escaping from insert mode to normal mode.

๐Ÿ’ก New users are strongly encouraged to learn the modal editing paradigm to get the smoothest experience.

EscapeSwitch to normal modenormal_mode
Ctrl-sCommit undo checkpointcommit_undo_checkpoint
Ctrl-rInsert a register contentinsert_register
Ctrl-w, Alt-BackspaceDelete previous worddelete_word_backward
Alt-d, Alt-DeleteDelete next worddelete_word_forward
Ctrl-uDelete to start of linekill_to_line_start
Ctrl-kDelete to end of linekill_to_line_end
Ctrl-h, Backspace, Shift-BackspaceDelete previous chardelete_char_backward
Ctrl-d, DeleteDelete next chardelete_char_forward
Ctrl-j, EnterInsert new lineinsert_newline

These keys are not recommended, but are included for new users less familiar with modal editors.

UpMove to previous linemove_line_up
DownMove to next linemove_line_down
LeftBackward a charmove_char_left
RightForward a charmove_char_right
PageUpMove one page uppage_up
PageDownMove one page downpage_down
HomeMove to line startgoto_line_start
EndMove to line endgoto_line_end_newline

As you become more comfortable with modal editing, you may want to disable some insert mode bindings. You can do this by editing your config.toml file.

up = "no_op"
down = "no_op"
left = "no_op"
right = "no_op"
pageup = "no_op"
pagedown = "no_op"
home = "no_op"
end = "no_op"

Select / extend mode

Accessed by typing v in normal mode.

Select mode echoes Normal mode, but changes any movements to extend selections rather than replace them. Goto motions are also changed to extend, so that vgl, for example, extends the selection to the end of the line.

Search is also affected. By default, n and N will remove the current selection and select the next instance of the search term. Toggling this mode before pressing n or N makes it possible to keep the current selection. Toggling it on and off during your iterative searching allows you to selectively add search terms to your selections.


Keys to use within picker. Remapping currently not supported.

Shift-Tab, Up, Ctrl-pPrevious entry
Tab, Down, Ctrl-nNext entry
PageUp, Ctrl-uPage up
PageDown, Ctrl-dPage down
HomeGo to first entry
EndGo to last entry
EnterOpen selected
Alt-EnterOpen selected in the background without closing the picker
Ctrl-sOpen horizontally
Ctrl-vOpen vertically
Ctrl-tToggle preview
Escape, Ctrl-cClose picker


Keys to use within prompt, Remapping currently not supported.

Escape, Ctrl-cClose prompt
Alt-b, Ctrl-LeftBackward a word
Ctrl-b, LeftBackward a char
Alt-f, Ctrl-RightForward a word
Ctrl-f, RightForward a char
Ctrl-e, EndMove prompt end
Ctrl-a, HomeMove prompt start
Ctrl-w, Alt-Backspace, Ctrl-BackspaceDelete previous word
Alt-d, Alt-Delete, Ctrl-DeleteDelete next word
Ctrl-uDelete to start of line
Ctrl-kDelete to end of line
Backspace, Ctrl-h, Shift-BackspaceDelete previous char
Delete, Ctrl-dDelete next char
Ctrl-sInsert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later
Ctrl-p, UpSelect previous history
Ctrl-n, DownSelect next history
Ctrl-rInsert the content of the register selected by following input char
TabSelect next completion item
BackTabSelect previous completion item
EnterOpen selected


Command mode can be activated by pressing :. The built-in commands are:

:quit, :qClose the current view.
:quit!, :q!Force close the current view, ignoring unsaved changes.
:open, :oOpen a file from disk into the current view.
:buffer-close, :bc, :bcloseClose the current buffer.
:buffer-close!, :bc!, :bclose!Close the current buffer forcefully, ignoring unsaved changes.
:buffer-close-others, :bco, :bcloseotherClose all buffers but the currently focused one.
:buffer-close-others!, :bco!, :bcloseother!Force close all buffers but the currently focused one.
:buffer-close-all, :bca, :bcloseallClose all buffers without quitting.
:buffer-close-all!, :bca!, :bcloseall!Force close all buffers ignoring unsaved changes without quitting.
:buffer-next, :bn, :bnextGoto next buffer.
:buffer-previous, :bp, :bprevGoto previous buffer.
:write, :wWrite changes to disk. Accepts an optional path (:write some/path.txt)
:write!, :w!Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)
:write-buffer-close, :wbcWrite changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)
:write-buffer-close!, :wbc!Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)
:new, :nCreate a new scratch buffer.
:format, :fmtFormat the file using the LSP formatter.
:indent-styleSet the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.)
:line-endingSet the document's default line ending. Options: crlf, lf.
:earlier, :earJump back to an earlier point in edit history. Accepts a number of steps or a time span.
:later, :latJump to a later point in edit history. Accepts a number of steps or a time span.
:write-quit, :wq, :xWrite changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)
:write-quit!, :wq!, :x!Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)
:write-all, :waWrite changes from all buffers to disk.
:write-all!, :wa!Forcefully write changes from all buffers to disk creating necessary subdirectories.
:write-quit-all, :wqa, :xaWrite changes from all buffers to disk and close all views.
:write-quit-all!, :wqa!, :xa!Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).
:quit-all, :qaClose all views.
:quit-all!, :qa!Force close all views ignoring unsaved changes.
:cquit, :cqQuit with exit code (default 1). Accepts an optional integer exit code (:cq 2).
:cquit!, :cq!Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).
:themeChange the editor theme (show current theme if no name specified).
:yank-joinYank joined selections. A separator can be provided as first argument. Default value is newline.
:clipboard-yankYank main selection into system clipboard.
:clipboard-yank-joinYank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.
:primary-clipboard-yankYank main selection into system primary clipboard.
:primary-clipboard-yank-joinYank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.
:clipboard-paste-afterPaste system clipboard after selections.
:clipboard-paste-beforePaste system clipboard before selections.
:clipboard-paste-replaceReplace selections with content of system clipboard.
:primary-clipboard-paste-afterPaste primary clipboard after selections.
:primary-clipboard-paste-beforePaste primary clipboard before selections.
:primary-clipboard-paste-replaceReplace selections with content of system primary clipboard.
:show-clipboard-providerShow clipboard provider name in status bar.
:change-current-directory, :cdChange the current working directory.
:show-directory, :pwdShow the current working directory.
:encodingSet encoding. Based on https://encoding.spec.whatwg.org.
:character-info, :charGet info about the character under the primary cursor.
:reload, :rlDiscard changes and reload from the source file.
:reload-all, :rlaDiscard changes and reload all documents from the source files.
:update, :uWrite changes only if the file has been modified.
:lsp-workspace-commandOpen workspace command picker
:lsp-restartRestarts the language servers used by the current doc
:lsp-stopStops the language servers that are used by the current doc
:tree-sitter-scopesDisplay tree sitter scopes, primarily for theming and development.
:tree-sitter-highlight-nameDisplay name of tree-sitter highlight scope under the cursor.
:debug-start, :dbgStart a debug session from a given template with given parameters.
:debug-remote, :dbg-tcpConnect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.
:debug-evalEvaluate expression in current debug context.
:vsplit, :vsOpen the file in a vertical split.
:vsplit-new, :vnewOpen a scratch buffer in a vertical split.
:hsplit, :hs, :spOpen the file in a horizontal split.
:hsplit-new, :hnewOpen a scratch buffer in a horizontal split.
:tutorOpen the tutorial.
:goto, :gGoto line number.
:set-language, :langSet the language of current buffer (show current language if no value specified).
:set-option, :setSet a config option at runtime.
For example to disable smart case search, use :set search.smart-case false.
:toggle-option, :toggleToggle a boolean config option at runtime.
For example to toggle smart case search, use :toggle search.smart-case.
:get-option, :getGet the current value of a config option.
:sortSort ranges in selection.
:rsortSort ranges in selection in reverse order.
:reflowHard-wrap the current selection of lines to a given width.
:tree-sitter-subtree, :ts-subtreeDisplay tree sitter subtree under cursor, primarily for debugging queries.
:config-reloadRefresh user config.
:config-openOpen the user config.toml file.
:config-open-workspaceOpen the workspace config.toml file.
:log-openOpen the helix log file.
:insert-outputRun shell command, inserting output before each selection.
:append-outputRun shell command, appending output after each selection.
:pipePipe each selection to the shell command.
:pipe-toPipe each selection to the shell command, ignoring output.
:run-shell-command, :shRun a shell command
:reset-diff-change, :diffget, :diffgReset the diff change at the cursor position.
:clear-registerClear given register. If no argument is provided, clear all registers.
:redrawClear and re-render the whole UI
:moveMove the current buffer and its corresponding file to a different path
:yank-diagnosticYank diagnostic(s) under primary cursor to register, or clipboard by default
:read, :rLoad a file into buffer

Language Support

The following languages and Language Servers are supported. To use Language Server features, you must first configure the appropriate Language Server.

You can check the language support in your installed helix version with hx --health.

Also see the Language Configuration docs and the Adding Languages guide for more language configuration information.

LanguageSyntax HighlightingTreesitter TextobjectsAuto IndentDefault LSP
adaโœ“โœ“ada_language_server, ada_language_server
docker-composeโœ“โœ“docker-compose-langserver, yaml-language-server
gjsโœ“โœ“โœ“typescript-language-server, vscode-eslint-language-server, ember-language-server
goโœ“โœ“โœ“gopls, golangci-lint-langserver
gtsโœ“โœ“โœ“typescript-language-server, vscode-eslint-language-server, ember-language-server
markdownโœ“marksman, markdown-oxide
pkgbuildโœ“โœ“โœ“pkgbuild-language-server, bash-language-server
protobufโœ“โœ“โœ“bufls, pb
typstโœ“tinymist, typst-lsp
yamlโœ“โœ“yaml-language-server, ansible-language-server

Migrating from Vim

Helix's editing model is strongly inspired from Vim and Kakoune, and a notable difference from Vim (and the most striking similarity to Kakoune) is that Helix follows the selection โ†’ action model. This means that whatever you are going to act on (a word, a paragraph, a line, etc.) is selected first and the action itself (delete, change, yank, etc.) comes second. A cursor is simply a single width selection.

See also Kakoune's Migrating from Vim and Helix's Migrating from Vim.

TODO: Mention textobjects, surround, registers


To override global configuration parameters, create a config.toml file located in your config directory:

  • Linux and Mac: ~/.config/helix/config.toml
  • Windows: %AppData%\helix\config.toml

๐Ÿ’ก You can easily open the config file by typing :config-open within Helix normal mode.

Example config:

theme = "onedark"

line-number = "relative"
mouse = false

insert = "bar"
normal = "block"
select = "underline"

hidden = false

You can use a custom configuration file by specifying it with the -c or --config command line argument, for example hx -c path/to/custom-config.toml. Additionally, you can reload the configuration file by sending the USR1 signal to the Helix process on Unix operating systems, such as by using the command pkill -USR1 hx.

Finally, you can have a config.toml local to a project by putting it under a .helix directory in your repository. Its settings will be merged with the configuration directory config.toml and the built-in configuration.


[editor] Section

scrolloffNumber of lines of padding around the edge of the screen when scrolling5
mouseEnable mouse modetrue
middle-click-pasteMiddle click paste supporttrue
scroll-linesNumber of lines to scroll per scroll wheel step3
shellShell to use when running external commandsUnix: ["sh", "-c"]
Windows: ["cmd", "/C"]
line-numberLine number display: absolute simply shows each line's number, while relative shows the distance from the current line. When unfocused or in insert mode, relative will still show absolute line numbersabsolute
cursorlineHighlight all lines with a cursorfalse
cursorcolumnHighlight all columns with a cursorfalse
guttersGutters to display: Available are diagnostics and diff and line-numbers and spacer, note that diagnostics also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty["diagnostics", "spacer", "line-numbers", "spacer", "diff"]
auto-completionEnable automatic pop up of auto-completiontrue
auto-formatEnable automatic formatting on savetrue
auto-saveEnable automatic saving on the focus moving away from Helix. Requires focus event support from your terminalfalse
idle-timeoutTime in milliseconds since last keypress before idle timers trigger.250
completion-timeoutTime in milliseconds after typing a word character before completions are shown, set to 5 for instant.250
preview-completion-insertWhether to apply completion item instantly when selectedtrue
completion-trigger-lenThe min-length of word under cursor to trigger autocompletion2
completion-replaceSet to true to make completions always replace the entire word and not just the part before the cursorfalse
auto-infoWhether to display info boxestrue
true-colorSet to true to override automatic detection of terminal truecolor support in the event of a false negativefalse
undercurlSet to true to override automatic detection of terminal undercurl support in the event of a false negativefalse
rulersList of column positions at which to display the rulers. Can be overridden by language specific rulers in languages.toml file[]
bufferlineRenders a line at the top of the editor displaying open buffers. Can be always, never or multiple (only shown if more than one buffer is in use)never
color-modesWhether to color the mode indicator with different colors depending on the mode itselffalse
text-widthMaximum line length. Used for the :reflow command and soft-wrapping if soft-wrap.wrap-at-text-width is set80
workspace-lsp-rootsDirectories relative to the workspace root that are treated as LSP roots. Should only be set in .helix/config.toml[]
default-line-endingThe line ending to use for new documents. Can be native, lf, crlf, ff, cr or nel. native uses the platform's native line ending (crlf on Windows, otherwise lf).native
insert-final-newlineWhether to automatically insert a trailing line-ending on write if missingtrue
popup-borderDraw border around popup, menu, all, or nonenone
indent-heuristicHow the indentation for a newly inserted line is computed: simple just copies the indentation level from the previous line, tree-sitter computes the indentation based on the syntax tree and hybrid combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being hybrid -> tree-sitter -> simple).hybrid
jump-label-alphabetThe characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first."abcdefghijklmnopqrstuvwxyz"

[editor.statusline] Section

Allows configuring the statusline at the bottom of the editor.

The configuration distinguishes between three areas of the status line:

[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]

Statusline elements can be defined as follows:

left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "โ”‚"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"

The [editor.statusline] key takes the following sub-keys:

leftA list of elements aligned to the left of the statusline["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]
centerA list of elements aligned to the middle of the statusline[]
rightA list of elements aligned to the right of the statusline["diagnostics", "selections", "register", "position", "file-encoding"]
separatorThe character used to separate elements in the statusline"โ”‚"
mode.normalThe text shown in the mode element for normal mode"NOR"
mode.insertThe text shown in the mode element for insert mode"INS"
mode.selectThe text shown in the mode element for select mode"SEL"

The following statusline elements can be configured:

modeThe current editor mode (mode.normal/mode.insert/mode.select)
spinnerA progress spinner indicating LSP activity
file-nameThe path/name of the opened file
file-absolute-pathThe absolute path/name of the opened file
file-base-nameThe basename of the opened file
file-modification-indicatorThe indicator to show whether the file is modified (a [+] appears when there are unsaved changes)
file-encodingThe encoding of the opened file if it differs from UTF-8
file-line-endingThe file line endings (CRLF or LF)
read-only-indicatorAn indicator that shows [readonly] when a file cannot be written
total-line-numbersThe total line numbers of the opened file
file-typeThe type of the opened file
diagnosticsThe number of warnings and/or errors
workspace-diagnosticsThe number of warnings and/or errors on workspace
selectionsThe number of active selections
primary-selection-lengthThe number of characters currently in primary selection
positionThe cursor position
position-percentageThe cursor position as a percentage of the total number of lines
separatorThe string defined in editor.statusline.separator (defaults to "โ”‚")
spacerInserts a space between elements (multiple/contiguous spacers may be specified)
version-controlThe current branch name or detached commit hash of the opened workspace
registerThe current selected register

[editor.lsp] Section

enableEnables LSP integration. Setting to false will completely disable language servers regardless of language settings.true
display-messagesDisplay LSP progress messages below statusline1false
auto-signature-helpEnable automatic popup of signature help (parameter hints)true
display-inlay-hintsDisplay inlay hints2false
display-signature-help-docsDisplay docs under signature help popuptrue
snippetsEnables snippet completions. Requires a server restart (:lsp-restart) to take effect after :config-reload/:set.true
goto-reference-include-declarationInclude declaration in the goto references popup.true

By default, a progress spinner is shown in the statusline beside the file path.


You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!

[editor.cursor-shape] Section

Defines the shape of cursor in each mode. Valid values for these options are block, bar, underline, or hidden.

๐Ÿ’ก Due to limitations of the terminal environment, only the primary cursor can change shape.

normalCursor shape in normal modeblock
insertCursor shape in insert modeblock
selectCursor shape in select modeblock

[editor.file-picker] Section

Set options for file picker and global search. Ignoring a file means it is not visible in the Helix file picker and global search.

All git related options are only enabled in a git repository.

hiddenEnables ignoring hidden filestrue
follow-symlinksFollow symlinks instead of ignoring themtrue
deduplicate-linksIgnore symlinks that point at files already shown in the pickertrue
parentsEnables reading ignore files from parent directoriestrue
ignoreEnables reading .ignore filestrue
git-ignoreEnables reading .gitignore filestrue
git-globalEnables reading global .gitignore, whose path is specified in git's config: core.excludesfile optiontrue
git-excludeEnables reading .git/info/exclude filestrue
max-depthSet with an integer value for maximum depth to recurseUnset by default

Ignore files can be placed locally as .ignore or put in your home directory as ~/.ignore. They support the usual ignore and negative ignore (unignore) rules used in .gitignore files.

Additionally, you can use Helix-specific ignore files by creating a local .helix/ignore file in the current workspace or a global ignore file located in your Helix config directory:

  • Linux and Mac: ~/.config/helix/ignore
  • Windows: %AppData%\helix\ignore


# unignore in file picker and global search

[editor.auto-pairs] Section

Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a simple boolean value, or a specific mapping of pairs of single characters.

To disable auto-pairs altogether, set auto-pairs to false:

auto-pairs = false # defaults to `true`

The default pairs are (){}[]''""``, but these can be customized by setting auto-pairs to a TOML table:

'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'

Additionally, this setting can be used in a language config. Unless the editor setting is false, this will override the editor config in documents with this language.

Example languages.toml that adds <> and removes ''

name = "rust"

'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'

[editor.search] Section

Search specific options.

smart-caseEnable smart case regex searching (case-insensitive unless pattern contains upper case characters)true
wrap-aroundWhether the search should wrap after depleting the matchestrue

[editor.whitespace] Section

Options for rendering whitespace with visible characters. Use :set whitespace.render all to temporarily enable visible whitespace.

renderWhether to render whitespace. May either be all or none, or a table with sub-keys space, nbsp, nnbsp, tab, and newlinenone
charactersLiteral characters to use when rendering whitespace. Sub-keys may be any of tab, space, nbsp, nnbsp, newline or tabpadSee example below


render = "all"
# or control each character
space = "all"
tab = "all"
nbsp = "none"
nnbsp = "none"
newline = "none"

space = "ยท"
nbsp = "โฝ"
nnbsp = "โฃ"
tab = "โ†’"
newline = "โŽ"
tabpad = "ยท" # Tabs will look like "โ†’ยทยทยท" (depending on tab width)

[editor.indent-guides] Section

Options for rendering vertical indent guides.

renderWhether to render indent guidesfalse
characterLiteral character to use for rendering the indent guideโ”‚
skip-levelsNumber of indent levels to skip0


render = true
character = "โ•Ž" # Some characters that work well: "โ–", "โ”†", "โ”Š", "โธฝ"
skip-levels = 1

[editor.gutters] Section

For simplicity, editor.gutters accepts an array of gutter types, which will use default settings for all gutter components.

gutters = ["diff", "diagnostics", "line-numbers", "spacer"]

To customize the behavior of gutters, the [editor.gutters] section must be used. This section contains top level settings, as well as settings for specific gutter components as subsections.

layoutA vector of gutters to display["diagnostics", "spacer", "line-numbers", "spacer", "diff"]


layout = ["diff", "diagnostics", "line-numbers", "spacer"]

[editor.gutters.line-numbers] Section

Options for the line number gutter

min-widthThe minimum number of characters to use3


min-width = 1

[editor.gutters.diagnostics] Section

Currently unused

[editor.gutters.diff] Section

The diff gutter option displays colored bars indicating whether a git diff represents that a line was added, removed or changed. These colors are controlled by the theme attributes diff.plus, diff.minus and diff.delta.

Other diff providers will eventually be supported by a future plugin system.

There are currently no options for this section.

[editor.gutters.spacer] Section

Currently unused

[editor.soft-wrap] Section

Options for soft wrapping lines that exceed the view width:

enableWhether soft wrapping is enabled.false
max-wrapMaximum free space left at the end of the line.20
max-indent-retainMaximum indentation to carry over when soft wrapping a line.40
wrap-indicatorText inserted before soft wrapped lines, highlighted with ui.virtual.wrapโ†ช
wrap-at-text-widthSoft wrap at text-width instead of using the full viewport size.false


enable = true
max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = ""  # set wrap-indicator to "" to hide it

[editor.smart-tab] Section

Options for navigating and editing using tab key.

enableIf set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run move_parent_node_end. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab.true
supersede-menuNormally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the smart-tab command. If this option is set to true, the smart-tab command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or C-n/C-p.false

Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the Enhanced Keyboard protocol, consider setting extra keybindings:

tab = "move_parent_node_end"
S-tab = "move_parent_node_start"

S-tab = "move_parent_node_start"

tab = "extend_parent_node_end"
S-tab = "extend_parent_node_start"


To use a theme add theme = "<name>" to the top of your config.toml file, or select it during runtime using :theme <name>.

Creating a theme

Create a file with the name of your theme as the file name (i.e mytheme.toml) and place it in your themes directory (i.e ~/.config/helix/themes or %AppData%\helix\themes on Windows). The directory might have to be created beforehand.

๐Ÿ’ก The names "default" and "base16_default" are reserved for built-in themes and cannot be overridden by user-defined themes.


Each line in the theme file is specified as below:

key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }

Where key represents what you want to style, fg specifies the foreground color, bg the background color, underline the underline style/color, and modifiers is a list of style modifiers. bg, underline and modifiers can be omitted to defer to the defaults.

To specify only the foreground color:

key = "#ffffff"

If the key contains a dot '.', it must be quoted to prevent it being parsed as a dotted key.

"key.key" = "#ffffff"

For inspiration, you can find the default theme.toml here and user-submitted themes here.

The details of theme creation

Color palettes

It's recommended to define a palette of named colors, and refer to them in the configuration values in your theme. To do this, add a table called palette to your theme file:

"ui.background" = "white"
"ui.text" = "black"

white = "#ffffff"
black = "#000000"

Keep in mind that the [palette] table includes all keys after its header, so it should be defined after the normal theme options.

The default palette uses the terminal's default 16 colors, and the colors names are listed below. The [palette] section in the config file takes precedence over it and is merged into the default palette.

Color Name


The following values may be used as modifier, provided they are supported by your terminal emulator.


๐Ÿ’ก The underlined modifier is deprecated and only available for backwards compatibility. Its behavior is equivalent to setting underline.style="line".

Underline style

One of the following values may be used as a value for underline.style, providing it is supported by your terminal emulator.



Extend other themes by setting the inherits property to an existing theme.

inherits = "boo_berry"

# Override the theming for "keyword"s:
"keyword" = { fg = "gold" }

# Override colors in the palette:
berry = "#2A2A4D"


The following is a list of scopes available to use for styling:

Syntax highlighting

These keys match tree-sitter scopes.

When determining styling for a highlight, the longest matching theme key will be used. For example, if the highlight is function.builtin.static, the key function.builtin will be used instead of function.

We use a similar set of scopes as Sublime Text. See also TextMate scopes.

  • attribute - Class attributes, HTML tag attributes

  • type - Types

    • builtin - Primitive types provided by the language (int, usize)
    • parameter - Generic type parameters (T)
    • enum
      • variant
  • constructor

  • constant (TODO: constant.other.placeholder for %v)

    • builtin Special constants provided by the language (true, false, nil etc)
      • boolean
    • character
      • escape
    • numeric (numbers)
      • integer
      • float
  • string (TODO: string.quoted.{single, double}, string.raw/.unquoted)?

    • regexp - Regular expressions
    • special
      • path
      • url
      • symbol - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
  • comment - Code comments

    • line - Single line comments (//)
    • block - Block comments (e.g. (/* */)
      • documentation - Documentation comments (e.g. /// in Rust)
  • variable - Variables

    • builtin - Reserved language variables (self, this, super, etc.)
    • parameter - Function parameters
    • other
      • member - Fields of composite data types (e.g. structs, unions)
        • private - Private fields that use a unique syntax (currently just ECMAScript-based languages)
  • label

  • punctuation

    • delimiter - Commas, colons
    • bracket - Parentheses, angle brackets, etc.
    • special - String interpolation brackets.
  • keyword

    • control
      • conditional - if, else
      • repeat - for, while, loop
      • import - import, export
      • return
      • exception
    • operator - or, in
    • directive - Preprocessor directives (#if in C)
    • function - fn, func
    • storage - Keywords describing how things are stored
      • type - The type of something, class, function, var, let, etc.
      • modifier - Storage modifiers like static, mut, const, ref, etc.
  • operator - ||, +=, >

  • function

    • builtin
    • method
      • private - Private methods that use a unique syntax (currently just ECMAScript-based languages)
    • macro
    • special (preprocessor in C)
  • tag - Tags (e.g. <body> in HTML)

    • builtin
  • namespace

  • special

  • markup

    • heading
      • marker
      • 1, 2, 3, 4, 5, 6 - heading text for h1 through h6
    • list
      • unnumbered
      • numbered
      • checked
      • unchecked
    • bold
    • italic
    • strikethrough
    • link
      • url - URLs pointed to by links
      • label - non-URL link references
      • text - URL and image descriptions in links
    • quote
    • raw
      • inline
      • block
  • diff - version control changes

    • plus - additions
      • gutter - gutter indicator
    • minus - deletions
      • gutter - gutter indicator
    • delta - modifications
      • moved - renamed or moved files/changes
      • conflict - merge conflicts
      • gutter - gutter indicator


These scopes are used for theming the editor interface:

  • markup
    • normal
      • completion - for completion doc popup UI
      • hover - for hover popup UI
    • heading
      • completion - for completion doc popup UI
      • hover - for hover popup UI
    • raw
      • inline
        • completion - for completion doc popup UI
        • hover - for hover popup UI
ui.background.separatorPicker separator below input line
ui.cursor.matchMatching bracket etc.
ui.cursor.primaryCursor with primary selection
ui.debug.breakpointBreakpoint indicator, found in the gutter
ui.debug.activeIndicator for the line at which debugging execution is paused at, found in the gutter
ui.gutter.selectedGutter for the line the cursor is on
ui.highlight.framelineLine at which debugging execution is paused at
ui.linenrLine numbers
ui.linenr.selectedLine number for the line the cursor is on
ui.statusline.inactiveStatusline (unfocused document)
ui.statusline.normalStatusline mode during normal mode (only if editor.color-modes is enabled)
ui.statusline.insertStatusline mode during insert mode (only if editor.color-modes is enabled)
ui.statusline.selectStatusline mode during select mode (only if editor.color-modes is enabled)
ui.statusline.separatorSeparator character in statusline
ui.bufferlineStyle for the buffer line
ui.bufferline.activeStyle for the active buffer in buffer line
ui.bufferline.backgroundStyle for bufferline background
ui.popupDocumentation popups (e.g. Space + k)
ui.popup.infoPrompt for multiple key options
ui.windowBorderlines separating splits
ui.helpDescription box for commands
ui.textDefault text style, command prompts, popup text, etc.
ui.text.focusThe currently selected line in the picker
ui.text.inactiveSame as ui.text but when the text is inactive (e.g. suggestions)
ui.text.infoThe key: command text in ui.popup.info boxes
ui.virtual.rulerRuler columns (see the editor.rulers config)
ui.virtual.whitespaceVisible whitespace characters
ui.virtual.indent-guideVertical indent width guides
ui.virtual.inlay-hintDefault style for inlay hints of all kinds
ui.virtual.inlay-hint.parameterStyle for inlay hints of kind parameter (LSPs are not required to set a kind)
ui.virtual.inlay-hint.typeStyle for inlay hints of kind type (LSPs are not required to set a kind)
ui.virtual.wrapSoft-wrap indicator (see the editor.soft-wrap config)
ui.virtual.jump-labelStyle for virtual jump labels
ui.menuCode and command completion menus
ui.menu.selectedSelected autocomplete item
ui.menu.scrollfg sets thumb color, bg sets track color of scrollbar
ui.selectionFor selections in the editing area
ui.highlightHighlighted lines in the picker preview
ui.cursorline.primaryThe line of the primary cursor (if cursorline is enabled)
ui.cursorline.secondaryThe lines of any other cursors (if cursorline is enabled)
ui.cursorcolumn.primaryThe column of the primary cursor (if cursorcolumn is enabled)
ui.cursorcolumn.secondaryThe columns of any other cursors (if cursorcolumn is enabled)
warningDiagnostics warning (gutter)
errorDiagnostics error (gutter)
infoDiagnostics info (gutter)
hintDiagnostics hint (gutter)
diagnosticDiagnostics fallback style (editing area)
diagnostic.hintDiagnostics hint (editing area)
diagnostic.infoDiagnostics info (editing area)
diagnostic.warningDiagnostics warning (editing area)
diagnostic.errorDiagnostics error (editing area)
diagnostic.unnecessaryDiagnostics with unnecessary tag (editing area)
diagnostic.deprecatedDiagnostics with deprecated tag (editing area)

Key remapping

Helix currently supports one-way key remapping through a simple TOML configuration file. (More powerful solutions such as rebinding via commands will be available in the future).

To remap keys, create a config.toml file in your helix configuration directory (default ~/.config/helix on Linux systems) with a structure like this:

# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
C-s = ":w" # Maps Ctrl-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps Ctrl-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Ctrl-Shift-Escape to extend_line
g = { a = "code_action" } # Maps `ga` to show possible code actions
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode

"A-x" = "normal_mode"     # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert mode

Minor modes

Minor modes are accessed by pressing a key (usually from normal mode), giving access to dedicated bindings. Bindings can be modified or added by nesting definitions.

k = "normal_mode" # Maps `jk` to exit insert mode

a = "code_action" # Maps `ga` to show possible code actions

# invert `j` and `k` in view mode
j = "scroll_up"
k = "scroll_down"

# create a new minor mode bound to `+`
m = ":run-shell-command make"
c = ":run-shell-command cargo build"
t = ":run-shell-command cargo test"

Special keys and modifiers

Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes C-, S- and A-. Special keys are encoded as follows:

Key nameRepresentation
Page Up"pageup"
Page Down"pagedown"

Keys can be disabled by binding them to the no_op command.

A list of commands is available in the Keymap documentation and in the source code at helix-term/src/commands.rs at the invocation of static_commands! macro and the TypableCommandList.


Language-specific settings and settings for language servers are configured in languages.toml files.

languages.toml files

There are three possible locations for a languages.toml file:

  1. In the Helix source code, which lives in the Helix repository. It provides the default configurations for languages and language servers.

  2. In your configuration directory. This overrides values from the built-in language configuration. For example, to disable auto-LSP-formatting in Rust:

    # in <config_dir>/helix/languages.toml
    command = "mylang-lsp"
    name = "rust"
    auto-format = false
  3. In a .helix folder in your project. Language configuration may also be overridden local to a project by creating a languages.toml file in a .helix folder. Its settings will be merged with the language configuration in the configuration directory and the built-in configuration.

Language configuration

Each language is configured by adding a [[language]] section to a languages.toml file. For example:

name = "mylang"
scope = "source.mylang"
injection-regex = "mylang"
file-types = ["mylang", "myl"]
comment-tokens = "#"
indent = { tab-width = 2, unit = "  " }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]

These configuration keys are available:

nameThe name of the language
language-idThe language-id for language servers, checkout the table at TextDocumentItem for the right id
scopeA string like source.js that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually source.<name> or text.<name> in case of markup languages
injection-regexregex pattern that will be tested against a language name in order to determine whether this language should be used for a potential language injection site.
file-typesThe filetypes of the language, for example ["yml", "yaml"]. See the file-type detection section below.
shebangsThe interpreters from the shebang line, for example ["sh", "bash"]
rootsA set of marker files to look for when trying to find the workspace root. For example Cargo.lock, yarn.lock
auto-formatWhether to autoformat this language when saving
diagnostic-severityMinimal severity of diagnostic for it to be displayed. (Allowed values: Error, Warning, Info, Hint)
comment-tokensThe tokens to use as a comment token, either a single token "//" or an array ["//", "///", "//!"] (the first token will be used for commenting). Also configurable as comment-token for backwards compatibility
block-comment-tokensThe start and end tokens for a multiline comment either an array or single table of { start = "/*", end = "*/"}. The first set of tokens will be used for commenting, any pairs in the array can be uncommented
indentThe indent to use. Has sub keys unit (the text inserted into the document when indenting; usually set to N spaces or "\t" for tabs) and tab-width (the number of spaces rendered for a tab)
language-serversThe Language Servers used for this language. See below for more information in the section Configuring Language Servers for a language
grammarThe tree-sitter grammar to use (defaults to the value of name)
formatterThe formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout
text-widthMaximum line length. Used for the :reflow command and soft-wrapping if soft-wrap.wrap-at-text-width is set, defaults to editor.text-width
workspace-lsp-rootsDirectories relative to the workspace root that are treated as LSP roots. Should only be set in .helix/config.toml. Overwrites the setting of the same name in config.toml if set.
persistent-diagnostic-sourcesAn array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.

File-type detection and the file-types key

Helix determines which language configuration to use based on the file-types key from the above section. file-types is a list of strings or tables, for example:

file-types = ["toml", { glob = "Makefile" }, { glob = ".git/config" }, { glob = ".github/workflows/*.yaml" } ]

When determining a language configuration to use, Helix searches the file-types with the following priorities:

  1. Glob: values in glob tables are checked against the full path of the given file. Globs are standard Unix-style path globs (e.g. the kind you use in Shell) and can be used to match paths for a specific prefix, suffix, directory, etc. In the above example, the { glob = "Makefile" } config would match files with the name Makefile, the { glob = ".git/config" } config would match config files in .git directories, and the { glob = ".github/workflows/*.yaml" } config would match any yaml files in .github/workflow directories. Note that globs should always use the Unix path separator / even on Windows systems; the matcher will automatically take the machine-specific separators into account. If the glob isn't an absolute path or doesn't already start with a glob prefix, */ will automatically be added to ensure it matches for any subdirectory.
  2. Extension: if there are no glob matches, any file-types string that matches the file extension of a given file wins. In the example above, the "toml" config matches files like Cargo.toml or languages.toml.

Language Server configuration

Language servers are configured separately in the table language-server in the same file as the languages languages.toml

For example:

command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
environment = { "ENV1" = "value1", "ENV2" = "value2" }

command = "efm-langserver"

documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }

These are the available options for a language server.

commandThe name or path of the language server binary to execute. Binaries must be in $PATH
argsA list of arguments to pass to the language server binary
configLSP initialization options
timeoutThe maximum time a request to the language server may take, in seconds. Defaults to 20
environmentAny environment variables that will be used when starting the language server { "KEY1" = "Value1", "KEY2" = "Value2" }
required-root-patternsA list of glob patterns to look for in the working directory. The language server is started if at least one of them is found.

A format sub-table within config can be used to pass extra formatting options to Document Formatting Requests. For example, with typescript:

# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }

Configuring Language Servers for a language

The language-servers attribute in a language tells helix which language servers are used for this language.

They have to be defined in the [language-server] table as described in the previous section.

Different languages can use the same language server instance, e.g. typescript-language-server is used for javascript, jsx, tsx and typescript by default.

The definition order of language servers affects the order in the results list of code action menu.

In case multiple language servers are specified in the language-servers attribute of a language, it's often useful to only enable/disable certain language-server features for these language servers.

As an example, efm-lsp-prettier of the previous example is used only with a formatting command prettier, so everything else should be handled by the typescript-language-server (which is configured by default). The language configuration for typescript could look like this:

name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]

or equivalent:

name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]

Each requested LSP feature is prioritized in the order of the language-servers array. For example, the first goto-definition supported language server (in this case typescript-language-server) will be taken for the relevant LSP request (command goto_definition). The features diagnostics, code-action, completion, document-symbols and workspace-symbols are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language. If no except-features or only-features is given, all features for the language server are enabled. If a language server itself doesn't support a feature, the next language server array entry will be tried (and so on).

The list of supported features is:

  • format
  • goto-definition
  • goto-declaration
  • goto-type-definition
  • goto-reference
  • goto-implementation
  • signature-help
  • hover
  • document-highlight
  • completion
  • code-action
  • workspace-command
  • document-symbols
  • workspace-symbols
  • diagnostics
  • rename-symbol
  • inlay-hints

Tree-sitter grammar configuration

The source for a language's tree-sitter grammar is specified in a [[grammar]] section in languages.toml. For example:

name = "mylang"
source = { git = "https://github.com/example/mylang", rev = "a250c4582510ff34767ec3b7dcdd3c24e8c8aa68" }

Grammar configuration takes these keys:

nameThe name of the tree-sitter grammar
sourceThe method of fetching the grammar - a table with a schema defined below

Where source is a table with either these keys when using a grammar from a git repository:

gitA git remote URL from which the grammar should be cloned
revThe revision (commit hash or tag) which should be fetched
subpathA path within the grammar directory which should be built. Some grammar repositories host multiple grammars (for example tree-sitter-typescript and tree-sitter-ocaml) in subdirectories. This key is used to point hx --grammar build to the correct path for compilation. When omitted, the root of repository is used

Choosing grammars

You may use a top-level use-grammars key to control which grammars are fetched and built when using hx --grammar fetch and hx --grammar build.

# Note: this key must come **before** the [[language]] and [[grammar]] sections
use-grammars = { only = [ "rust", "c", "cpp" ] }
# or
use-grammars = { except = [ "yaml", "json" ] }

When omitted, all grammars are fetched and built.


This section contains guides for adding new language server configurations, tree-sitter grammars, textobject queries, and other similar items.

Adding new languages to Helix

In order to add a new language to Helix, you will need to follow the steps below.

Language configuration

  1. Add a new [[language]] entry in the languages.toml file and provide the necessary configuration for the new language. For more information on language configuration, refer to the language configuration section of the documentation. A new language server can be added by extending the [language-server] table in the same file.
  2. If you are adding a new language or updating an existing language server configuration, run the command cargo xtask docgen to update the Language Support documentation.

๐Ÿ’ก If you are adding a new Language Server configuration, make sure to update the Language Server Wiki with the installation instructions.

Grammar configuration

  1. If a tree-sitter grammar is available for the new language, add a new [[grammar]] entry to the languages.toml file.
  2. If you are testing the grammar locally, you can use the source.path key with an absolute path to the grammar. However, before submitting a pull request, make sure to switch to using source.git.


  1. In order to provide syntax highlighting and indentation for the new language, you will need to add queries.
  2. Create a new directory for the language with the path runtime/queries/<name>/.
  3. Refer to the tree-sitter website for more information on writing queries.
  4. A list of highlight captures can be found on the themes page.

๐Ÿ’ก In Helix, the first matching query takes precedence when evaluating queries, which is different from other editors such as Neovim where the last matching query supersedes the ones before it. See this issue for an example.

Common issues

  • If you encounter errors when running Helix after switching branches, you may need to update the tree-sitter grammars. Run the command hx --grammar fetch to fetch the grammars and hx --grammar build to build any out-of-date grammars.
  • If a parser is causing a segfault, or you want to remove it, make sure to remove the compiled parser located at runtime/grammars/<name>.so.
  • If you are attempting to add queries and Helix is unable to locate them, ensure that the environment variable HELIX_RUNTIME is set to the location of the runtime folder you're developing in.

Adding textobject queries

Helix supports textobjects that are language specific, such as functions, classes, etc. These textobjects require an accompanying tree-sitter grammar and a textobjects.scm query file to work properly. Tree-sitter allows us to query the source code syntax tree and capture specific parts of it. The queries are written in a lisp dialect. More information on how to write queries can be found in the official tree-sitter documentation.

Query files should be placed in runtime/queries/{language}/textobjects.scm when contributing to Helix. Note that to test the query files locally you should put them under your local runtime directory (~/.config/helix/runtime on Linux for example).

The following captures are recognized:

Capture Name

Example query files can be found in the helix GitHub repository.

Queries for textobject based navigation

Tree-sitter based navigation in Helix is done using captures in the following order:

  • object.movement
  • object.around
  • object.inside

For example if a function.around capture has been already defined for a language in its textobjects.scm file, function navigation should also work automatically. function.movement should be defined only if the node captured by function.around doesn't make sense in a navigation context.

Adding indent queries

Helix uses tree-sitter to correctly indent new lines. This requires a tree- sitter grammar and an indent.scm query file placed in runtime/queries/ {language}/indents.scm. The indentation for a line is calculated by traversing the syntax tree from the lowest node at the beginning of the new line (see Indent queries). Each of these nodes contributes to the total indent when it is captured by the query (in what way depends on the name of the capture.

Note that it matters where these added indents begin. For example, multiple indent level increases that start on the same line only increase the total indent level by 1. See Capture types.

By default, Helix uses the hybrid indentation heuristic. This means that indent queries are not used to compute the expected absolute indentation of a line but rather the expected difference in indentation between the new and an already existing line. This difference is then added to the actual indentation of the already existing line. Since this makes errors in the indent queries harder to find, it is recommended to disable it when testing via :set indent-heuristic tree-sitter. The rest of this guide assumes that the tree-sitter heuristic is used.

Indent queries

When Helix is inserting a new line through o, O, or <ret>, to determine the indent level for the new line, the query in indents.scm is run on the document. The starting position of the query is the end of the line above where a new line will be inserted.

For o, the inserted line is the line below the cursor, so that starting position of the query is the end of the current line.

fn main() {
fn need_hero(some_hero: Hero, life: Life) -> {
    matches!(some_hero, Hero { // โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
        strong: true,//โ†โ•ฎ  โ†‘  โ†‘                     โ”‚
        fast: true,  // โ”‚  โ”‚  โ•ฐโ”€โ”€ query start       โ”‚
        sure: true,  // โ”‚  โ•ฐโ”€โ”€โ”€โ”€โ”€ cursor            โ”œโ”€ traversal 
        soon: true,  // โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ new line inserted โ”‚  start node
    }) &&            //                             โ”‚
//  โ†‘                                               โ”‚
//  โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
    some_hero > life

For O, the newly inserted line is the current line, so the starting position of the query is the end of the line above the cursor.

fn main() {
fn need_hero(some_hero: Hero, life: Life) -> { // โ†โ”€โ•ฎ
    matches!(some_hero, Hero { // โ†โ•ฎ          โ†‘     โ”‚
        strong: true,//    โ†‘   โ•ญโ”€โ”€โ”€โ•ฏ          โ”‚     โ”‚
        fast: true,  //    โ”‚   โ”‚ query start โ”€โ•ฏ     โ”‚
        sure: true,  //    โ•ฐโ”€โ”€โ”€โ”ผ cursor             โ”œโ”€ traversal
        soon: true,  //        โ•ฐ new line inserted  โ”‚  start node
    }) &&            //                             โ”‚
    some_hero > life //                             โ”‚
} // โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

From this starting node, the syntax tree is traversed up until the root node. Each indent capture is collected along the way, and then combined according to their capture types and scopes to a final indent level for the line.

Capture types

  • @indent (default scope tail): Increase the indent level by 1. Multiple occurrences in the same line do not stack. If there is at least one @indent and one @outdent capture on the same line, the indent level isn't changed at all.
  • @outdent (default scope all): Decrease the indent level by 1. The same rules as for @indent apply.
  • @indent.always (default scope tail): Increase the indent level by 1. Multiple occurrences on the same line do stack. The final indent level is @indent.always โ€“ @outdent.always. If an @indent and an @indent.always are on the same line, the @indent is ignored.
  • @outdent.always (default scope all): Decrease the indent level by 1. The same rules as for @indent.always apply.
  • @align (default scope all): Align everything inside this node to some anchor. The anchor is given by the start of the node captured by @anchor in the same pattern. Every pattern with an @align should contain exactly one @anchor. Indent (and outdent) for nodes below (in terms of their starting line) the @align node is added to the indentation required for alignment.
  • @extend: Extend the range of this node to the end of the line and to lines that are indented more than the line that this node starts on. This is useful for languages like Python, where for the purpose of indentation some nodes (like functions or classes) should also contain indented lines that follow them.
  • @extend.prevent-once: Prevents the first extension of an ancestor of this node. For example, in Python a return expression always ends the block that it is in. Note that this only stops the extension of the next @extend capture. If multiple ancestors are captured, only the extension of the innermost one is prevented. All other ancestors are unaffected (regardless of whether the innermost ancestor would actually have been extended).

@indent / @outdent

Consider this example:

fn main() {
fn shout(things: Vec<Thing>) {
    //                       โ†‘
    //                       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ indent level
    //                    @indent                    โ”œโ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„
    //                                               โ”‚
    let it_all = |out| { things.filter(|thing| { //  โ”‚      1
    //                 โ†‘                       โ†‘     โ”‚
    //                 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„
    //              @indent                 @indent  โ”‚
    //                                               โ”‚      2
        thing.can_do_with(out) //                    โ”‚
    })}; //                                          โ”œโ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„
  //โ†‘โ†‘โ†‘                                              โ”‚      1
} //โ•ฐโ”ผโ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„
// 3x @outdent
((block) @indent)
["}" ")"] @outdent

Note how on the second line, we have two blocks begin on the same line. In this case, since both captures occur on the same line, they are combined and only result in a net increase of 1. Also note that the closing }s are part of the @indent captures, but the 3 @outdents also combine into 1 and result in that line losing one indent level.

@extend / @extend.prevent-once

For an example of where @extend can be useful, consider Python, which is whitespace-sensitive.

] @indent

class Hero:
    def __init__(self, strong, fast, sure, soon):#  โ†โ”€โ•ฎ
        self.is_strong = strong #                     โ”‚
        self.is_fast = fast     # โ•ญโ”€โ”€โ”€ query start    โ”‚
        self.is_sure = sure     # โ”‚ โ•ญโ”€ cursor         โ”‚
        self.is_soon = soon     # โ”‚ โ”‚                 โ”‚
        #     โ†‘            โ†‘      โ”‚ โ”‚                 โ”‚
        #     โ”‚            โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚                 โ”‚
        #     โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ                 โ”‚
        #                                             โ”œโ”€ traversal
    def need_hero(self, life):         #              โ”‚  start node
        return (                       #              โ”‚
            self.is_strong             #              โ”‚
            and self.is_fast           #              โ”‚
            and self.is_sure           #              โ”‚
            and self.is_soon           #              โ”‚
            and self > life            #              โ”‚
        ) # โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Without braces to catch the scope of the function, the smallest descendant of the cursor on a line feed ends up being the entire inside of the class. Because of this, it will miss the entire function node and its indent capture, leading to an indent level one too small.

To address this case, @extend tells helix to "extend" the captured node's span to the line feed and every consecutive line that has a greater indent level than the line of the node.

(parenthesized_expression) @indent

] @indent @extend

class Hero:
    def __init__(self, strong, fast, sure, soon):#  โ†โ”€โ•ฎ
        self.is_strong = strong #                     โ”‚
        self.is_fast = fast     # โ•ญโ”€โ”€โ”€ query start    โ”œโ”€ traversal
        self.is_sure = sure     # โ”‚ โ•ญโ”€ cursor         โ”‚  start node
        self.is_soon = soon     # โ”‚ โ”‚ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
        #     โ†‘            โ†‘      โ”‚ โ”‚                 
        #     โ”‚            โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚
        #     โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
    def need_hero(self, life):
        return (
            and self.is_fast
            and self.is_sure
            and self.is_soon
            and self > life

Furthermore, there are some cases where extending to everything with a greater indent level may not be desirable. Consider the need_hero function above. If our cursor is on the last line of the returned expression.

class Hero:
    def __init__(self, strong, fast, sure, soon):
        self.is_strong = strong
        self.is_fast = fast
        self.is_sure = sure
        self.is_soon = soon

    def need_hero(self, life):
        return (
            and self.is_fast
            and self.is_sure
            and self.is_soon
            and self > life
        ) # โ†โ”€โ”€โ”€ cursor
    #โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ where cursor should go on new line

In Python, the are a few tokens that will always end a scope, such as a return statement. Since the scope ends, so should the indent level. But because the function span is extended to every line with a greater indent level, a new line would just continue on the same level. And an @outdent would not help us here either, since it would cause everything in the parentheses to become outdented as well.

To help, we need to signal an end to the extension. We can do this with @extend.prevent-once.

(parenthesized_expression) @indent

] @indent @extend

(return_statement) @extend.prevent-once

@indent.always / @outdent.always

As mentioned before, normally if there is more than one @indent or @outdent capture on the same line, they are combined.

Sometimes, there are cases when you may want to ensure that every indent capture is additive, regardless of how many occur on the same line. Consider this example in YAML.

  - foo: bar
# โ†‘ โ†‘
# โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ start of map
# โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ start of list element
    baz: quux # โ†โ”€โ”€โ”€ cursor
    # โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ where the cursor should go on a new line
    garply: waldo
  - quux:
      bar: baz
    xyzzy: thud
    fred: plugh

In YAML, you often have lists of maps. In these cases, the syntax is such that the list element and the map both start on the same line. But we really do want to start an indentation for each of these so that subsequent keys in the map hang over the list and align properly. This is where @indent.always helps.

((block_sequence_item) @item @indent.always @extend
  (#not-one-line? @item))

    key: (_) @key
    value: (_) @val
    (#not-same-line? @key @val)
  ) @indent.always @extend


In some cases, an S-expression cannot express exactly what pattern should be matched. For that, tree-sitter allows for predicates to appear anywhere within a pattern, similar to how #set! declarations work:

  (child_kind) @indent
  (#predicate? arg1 arg2 ...)

The number of arguments depends on the predicate that's used. Each argument is either a capture (@name) or a string ("some string"). The following predicates are supported by tree-sitter:

  • #eq?/#not-eq?: The first argument (a capture) must/must not be equal to the second argument (a capture or a string).

  • #match?/#not-match?: The first argument (a capture) must/must not match the regex given in the second argument (a string).

  • #any-of?/#not-any-of?: The first argument (a capture) must/must not be one of the other arguments (strings).

Additionally, we support some custom predicates for indent queries:

  • #not-kind-eq?: The kind of the first argument (a capture) must not be equal to the second argument (a string).

  • #same-line?/#not-same-line?: The captures given by the 2 arguments must/must not start on the same line.

  • #one-line?/#not-one-line?: The captures given by the fist argument must/must span a total of one line.


Added indents don't always apply to the whole node. For example, in most cases when a node should be indented, we actually only want everything except for its first line to be indented. For this, there are several scopes (more scopes may be added in the future if required):

  • tail: This scope applies to everything except for the first line of the captured node.
  • all: This scope applies to the whole captured node. This is only different from tail when the captured node is the first node on its line.

For example, imagine we have the following function

fn main() {
fn aha() { // โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
  let take = "on me";  // โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ  scope:  โ”‚
  let take = "me on";             //     โ”œโ”€ "tail"  โ”œโ”€ (block) @indent
  let ill = be_gone_days(1 || 2); //     โ”‚          โ”‚
} // โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€ "}" @outdent
                                         //                scope: "all"

We can write the following query with the #set! declaration:

((block) @indent
 (#set! "scope" "tail"))
("}" @outdent
 (#set! "scope" "all"))

As we can see, the "tail" scope covers the node, except for the first line. Everything up to and including the closing brace gets an indent level of 1. Then, on the closing brace, we encounter an outdent with a scope of "all", which means the first line is included, and the indent level is cancelled out on this line. (Note these scopes are the defaults for @indent and @outdentโ€”they are written explicitly for demonstration.)

Adding Injection Queries

Writing language injection queries allows one to highlight a specific node as a different language. In addition to the standard language injection options used by tree-sitter, there are a few Helix specific extensions that allow for more control.

And example of a simple query that would highlight all strings as bash in Nix:

((string_expression (string_fragment) @injection.content)
  (#set! injection.language "bash"))

Capture Types

  • @injection.language (standard): The captured node may contain the language name used to highlight the node captured by @injection.content.

  • @injection.content (standard): Marks the content to be highlighted as the language captured with @injection.language et al.

  • @injection.filename (extension): The captured node may contain a filename with a file-extension known to Helix, highlighting @injection.content as that language. This uses the language extensions defined in both the default languages.toml distributed with Helix, as well as user defined languages.

  • @injection.shebang (extension): The captured node may contain a shebang used to choose a language to highlight as. This also uses the shebangs defined in the default and user languages.toml.


  • injection.combined (standard): Indicates that all the matching nodes in the tree should have their content parsed as one nested document.

  • injection.language (standard): Forces the captured content to be highlighted as the given language

  • injection.include-children (standard): Indicates that the content nodeโ€™s entire text should be re-parsed, including the text of its child nodes. By default, child nodesโ€™ text will be excluded from the injected document.

  • injection.include-unnamed-children (extension): Same as injection.include-children but only for unnamed child nodes.


  • #eq? (standard): The first argument (a capture) must be equal to the second argument (a capture or a string).

  • #match? (standard): The first argument (a capture) must match the regex given in the second argument (a string).

  • #any-of? (standard): The first argument (a capture) must be one of the other arguments (strings).