Lesson 08 built in

Code awareness

Tree-sitter is compiled into bi, so every open file already has a parse tree. Highlighting, block context, symbols, split/join and a handful of marks on the text all come from it — no plugin, no query files to install.

A parse tree for twenty languages

bi ships twenty grammars: Rust, C, C++, C3, Go, Python, Lua, Bash, CSS, GLSL, HLSL, Slang, HCL/Terraform, Dockerfile, CMake, TOML, YAML, JSON, INI and Markdown. The grammar is picked by the file name and reparsed incrementally on every keystroke, and only the visible rows are highlighted, so a large file costs no more than a small one.

When the name lies — a script with no extension, a .txt that is really JSON — tell bi what to read it as:

Command Does
:set syntax bash parse this buffer with another grammar
:set syntax auto hand the choice back to the file name
:set syntax report the grammar actually in force

A name bi has no grammar for is refused by name rather than silently leaving the buffer plain.

vs vim
There is nothing to install: no nvim-treesitter, no :TSInstall, no parser compiled on first open. The grammars are in the binary.

Which block am I in

A closing brace says nothing. bi repeats the line that opened the block around the cursor after the line that closes it, behind the language’s comment marker, and when that opening line has scrolled off the top it draws it over the pane’s first row instead.

lstring.c What block am I in 1 / 3
What block am I in: step 0 What block am I in: step 1 What block am I in: step 2
Option Does
context_depth = 1 how many enclosing blocks get their closer annotated; 0 turns it off
context_header_depth = 1 how many opening lines may stack over the top row
context_min_lines = 1 blocks shorter than this are left alone

Nothing is shown while the cursor is on the opening line itself — it is right there already.

Symbols

:symbols (or :sym) lists the modules, functions, types and methods tree-sitter found in the buffer. It is a walk of the tree that is already there: no index, no tags file, nothing to go stale after an edit.

lstring.c Jump to a declaration 1 / 4
Jump to a declaration: step 0 Jump to a declaration: step 1 Jump to a declaration: step 2 Jump to a declaration: step 3

A file with no declarations says so; a file bi has no grammar for says that instead — which is what :set syntax is for.

Split and join lists

:tssplit breaks the bracketed list around the cursor — call arguments, parameters, arrays, struct literals, a { } block — onto one line per element. :tsjoin puts it back. “Element” means what the grammar says, not what a regex guessed, so a comma inside a nested call stays where it is.

lstring.c Split an argument list 1 / 3
Split an argument list: step 0 Split an argument list: step 1 Split an argument list: step 2

No trailing comma is added on split; one pressed against the closer is removed on join.

Reflow and reindent

gq is an operator: gqip rewraps a paragraph to textwidth, keeping the indentation and the comment leader (///, //, #, --, *, > …) on every line. Inside code, name the lines — gq3j — so the paragraph does not run on into the function below. = reindents by what the brackets say.

src/color.rs Reflow a comment 1 / 3
Reflow a comment: step 0 Reflow a comment: step 1 Reflow a comment: step 2
Key Does
gq{motion} reflow the lines the motion touches — gqip the paragraph
gqq gqgq reflow the cursor’s line
={motion} reindent — == the line, gg=G the file
:set textwidth 100 the width gq folds to; 80 by default
vs vim
textwidth defaults to 80, not 0: bi never wraps while you type, so the option only means “the width gq aims at”.

Marks on the text

A few things are drawn over the text without being in it. Each is an option you can switch off, globally or per filetype.

Indent guides

A thin vertical line at every indentation step, never over the text itself. A blank line keeps the guides of the block it sits in. :set indent_guides false.

TODO: and friends

TODO:, FIX:, HACK:, WARN:, PERF:, NOTE: and TEST: get five theme colours — FIXME, BUG, XXX, OPTIM and the rest are aliases. Uppercase, on a word boundary, colon required, TODO(name): included. They are matched anywhere, not only in comments, because a Markdown list or a YAML file is exactly where people write them. :set todo_comments false.

Colour swatches

#fb4934, #f94, rgb(251,73,52), rgba(…) and the shader spelling rgb(0.5f,0.1f,0.1) are painted in the colour they name, with black or white text chosen by contrast. A colour must be a whole token, so #define is not one. :set color_swatches false.

src/theme.rs Tags and swatches 1 / 1
Tags and swatches: step 0

Showing the whitespace

:whitespace (:ws) toggles a literal view of every blank: · for a space, for a tab at the column it starts, for a newline and for a non-breaking space. Indent guides stand down while it is on.

lstring.c See every blank 1 / 2
See every blank: step 0 See every blank: step 1

Git signs

One cell to the left of the line numbers is held open for signs. For every line that differs from what git add would take, git draws (added or changed) or (lines removed under this row), and the focused window’s status row carries a +3 ~1 -2 numstat. The diff is against the index and follows the buffer as you type — nothing has to be saved.

lstring.c Git signs 1 / 4
Git signs: step 0 Git signs: step 1 Git signs: step 2 Git signs: step 3
Command Does
:set git_signs false hide the signs and the numstat
:set gutter 0 give the sign column back; a larger number reserves more
:e re-read the baseline after a git add elsewhere

A diagnostic’s wins the cell when both want it. Untracked files and directories outside a repository simply show nothing.

vs vim
This is the whole of git in bi, on purpose: no hunk motions, no staging, no blame. It answers “what have I touched” without a plugin.

A flash on yank

yy is the one command whose effect is invisible, so what it read lights up for yank_flash milliseconds — charwise, linewise, or one span per row of a rectangle. Deletes and pastes do not flash; you can already see those.

lstring.c A flash on yank 1 / 2
A flash on yank: step 0 A flash on yank: step 1
Option Does
yank_flash = 150 milliseconds; 0 turns it off