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.
/// 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;
}
Find the starting position for the search
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;
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;
}
}
Start the search
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 };
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;
}