Drawn live in your browser: my name sent as a LoRa packet, and painted into the spectrum below it.

Rani

I contribute to the code editor I use, the language it’s written in, and the web framework this site is built with. Oh, and I also program satellites.

Up to 16 characters. It goes out the same way my name did.

Making multi-cursor editing fast in Zed

Put a cursor on every line of a file and start typing, and Zed used to struggle: about 24 ms per keystroke at 1,000 cursors, and a hang at 10,000. The issue had been open for a year.

I profiled where each keystroke’s time went and took it apart one layer at a time. Each pull request below was measured against main as it was that week, so the numbers are separate before and after pairs, not one running total.

cursors.rs
cursors

1,000 lines, cursor at column 20

This buffer only copies your keystroke to every line. Zed also records each edit in a collaborative text history, maps every cursor through its display layers and draws the result, which is where the time went.

  1. #58510, merged 17 June 2026 by Anthony-Eid

    Speed up multi cursor editing

    Fast paths for the display map when there are no inlays or soft wraps, batched selection resolving when nothing is folded, and rendering only the selections that are on screen. Plus a benchmark at 1,000, 10,000 and 100,000 cursors.

    Typing, 1,000 cursors−51%
    before24.0 msafter11.7 ms
    Display sync, 10,000 cursors−66%
    before133 msafter45.0 ms
    Type and two word deletes, 10,000 cursors−37%
    before959 msafter606 ms
  2. Every keystroke sent the new selections to collaborators, remapping every cursor, even when nobody else was in the project. Now it only broadcasts for shared projects, and republishes the cursors when a project becomes shared so nothing is missed.

    Type and two word deletes, 10,000 cursors−5%
    before615 msafter583 ms
  3. #64409, merged 5 October 2026 by SomeoneToIgnore

    Reuse tab point cursor state across calls

    The tab map rebuilt its iterator from column zero on every lookup, five times per cursor per keystroke. It now keeps its position between calls and keeps scanning forward.

    While writing randomized tests for it, I found and filed an unrelated fold map bug (#64444).

    One display sync at 10,000 cursors, by layer
    InlayMap
    20 µs
    FoldMap
    204 µs
    TabMap
    46,160 µs
    WrapMap
    190 µs

    From the timers in the pull request. Only the tab map was measured again after the change; the other layers are shown as they were on main. At 1,000 cursors the tab map went from 3,856 µs to 1,380 µs.

  4. When nothing folds, wraps or inserts text, buffer rows and display rows are the same, so the change list can skip a round trip through the display map for every cursor.

    Type and two word deletes, 10,000 cursors−7%
    before549 msafter509 ms

My first Zed pull request, in December 2025, added banners to the extensions page that tell people searching for Basedpyright, Ruff or Ty that Zed now has them built in (#43872, merged 2 December 2025). All my Zed pull requests

Fixing a Rust compiler error that printed impossible types

When two function pointer types differ only in how general their lifetimes are, rustc explains the mismatch by printing both types. With nested function pointers, it repeated the outer for<'a> inside the inner one, so the note showed a type you can’t write.

src/lib.rs
type F1 = fn(fn(&'static ()));
type F2 = for<'a> fn(fn(&'a ()));

fn issue_134410(a: &mut F1) {
    let _: &mut F2 = a;
}
error[E0308]: mismatched types
 --> src/lib.rs:5:22
  |
5 |     let _: &mut F2 = a;
  |                      ^ one type is more general than the other
  |
  = note: expected mutable reference `&mut for<'a> fn(for<'a> fn(&'a ()))`
             found mutable reference `&mut fn(fn(&()))`

rustc printed a second for<'a> that isn't in the source. Rust wouldn't even accept it there, because the outer for<'a> already declares 'a.

The cause was in how the pretty printer names lifetimes. When it printed a binder, it named every lifetime at or above that binder’s depth, so a lifetime that belonged to an outer binder got listed again at each level it passed through. I changed it to name only the lifetimes the current binder actually binds. That also made an older special case for the error labels unnecessary, so I deleted it. The example above is adapted from the regression test I added.

#159232, merged 26 August 2026. Fixes #134410 and the same leak in #111365.

Letting Next.js apps type-check their stylesheet imports

TypeScript 5.6 added noUncheckedSideEffectImports, which reports an import like import './globals.css' when nothing declares that module. Next.js declared CSS modules but not plain stylesheets, so turning the option on broke the type check on an app’s own global CSS.

tsconfig.json has "noUncheckedSideEffectImports": true

app/layout.tsx

1import type { Metadata } from 'next'
2import './globals.css'
Type error: Cannot find module './globals.css' or its corresponding type declarations.
3
4export const metadata: Metadata = {
5 title: 'My app',
6}

packages/next/types/global.d.ts

37declare module '*.module.scss' {
38 const classes: { readonly [key: string]: string }
39 export default classes
40}
41
42// We implement the behavior of `import 'server-only'` …

Next.js declared CSS modules, but nothing declared a plain stylesheet, so the import is an error.

The fix is three empty module declarations, for .css, .sass and .scss, next to the ones Next.js already had for server-only and client-only. I extended the production test for the option so it builds an app that imports all three, and checked that it fails without the fix.

#88199, merged 12 January 2026 by timneutkens. Fixes #88197.

A radio driver for a satellite

Our team at the Herzliya Space Laboratory is building the communication card for a satellite: an ATmega328PB microcontroller and an Ebyte E22-400T30D LoRa radio module. My part is the driver for the module, the example sketches we use to bring up a new board, and the bench tests we’ve run with them. The beacon and transponder logic that will fly is a separate sketch that uses the driver.

The E22 isn’t a radio chip you program directly. It’s a small board with its own microcontroller in front of an SX1268 radio. You talk to it over a 9,600-baud serial port, two pins select the mode, and the whole configuration is nine register bytes. Here they are, starting from the values our modules shipped with.

E22-400T30D configuration registers

What the driver sends over the serial port

C0 00 09 00 00 00 62 00 17 83 00 00

C0 writes and saves to flash, starting at register 00, nine bytes.

05h REG2 = 0x17 = 23

Bits 7 to 0. Click a bit to flip it.

REG2 is 0x17.

Channel bits 7–0
Channel 23: 433.125 MHz

Built so it can’t hang

Every wait in the driver has a timeout, and every call returns false instead of blocking forever. Once the card is in orbit, the only thing the flight computer can do to it is turn it off and on again.

It also has to fit in 2 KB of RAM, so buffers stay small, strings live in flash and there is no printf. CI compiles every example for the flight board’s ATmega328PB, an Arduino Uno and an ESP32 on each push.

The driver on GitHub

On the bench, 6 October 2026

Two Arduino Unos and two E22-400T30D modules, at up to 21 dBm.

Passed

  • Reading and writing the configuration, verified by reading it back
  • All six air rates in both directions: 120 of 120 packets delivered
  • Different air rate, channel or network ID on the two modules: nothing received
  • Fixed-point sends arrive with the 3-byte header removed
  • A 300-byte message, split into 240-byte and 32-byte packets
  • Sleep and configuration modes receive nothing
  • UART at 19,200 and 38,400 baud, with the switch to 9,600 for configuration
  • The transmission showed up on the lab's spectrum analyzer

Not tested yet

  • The flight board itself, which has no AUX line
  • Hardware serial ports and the ESP32
  • Saving the configuration to flash
  • Timeouts with the module disconnected
  • Wake-on-radio receive, listen before talk and relay
  • Transmit power above 21 dBm

Other projects

  • Hativon

    חטיבון

    The website for my school's newspaper, in Hebrew and right to left. Students write drafts that save as they type and publish them; teachers review posts and accounts from a dashboard.

    Next.js, TypeScript, Bun

    Visit HativonSource code