Helix

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 binary to your system's $PATH to use it from the command line.

Linux, macOS, Windows and OpenBSD packaging status

Packaging status

Linux

The following third party repositories are available:

Ubuntu

Add the PPA for Helix:

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

Fedora/RHEL

Enable the COPR repository for Helix:

sudo dnf copr enable varlad/helix
sudo dnf install helix

Arch Linux extra

Releases are available in the extra repository:

sudo pacman -S helix

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

NixOS

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.

Flatpak

Helix is available on Flathub:

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

Snap

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.

AppImage

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

macOS

Homebrew Core

brew install helix

Windows

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

Winget

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

scoop install helix

Chocolatey

choco install helix

MSYS2

For 64-bit Windows 8.1 or above:

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

Building from source

Requirements:

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.

๐Ÿ’ก 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 set a HELIX_RUNTIME environment variable to point to that directory and add it to your ~/.bashrc or equivalent:

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.

Windows

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:

MethodCommand
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.
  3. $HELIX_RUNTIME.
  4. 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.

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.

Registers

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.

Surround

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
wWord
WWORD
pParagraph
(, [, ', etc.Specified surround pairs
mThe closest surround pair
fFunction
tType (or Class)
aArgument/parameter
cComment
TTest
gChange

๐Ÿ’ก 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 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.

Tree-sitter-nav-demo

For the full reference see the unimpaired 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.

(call
  function: (identifier) ; func
  arguments:
    (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:

            โ”Œโ”€โ”€โ”€โ”€โ”
            โ”‚callโ”‚
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
      โ”‚                โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”
โ”‚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.

Keymap

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

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

Normal mode

Normal mode is the default mode when you launch helix. Return to it from other modes by typing Escape.

Movement

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

KeyDescriptionCommand
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 or m)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 half page uphalf_page_up
Ctrl-dMove half page downhalf_page_down
Ctrl-iJump forward on the jumplistjump_forward
Ctrl-oJump backward on the jumplistjump_backward
Ctrl-sSave the current selection to the jumplistsave_selection

Changes

KeyDescriptionCommand
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 (currently nonfunctional/disabled) (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

Shell

KeyDescriptionCommand
|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

KeyDescriptionCommand
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>.

KeyDescriptionCommand
/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.

KeyDescriptionCommand
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.

KeyDescriptionCommand
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-dMove half page downhalf_page_down
Ctrl-uMove half page uphalf_page_up

Goto mode

Accessed by typing g in normal mode.

Jumps to various locations.

KeyDescriptionCommand
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 selectiongoto_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

Match mode

Accessed by typing m in normal mode.

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

KeyDescriptionCommand
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.

KeyDescriptionCommand
w, Ctrl-wSwitch to next windowrotate_view
v, Ctrl-vVertical right splitvsplit
s, Ctrl-sHorizontal bottom splithsplit
fGo to files in the selection in horizontal splitsgoto_file
FGo to files in the selection 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.

KeyDescriptionCommand
fOpen file pickerfile_picker
FOpen file picker at current working directoryfile_picker_in_current_directory
bOpen buffer pickerbuffer_picker
jOpen jumplist pickerjumplist_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
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.

KeyDescription
Ctrl-uScroll up
Ctrl-dScroll down

Unimpaired

These mappings are in the style of vim-unimpaired.

KeyDescriptionCommand
]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.

KeyDescriptionCommand
EscapeSwitch to normal modenormal_mode
Ctrl-sCommit undo checkpointcommit_undo_checkpoint
Ctrl-xAutocompletecompletion
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.

KeyDescriptionCommand
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.

[keys.insert]
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.

Picker

Keys to use within picker. Remapping currently not supported.

KeyDescription
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

Prompt

Keys to use within prompt, Remapping currently not supported.

KeyDescription
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

Commands

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

NameDescription
: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-8 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.
: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.

Language Support

The following languages and Language Servers are supported. To use Language Server features, you must first install 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
astroโœ“
awkโœ“โœ“awk-language-server
bashโœ“โœ“โœ“bash-language-server
bassโœ“bass
beancountโœ“
bibtexโœ“texlab
bicepโœ“bicep-langserver
blueprintโœ“blueprint-compiler
cโœ“โœ“โœ“clangd
c-sharpโœ“โœ“OmniSharp
cabal
cairoโœ“โœ“โœ“cairo-language-server
capnpโœ“โœ“
clojureโœ“clojure-lsp
cmakeโœ“โœ“โœ“cmake-language-server
commentโœ“
common-lispโœ“cl-lsp
cponโœ“โœ“
cppโœ“โœ“โœ“clangd
crystalโœ“โœ“crystalline
cssโœ“vscode-css-language-server
cueโœ“cuelsp
dโœ“โœ“โœ“serve-d
dartโœ“โœ“dart
devicetreeโœ“
dhallโœ“โœ“dhall-lsp-server
diffโœ“
dockerfileโœ“docker-langserver
dotโœ“dot-language-server
dtdโœ“
edocโœ“
eexโœ“
ejsโœ“
elixirโœ“โœ“โœ“elixir-ls
elmโœ“โœ“elm-language-server
elvishโœ“elvish
envโœ“
erbโœ“
erlangโœ“โœ“erlang_ls
esdlโœ“
fishโœ“โœ“โœ“
forthโœ“forth-lsp
fortranโœ“โœ“fortls
fsharpโœ“fsautocomplete
gdscriptโœ“โœ“โœ“
geminiโœ“
git-attributesโœ“
git-commitโœ“โœ“
git-configโœ“
git-ignoreโœ“
git-rebaseโœ“
gleamโœ“โœ“gleam
glslโœ“โœ“โœ“
goโœ“โœ“โœ“gopls
godot-resourceโœ“
gomodโœ“gopls
gotmplโœ“gopls
goworkโœ“gopls
graphqlโœ“
hareโœ“
haskellโœ“โœ“haskell-language-server-wrapper
haskell-persistentโœ“
hclโœ“โœ“terraform-ls
heexโœ“โœ“elixir-ls
hostsโœ“
htmlโœ“vscode-html-language-server
hurlโœ“โœ“
idrisidris2-lsp
iexโœ“
iniโœ“
javaโœ“โœ“โœ“jdtls
javascriptโœ“โœ“โœ“typescript-language-server
jinjaโœ“
jsdocโœ“
jsonโœ“โœ“vscode-json-language-server
jsonnetโœ“jsonnet-language-server
jsxโœ“โœ“โœ“typescript-language-server
juliaโœ“โœ“โœ“julia
justโœ“โœ“โœ“
kdlโœ“
kotlinโœ“kotlin-language-server
latexโœ“โœ“texlab
leanโœ“lean
ledgerโœ“
llvmโœ“โœ“โœ“
llvm-mirโœ“โœ“โœ“
llvm-mir-yamlโœ“โœ“
luaโœ“โœ“โœ“lua-language-server
makeโœ“
markdocโœ“markdoc-ls
markdownโœ“marksman
markdown.inlineโœ“
matlabโœ“โœ“โœ“
mermaidโœ“
mesonโœ“โœ“
mintmint
msbuildโœ“โœ“
nasmโœ“โœ“
nickelโœ“โœ“nls
nimโœ“โœ“โœ“nimlangserver
nixโœ“nil
nuโœ“
nunjucksโœ“
ocamlโœ“โœ“ocamllsp
ocaml-interfaceโœ“ocamllsp
odinโœ“โœ“ols
openclโœ“โœ“โœ“clangd
openscadโœ“openscad-lsp
orgโœ“
pascalโœ“โœ“pasls
passwdโœ“
pemโœ“
perlโœ“perlnavigator
phpโœ“โœ“โœ“intelephense
poโœ“โœ“
podโœ“
ponylangโœ“โœ“โœ“
prismaโœ“prisma-language-server
prologswipl
protobufโœ“โœ“bufls, pb
prqlโœ“
purescriptโœ“purescript-language-server
pythonโœ“โœ“โœ“pylsp
qmlโœ“โœ“qmlls
rโœ“R
racketโœ“racket
regexโœ“
regoโœ“regols
rescriptโœ“โœ“rescript-language-server
rmarkdownโœ“โœ“R
robotโœ“robotframework_ls
ronโœ“โœ“
rstโœ“
rubyโœ“โœ“โœ“solargraph
rustโœ“โœ“โœ“rust-analyzer
sageโœ“โœ“
scalaโœ“โœ“metals
schemeโœ“
scssโœ“vscode-css-language-server
slintโœ“โœ“slint-lsp
smithyโœ“cs
smlโœ“
solidityโœ“solc
sqlโœ“
sshclientconfigโœ“
starlarkโœ“โœ“
straceโœ“
svelteโœ“โœ“svelteserver
swayโœ“โœ“โœ“forc
swiftโœ“sourcekit-lsp
t32โœ“
tablegenโœ“โœ“โœ“
taskโœ“
tfvarsโœ“โœ“terraform-ls
todotxtโœ“
tomlโœ“taplo
tsqโœ“
tsxโœ“โœ“โœ“typescript-language-server
twigโœ“
typescriptโœ“โœ“โœ“typescript-language-server
ungrammarโœ“
unisonโœ“
uxntalโœ“
vโœ“โœ“โœ“v-analyzer
valaโœ“vala-language-server
verilogโœ“โœ“svlangserver
vhdlโœ“vhdl_ls
vhsโœ“
vueโœ“vue-language-server
wastโœ“
watโœ“
webcโœ“
wgslโœ“wgsl_analyzer
witโœ“โœ“
wrenโœ“โœ“โœ“
xitโœ“
xmlโœ“โœ“
yamlโœ“โœ“yaml-language-server
yuckโœ“
zigโœ“โœ“โœ“zls

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

Configuration

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"

[editor]
line-number = "relative"
mouse = false

[editor.cursor-shape]
insert = "bar"
normal = "block"
select = "underline"

[editor.file-picker]
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

[editor] Section

KeyDescriptionDefault
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. Used for autocompletion, set to 0 for instant400
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

[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:

[editor.statusline]
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:

KeyDescriptionDefault
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:

KeyDescription
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-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

KeyDescriptionDefault
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
1

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

2

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.

KeyDescriptionDefault
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.

KeyDescriptionDefault
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.excludefile optiontrue
git-excludeEnables reading .git/info/exclude filestrue
max-depthSet with an integer value for maximum depth to recurseDefaults to None.

[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:

[editor]
auto-pairs = false # defaults to `true`

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

[editor.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'

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 ''

[[language]]
name = "rust"

[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'

[editor.search] Section

Search specific options.

KeyDescriptionDefault
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.

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

Example

[editor.whitespace]
render = "all"
# or control each character
[editor.whitespace.render]
space = "all"
tab = "all"
newline = "none"

[editor.whitespace.characters]
space = "ยท"
nbsp = "โฝ"
tab = "โ†’"
newline = "โŽ"
tabpad = "ยท" # Tabs will look like "โ†’ยทยทยท" (depending on tab width)

[editor.indent-guides] Section

Options for rendering vertical indent guides.

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

Example:

[editor.indent-guides]
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.

[editor]
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.

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

Example:

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

[editor.gutters.line-numbers] Section

Options for the line number gutter

KeyDescriptionDefault
min-widthThe minimum number of characters to use3

Example:

[editor.gutters.line-numbers]
min-width = 1

[editor.gutters.diagnostics] Section

Currently unused

[editor.gutters.diff] Section

Currently unused

[editor.gutters.spacer] Section

Currently unused

[editor.soft-wrap] Section

Options for soft wrapping lines that exceed the view width:

KeyDescriptionDefault
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

Example:

[editor.soft-wrap]
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

KeyDescriptionDefault
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

Themes

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.

Overview

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.

Using the linter

Use the supplied linting tool to check for errors and missing scopes:

cargo xtask themelint onedark # replace onedark with <name>

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"

[palette]
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
reset
black
red
green
yellow
blue
magenta
cyan
gray
light-red
light-green
light-yellow
light-blue
light-magenta
light-cyan
light-gray
white

Modifiers

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

Modifier
bold
dim
italic
underlined
slow_blink
rapid_blink
reversed
hidden
crossed_out

๐Ÿ’ก 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.

Modifier
line
curl
dashed
dotted
double_line

Inheritance

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:
[palette]
berry = "#2A2A4D"

Scopes

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)
    • 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)
  • 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
    • 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
    • minus - deletions
    • delta - modifications
      • moved - renamed or moved files/changes

Interface

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
KeyNotes
ui.background
ui.background.separatorPicker separator below input line
ui.cursor
ui.cursor.normal
ui.cursor.insert
ui.cursor.select
ui.cursor.matchMatching bracket etc.
ui.cursor.primaryCursor with primary selection
ui.cursor.primary.normal
ui.cursor.primary.insert
ui.cursor.primary.select
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.gutterGutter
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.statuslineStatusline
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.popupDocumentation popups (e.g. Space + k)
ui.popup.infoPrompt for multiple key options
ui.windowBorderlines separating splits
ui.helpDescription box for commands
ui.textCommand 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.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.selection.primary
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)

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'
[keys.normal]
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

[keys.insert]
"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.

[keys.insert.j]
k = "normal_mode" # Maps `jk` to exit insert mode

[keys.normal.g]
a = "code_action" # Maps `ga` to show possible code actions

# invert `j` and `k` in view mode
[keys.normal.z]
j = "scroll_up"
k = "scroll_down"

# create a new minor mode bound to `+`
[keys.normal."+"]
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
Backspace"backspace"
Space"space"
Return/Enter"ret"
-"minus"
Left"left"
Right"right"
Up"up"
Down"down"
Home"home"
End"end"
Page Up"pageup"
Page Down"pagedown"
Tab"tab"
Delete"del"
Insert"ins"
Null"null"
Escape"esc"

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.

Languages

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
    
    [language-server.mylang-lsp]
    command = "mylang-lsp"
    
    [[language]]
    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:

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

These configuration keys are available:

KeyDescription
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-tokenThe token to use as a comment-token
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.

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 = ["Makefile", "toml", { suffix = ".git/config" }]

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

  1. Exact match: if the filename of a file is an exact match of a string in a file-types list, that language wins. In the example above, "Makefile" will match against Makefile files.
  2. Extension: if there are no exact matches, any file-types string that matches the file extension of a given file wins. In the example above, the "toml" matches files like Cargo.toml or languages.toml.
  3. Suffix: if there are still no matches, any values in suffix tables are checked against the full path of the given file. In the example above, the { suffix = ".git/config" } would match against any config files in .git directories. Note: / is used as the directory separator but is replaced at runtime with the appropriate path separator for the operating system, so this rule would match against .git\config files on Windows.

Language Server configuration

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

For example:

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

[language-server.efm-lsp-prettier]
command = "efm-langserver"

[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }

These are the available options for a language server.

KeyDescription
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" }

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

[language-server.typescript-language-server]
# 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.

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:

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

or equivalent:

[[language]]
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:

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

Grammar configuration takes these keys:

KeyDescription
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:

KeyDescription
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.

Guides

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.

Queries

  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.

๐Ÿ’ก 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.

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
function.inside
function.around
class.inside
class.around
test.inside
test.around
parameter.inside
comment.inside
comment.around

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.

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.

#![allow(unused)]
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.

#![allow(unused)]
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:

#![allow(unused)]
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.

]
  (parenthesized_expression)
  (function_definition)
  (class_definition)
] @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

]
  (function_definition)
  (class_definition)
] @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 (
            self.is_strong
            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 (
            self.is_strong
            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

]
  (function_definition)
  (class_definition)
] @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))

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

Predicates

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:

(some_kind
  (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).

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.

Scopes

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

#![allow(unused)]
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.

Settings

  • 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.

Predicates

  • #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).