Back to blog

Paste an error into my guides page and it picks the guide

The cursor at the end of my guides path is now a prompt. Paste a compiler error into it and the guide carrying that error is highlighted, without the text ever leaving your browser.

• 5 min read •
alpine.js typescript rust search

My guides page is a list of short fixes, one problem each. You narrow it by clicking through a path, starting from a technology and ending at the error you see. At its end sits a blinking block cursor, like a terminal's. Since 28 September that cursor is where you type. Paste in the first line of the error rustc gave me when an actix-rt update broke my build:

error[E0432]: unresolved import `actix_rt::net`

The guide that fixes it is highlighted, and Enter opens it. Two days earlier I had decided the guides would be found through search engines only, with no search on the site. This reversed that.

There is no search server behind it. When the server renders the page, every row already carries its own search text in a data-find attribute, which is extra text an HTML element carries for scripts to read. A guide carries its title, each symptom, each technology and each tag, one per line:

pub fn find_text(&self) -> String {
    std::iter::once(self.title.as_str())
        .chain(self.symptoms.iter().map(String::as_str))
        .chain(self.tech.iter().map(|t| t.name.as_str()))
        .chain(self.tags.iter().map(String::as_str))
        .collect::<Vec<_>>()
        .join("\n")
}

One per line is on purpose. A paste gets matched against whole lines, so two symptoms must never run together, and a Rust test checks that. The filtering happens in the browser, in a component built with Alpine.js, a small library that adds behaviour through HTML attributes. It only hides and shows what the server rendered. What you type never leaves your browser or goes into a URL. Enter only follows a link the server already wrote into the row. Without JavaScript the text box stays hidden and the clickable path works as before.

Filtering on typed words is the easy part. The obvious rule keeps a row when every word you type appears somewhere in its text, and for a word such as actix it works fine.

A pasted error breaks that rule. A real paste carries an error code and a file path with line numbers, and no guide contains those, so every row fails. With the two lines rustc prints for E0432, its code for an import it can't resolve, the word rule matches nothing at all:

error[E0432]: unresolved import `actix_rt::net`
  --> actix-server-2.8.0/src/builder.rs:3:15

So there is a second rule. First the paste and the row's lines both go to lower case, and every run of characters that isn't a letter or a digit becomes one space. Backticks, quotes, colons and underscores stop mattering, and the guide's symptom turns into this:

unresolved import actix rt net

Then it asks whether the paste holds one of the row's lines whole. Both sides get a space at each end, so a line only matches on word edges, and a tag like rust can't match inside trust. I've split the long line to fit:

function wholePart(typed: string, of: string[]): number {
    return Math.max(0, ...of.map(normalise)
        .filter((part) => part && ` ${typed} `.includes(` ${part} `))
        .map((part) => part.length));
}

The function returns a length rather than yes or no because the same paste also holds actix-rt, one of the technology choices you can click. The highlight goes to the row with the longest whole line. The symptom is much longer than a technology name, so the guide wins.

The other problem was in hiding rows. The first version hid the filtered rows with Alpine's x-show, and under fast typing, rows that no longer matched stayed on screen. As far as I can tell from Alpine's source, x-show doesn't change the page straight away. It hands the change to x-transition's code, where showing a row waits for the next animation frame, just before the browser paints, and hiding takes another route with its own delays. Fast typing got the two applied out of order. Those delays are deliberate, and they matter for elements that animate or listen for clicks outside themselves. These rows do neither.

So the filtered rows now bind the hidden attribute instead, with Alpine's x-bind shorthand, :hidden="hides($el)". Alpine sets or removes an attribute as soon as its update runs, with no frame in between. Two short messages that rarely change, "nothing matches" and the help text, still use x-show.

The catch with hidden is that any CSS rule setting display on the same element beats it. On this page the step heading above the choices is inline-block, and it gets hidden when no choice matches. Tailwind's base styles, Preflight, include a [hidden] rule with display: none !important, so the heading still goes. Without Tailwind you'd add that rule yourself.

The matcher has one weakness. It only knows the exact symptom text a guide carries. The symptom on my sqlx guide is the full error line for my own site's column, total_visits and all. Your error names your own column, so your paste no longer holds that line whole. With sum_amount instead, the only line it holds whole is the word "rust". The sqlx guide stays in the list, but the highlight falls on the Rust choice at the top, so Enter steps into Rust instead of opening the guide. I think the answer is to write each symptom as the part of the error that stays the same for everyone, or to add that part as a second symptom.

For a short list, I think search text in the page plus a filter in the browser is enough, and it leaves nothing new on the server to secure. If people will paste into it, put a whole-line check next to the word check and rank by the longest match. The prompt also takes help. There are only six guides so far, so the odds aren't great, but you can paste the error you're stuck on at ericjingryd.com/guides.


Sources and further reading: