mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-15 15:32:03 -04:00
cli: suggest the closest flag or command on a typo
An unknown --flag now logs the nearest accepted spelling within two edits as did_you_mean, painted green next to the red typo in the pretty log format; logfmt stays plain. A bare first argument within two edits of a command name is rejected with the same hint instead of being fetched as a url. The Levenshtein helper moves from SlashCommand into string.zig so the agent REPL and the CLI share it, with the table widened to fit the longest flag name.
This commit is contained in:
1 parent
5d732eb8f1
commit
88d133a676
4 files changed
+177
-30
No files matched your search
@@ -425,6 +425,40 @@ pub fn isOneOf(needle: []const u8, haystack: []const []const u8) bool {
|
||||
} else false;
|
||||
}
|
||||
|
||||
/// Case-insensitive. Inputs over 64 bytes return `maxInt`; that fits the
|
||||
/// longest CLI flag.
|
||||
fn editDistance(a: []const u8, b: []const u8) usize {
|
||||
const max = 64;
|
||||
if (a.len > max or b.len > max) return std.math.maxInt(usize);
|
||||
var prev: [max + 1]u8 = undefined;
|
||||
var cur: [max + 1]u8 = undefined;
|
||||
for (0..b.len + 1) |j| prev[j] = @intCast(j);
|
||||
for (a, 1..) |ca, i| {
|
||||
const la = std.ascii.toLower(ca);
|
||||
cur[0] = @intCast(i);
|
||||
for (b, 1..) |cb, j| {
|
||||
const cost: u8 = if (la == std.ascii.toLower(cb)) 0 else 1;
|
||||
cur[j] = @min(@min(prev[j] + 1, cur[j - 1] + 1), prev[j - 1] + cost);
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
return prev[b.len];
|
||||
}
|
||||
|
||||
/// Earlier candidates win ties.
|
||||
pub fn closest(name: []const u8, candidates: []const []const u8, max_dist: usize) ?[]const u8 {
|
||||
var best: ?[]const u8 = null;
|
||||
var best_dist: usize = std.math.maxInt(usize);
|
||||
for (candidates) |cand| {
|
||||
const dist = editDistance(name, cand);
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
return if (best_dist <= max_dist) best else null;
|
||||
}
|
||||
|
||||
/// Largest prefix of `bytes` whose length is at most `max_bytes` and
|
||||
/// ends on a UTF-8 codepoint boundary. Invalid sequences count as one
|
||||
/// byte each so the function never loops.
|
||||
@@ -519,6 +553,34 @@ test "truncateUtf8" {
|
||||
try testing.expectEqual("\xFFx", truncateUtf8("\xFFx", 2));
|
||||
}
|
||||
|
||||
test "editDistance" {
|
||||
try testing.expectEqual(@as(usize, 0), editDistance("", ""));
|
||||
try testing.expectEqual(@as(usize, 0), editDistance("wait-ms", "wait-ms"));
|
||||
try testing.expectEqual(@as(usize, 0), editDistance("Wait-MS", "wait-ms"));
|
||||
try testing.expectEqual(@as(usize, 1), editDistance("wait-mss", "wait-ms"));
|
||||
try testing.expectEqual(@as(usize, 1), editDistance("wait-m", "wait-ms"));
|
||||
try testing.expectEqual(@as(usize, 1), editDistance("wait_ms", "wait-ms"));
|
||||
try testing.expectEqual(@as(usize, 3), editDistance("kitten", "sitting"));
|
||||
try testing.expectEqual(@as(usize, 3), editDistance("", "abc"));
|
||||
try testing.expectEqual(@as(usize, 3), editDistance("abc", ""));
|
||||
|
||||
const long = "x" ** 64;
|
||||
try testing.expectEqual(@as(usize, 0), editDistance(long, long));
|
||||
try testing.expectEqual(std.math.maxInt(usize), editDistance(long ++ "x", long));
|
||||
}
|
||||
|
||||
test "closest" {
|
||||
const names = [_][]const u8{ "--dump", "--wait-ms", "--wait-until" };
|
||||
try testing.expectEqual("--wait-ms", closest("--wait-mss", &names, 2));
|
||||
try testing.expectEqual("--dump", closest("--dmup", &names, 2));
|
||||
try testing.expectEqual(null, closest("--totally-wrong", &names, 2));
|
||||
try testing.expectEqual(null, closest("--wait-mss", &names, 0));
|
||||
try testing.expectEqual(null, closest("--dump", &.{}, 2));
|
||||
|
||||
const tie = [_][]const u8{ "ab", "ac" };
|
||||
try testing.expectEqual("ab", closest("a", &tie, 1));
|
||||
}
|
||||
|
||||
test "latin1ToUtf8" {
|
||||
const cases = [_]struct { in: []const u8, out: []const u8 }{
|
||||
.{ .in = "caf\xE9.txt", .out = "café.txt" },
|
||||
|
||||
Reference in new issue
Block a user