diff options
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 76 |
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). |
