summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig76
1 files changed, 76 insertions, 0 deletions
diff --git a/src/look.zig b/src/look.zig
index 7169cf8b..36390ec2 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -225,6 +225,82 @@ test ".pdf Look paths are ordinary files when MuPDF is disabled" {
}
}
+/// Where a look-able word actually SITS inside a run of non-whitespace.
+pub const Span = struct { start: usize, end: usize };
+
+/// The punctuation a path wears in prose and never owns. Two sets, because
+/// the two ends are not alike: a directory may legally END in `/`, and the
+/// `:` that closes `grep -n`'s `main.zig:100:` is junk on the right and
+/// meaningful nowhere on the left.
+const lead_trim = "([{<\"'`*";
+const trail_trim = ")]}>\"'`*,;:.!?";
+
+/// The largest look-able span inside one whitespace-delimited `word`, or null
+/// when nothing in it resolves. This is the whole heuristic behind n/N: split
+/// on whitespace, and take the biggest piece of each run that Look can act on.
+///
+/// TWO resolve attempts at most, which is what keeps a motion across a
+/// screenful of prose from being a hundred realpaths: the run with every
+/// wrapper character peeled off BOTH ends at once, then — only if that found
+/// nothing — the run exactly as written.
+///
+/// PEELED FIRST, which is the ordering that matters. `resolve` is lenient
+/// about a tail it cannot parse (`main.zig:12:3,` yields the FILE and drops
+/// the position, by design), so asking it about the raw run first would
+/// happily answer yes and swallow the comma along with the `:3`. Peeling
+/// first hands it `main.zig:12:3` and the look lands on the column. The raw
+/// run stays as the fallback for the file genuinely named `foo,` or `..`,
+/// where the peel eats something real.
+///
+/// Deliberately NOT a search for the longest resolving substring: that costs
+/// a syscall per prefix to find a path hiding inside a word nobody typed as
+/// one. A run needing a cleverer peel is still one Enter away with the cursor
+/// parked on it.
+///
+/// Direction-free on purpose: n and N ask this the same question about the
+/// same run and get the same span back, which is what lets the two motions be
+/// exact inverses of each other.
+pub fn lookableSpan(word: []const u8, cwd: []const u8, realbuf: *[4096]u8) ?Span {
+ if (word.len == 0) return null;
+ var lo: usize = 0;
+ var hi: usize = word.len;
+ while (lo < hi and std.mem.indexOfScalar(u8, lead_trim, word[lo]) != null) lo += 1;
+ while (hi > lo and std.mem.indexOfScalar(u8, trail_trim, word[hi - 1]) != null) hi -= 1;
+ if (lo < hi and resolve(word[lo..hi], cwd, realbuf) != .none) return .{ .start = lo, .end = hi };
+ // nothing came off, so the peeled attempt WAS the raw one
+ if (lo == 0 and hi == word.len) return null;
+ if (resolve(word, cwd, realbuf) == .none) return null;
+ return .{ .start = 0, .end = word.len };
+}
+
+test "lookableSpan peels prose punctuation off a path, largest first" {
+ if (!platform_has_fs) return;
+ var realbuf: [4096]u8 = undefined;
+ // the bare run resolves whole, wrappers and all left alone
+ try std.testing.expectEqualDeep(
+ @as(?Span, .{ .start = 0, .end = "src/look.zig".len }),
+ lookableSpan("src/look.zig", ".", &realbuf),
+ );
+ // ...and a wrapped one gives back the span INSIDE the wrappers
+ try std.testing.expectEqualDeep(
+ @as(?Span, .{ .start = 1, .end = 1 + "src/look.zig".len }),
+ lookableSpan("(src/look.zig),", ".", &realbuf),
+ );
+ // the `:LINE:COL` tail is part of the span: it is what a look READS
+ try std.testing.expectEqualDeep(
+ @as(?Span, .{ .start = 0, .end = "src/look.zig:12:3".len }),
+ lookableSpan("src/look.zig:12:3,", ".", &realbuf),
+ );
+ // grep -n's trailing delimiter comes off, the line number stays
+ try std.testing.expectEqualDeep(
+ @as(?Span, .{ .start = 0, .end = "src/look.zig:12".len }),
+ lookableSpan("src/look.zig:12:", ".", &realbuf),
+ );
+ try std.testing.expectEqual(@as(?Span, null), lookableSpan("nothing-here", ".", &realbuf));
+ try std.testing.expectEqual(@as(?Span, null), lookableSpan("", ".", &realbuf));
+ try std.testing.expectEqual(@as(?Span, null), lookableSpan("((()))", ".", &realbuf));
+}
+
/// Resolve a looked-at word against the pane's directory. `realbuf` must
/// outlive the returned Target (native paths point into it; web paths are
/// process-lifetime slices in the embedded source archive).