Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The findCallback() function

This one is big. We'll start with a stub, filled with placeholders. We reset the highlight and we handle clean up at the start of the function, so that later it can return at any point.

Editor.zig
/// Called by promptForInput() for every valid inserted character.
/// The saved view is restored when the current query isn't found, or when
/// backspace clears the query, so that the search starts from the original
/// position.
fn findCallback(e: *Editor, ca: t.PromptCbArgs) t.EditorError!void {
    // 1. variables
    // 2. restore line highlight
    // 3. clean up
    // 4. query is empty so no need to search, but restore position
    // 5. handle backspace
    // 6. find the starting line and the column offset for the search
    // 7. start the search
}

Variables

As we did before, we have a static struct which will save the current state of the search.

    const static = struct {
        var found: bool = false;
        var view: t.View = .{};
        var pos: t.Pos = .{};
        var oldhl: []t.Highlight = &.{};
    };

We also define some constants:

    const empty = ca.input.items.len == 0;
    const numrows = e.buffer.rows.items.len;

Restore line highlight before incsearch highlight

Before a new search attempt, we restore the underlying line highlight, so that if the search fails, the search highlight has been cleared already.

    // restore line highlight before incsearch highlight, or clean up
    if (static.oldhl.len > 0) {
        @memcpy(e.rowAt(static.pos.lnr).hl, static.oldhl);
    }

Clean up

The clean up must also be handled early. This block runs during the last invocation of the callback, that is done for this exact purpose.

In this step we free the search highlight, reset our static variables and restore the view if necessary.

    // clean up
    if (ca.final) {
        e.alc.free(static.oldhl);
        static.oldhl = &.{};
        if (empty or ca.key == .esc) {
            e.view = ca.saved;
        }
        if (!static.found and ca.key == .enter) {
            try e.statusMessage("No match found", .{});
        }
        static.found = false;
        return;
    }

Empty query

This happens after we press Backspace and the query is now empty. We don't cancel the search yet, but we restore the original view. Search will be canceled if we press Backspace again. We also reset static.found because it was true if that character we just deleted was a match.

    // Query is empty so no need to search, but restore position
    if (empty) {
        static.found = false;
        e.view = ca.saved;
        return;
    }

Handle Backspace

This happens when we press Backspace, but the query is not empty. In this case we restore our static view, which is set later on. Note that if the current query can't be found, this would be the same of the original view, but what matters is that we must restore it, whatever it is.

    // when pressing backspace we restore the previously saved view
    // cursor might move or not, depending on whether there is a match at
    // cursor position
    if (ca.key == .backspace or ca.key == .ctrl_h) {
        e.view = static.view;
    }

We define some constants, to make the function flow more understandable.

    //////////////////////////////////////////
    //   Find the starting position
    //////////////////////////////////////////

    const V = &e.view;

    const prev = ca.key == .ctrl_t;
    const next = ca.key == .ctrl_g;

    // current cursor position
    var pos = t.Pos{ .lnr = V.cy, .col = V.cx };

    const eof = V.cy == numrows;
    const last_char_in_row = !eof and V.rx == e.currentRow().render.len;
    const last_row = V.cy == numrows - 1;

    // must move the cursor forward before searching when we don't want to
    // match at cursor position
    const step_fwd = next or empty or !static.found;

Warning

If we skip the !eof check when defining last_char_in_row, we would cause panic when starting a search at the end of the file. This happens because e.currentRow() tries to get a pointer to a line that doesn't exist. Watch out for these things!

We are determining where the search must start, and that's either at cursor position, or just after that (one character to the right). That is, we must decide whether to accept a match at cursor position or not.

We want to step forward:

  • if we press Ctrl-G, looking for the next match

  • if we are at the starting position, because either:

    • we just started a search
    • query is empty
    • a match hasn't been found

In any of these cases:

    if (step_fwd) {
        if (eof or (last_row and last_char_in_row)) {
            if (!opt.wrapscan) { // restart from the beginning of the file?
                return;
            }
        }
        else if (last_char_in_row) { // start searching from next line
            pos.lnr = V.cy + 1;
        }
        else { // start searching after current column
            pos.col = V.cx + 1;
            pos.lnr = V.cy;
        }
    }

Our match is an optional slice of the chars.items array of the Row where the match was found. We try to find it with the appropriate functions, which we'll define later.

    //////////////////////////////////////////
    //          Start the search
    //////////////////////////////////////////

    var match: ?[]const u8 = null;

    if (!prev) {
        match = e.findForward(ca.input.items, &pos);
    }
    else {
        match = e.findBackward(ca.input.items, &pos);
    }

    static.found = match != null;

If a match is found, we update the cursor position and the static variables.

Since match is a slice of the original array, we can find the column with pointer arithmetic, by subtracting the address of the first character of the chars.items array from the address of the first character of our match.

    const row = e.rowAt(pos.lnr);

    if (match) |m| {
        V.cy = pos.lnr;
        V.cx = &m[0] - &row.chars.items[0];

        static.view = e.view;
        static.pos = .{ .lnr = pos.lnr, .col = V.cx };

&row.chars.items[0]&m[0]Match (m)Row(row.chars.items)

Note

Since we pass &pos to the functions, we could set the column there, but this works anyway (it's actually less trouble). Initially I wasn't using Pos, but I'm keeping it to show an example of pointer arithmetic in Zig. Feel free to refactor it if it suits you better.

Before setting the new highlight, we store a copy in static.oldhl. It will be restored at the top of the callback, every time the callback is invoked.

Note that we are matching against row.chars.items (the real row), but the highlight must match the characters in the rendered row, so we must convert our match position first, with cxToRx.

        // first make a copy of current highlight, to be restored later
        static.oldhl = try e.alc.realloc(static.oldhl, row.render.len);
        @memcpy(static.oldhl, row.hl);

        // apply search highlight
        const start = row.cxToRx(V.cx);
        const end = row.cxToRx(V.cx + m.len);
        @memset(row.hl[start .. end], t.Highlight.incsearch);
    }

If a match wasn't found, we restore the initial view (before we started searching).

We must also handle the case that wrapscan is disabled, a match isn't found in the current searching direction, but there was possibly a match before, so we just remain there, and set the highlight at current position. We need to set it because the original has been restored at the top.

Also here we do the same conversion, but we use the saved position.

    else if (next or prev) {
        // the next match wasn't found in the searching direction
        // we still set the highlight for the current match, since the original
        // highlight has been restored at the top of the function
        // this can definitely happen with !wrapscan
        const start = row.cxToRx(static.pos.col);
        const end = row.cxToRx(static.pos.col + ca.input.items.len);
        @memset(row.hl[start .. end], t.Highlight.incsearch);
    }
    else {
        // a match wasn't found because the input couldn't be found
        // restore the original view (from before the start of the search)
        e.view = ca.saved;
    }