move away from deprecated thingies/bump srf
All checks were successful
Generic zig build / build (push) Successful in 22s

This commit is contained in:
Emil Lerch 2026-09-03 13:58:24 -07:00
parent da49d843ca
commit eb6cc1f388
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 128 additions and 80 deletions

View file

@ -5,8 +5,8 @@
.minimum_zig_version = "0.16.0",
.dependencies = .{
.srf = .{
.url = "git+https://git.lerch.org/lobo/srf.git#7f3a43e838fc472bdb1af6c1b692f2255dd19f48",
.hash = "srf-0.0.0-qZj57-17AgAXn2at0WH1xV75oJhr3vTFeU5FxU7kAhtf",
.url = "git+https://git.lerch.org/lobo/srf.git#af8b998d3fd3f240277d7abf19343d3ddc25ed3a",
.hash = "srf-0.0.0-qZj57wqMAgAUtX8nN3ZyshqlUGDNjsyZqCUcguVqI2Og",
},
},
.paths = .{

View file

@ -22,7 +22,7 @@ stdenv.mkDerivation (finalAttrs: {
zigDeps = zig.fetchDeps {
inherit (finalAttrs) pname version src;
fetchAll = true;
hash = "sha256-0A2IlywPDkgX8+oiZ1Xd2zF0o07T4moBRngddwl1GBk=";
hash = "sha256-yWUmqKxop3uCqvHBrb16pgAgA2O+0kholkY+M7Zftbk=";
};
postConfigure = ''

View file

@ -430,14 +430,14 @@ const Harness = struct {
/// Asserts the transcript contains `needle`, which keeps the tests readable
/// without reimplementing a JSON-RPC client here.
fn expectSent(self: *Harness, needle: []const u8) !void {
if (std.mem.indexOf(u8, self.transcript(), needle) == null) {
if (std.mem.find(u8, self.transcript(), needle) == null) {
std.debug.print("\nexpected to find: {s}\nin transcript:\n{s}\n", .{ needle, self.transcript() });
return error.NotSent;
}
}
fn expectNotSent(self: *Harness, needle: []const u8) !void {
if (std.mem.indexOf(u8, self.transcript(), needle) != null) {
if (std.mem.find(u8, self.transcript(), needle) != null) {
std.debug.print("\nexpected NOT to find: {s}\nin transcript:\n{s}\n", .{ needle, self.transcript() });
return error.UnexpectedlySent;
}

View file

@ -442,14 +442,14 @@ test "missing magic header is reported on line 0" {
try testing.expect(diags.len > 0);
try testing.expectEqual(@as(u32, 0), diags[0].range.start.line);
try testing.expectEqual(Severity.err, diags[0].severity);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "Magic header") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "Magic header") != null);
}
test "duplicate magic header is reported" {
const diags = try analyzeForTest("#!srfv1\n#!srfv1\nname::alice\n");
defer freeDiagnostics(testing.allocator, diags);
try testing.expect(diags.len > 0);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "duplicate magic") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "duplicate magic") != null);
}
test "empty document reports the missing header" {
@ -458,8 +458,8 @@ test "empty document reports the missing header" {
const diags = try analyzeForTest("");
defer freeDiagnostics(testing.allocator, diags);
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "empty") != null);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "#!srfv1") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "empty") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "#!srfv1") != null);
}
test "whitespace-only document is treated as empty" {
@ -569,8 +569,8 @@ test "a long-format overrun is left to srf, which reports it as of 4bec474" {
const all = try analyzeForTest("#!srfv1\n#!long\nk:3:hello\n");
defer freeDiagnostics(testing.allocator, all);
try testing.expectEqual(@as(usize, 1), all.len);
try testing.expect(std.mem.indexOf(u8, all[0].message, "additional bytes after field value") != null);
try testing.expect(std.mem.indexOf(u8, all[0].message, "restated as 5") != null);
try testing.expect(std.mem.find(u8, all[0].message, "additional bytes after field value") != null);
try testing.expect(std.mem.find(u8, all[0].message, "restated as 5") != null);
try testing.expectEqual(@as(u32, 2), all[0].range.start.line);
}
@ -579,7 +579,7 @@ test "a length prefix past the end of the file is an error" {
defer freeDiagnostics(testing.allocator, diags);
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expectEqual(Severity.err, diags[0].severity);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "declares 99 bytes but only 6 remain") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "declares 99 bytes but only 6 remain") != null);
}
test "a compact-format overrun is left to srf, which reports it" {
@ -593,8 +593,8 @@ test "a compact-format overrun is left to srf, which reports it" {
try testing.expectEqual(@as(usize, 1), all.len);
try testing.expectEqual(Severity.err, all[0].severity);
try testing.expectEqual(@as(u32, 1), all[0].range.start.line);
try testing.expect(std.mem.indexOf(u8, all[0].message, "2 additional bytes") != null);
try testing.expect(std.mem.indexOf(u8, all[0].message, "restated as 5") != null);
try testing.expect(std.mem.find(u8, all[0].message, "2 additional bytes") != null);
try testing.expect(std.mem.find(u8, all[0].message, "restated as 5") != null);
}
test "a compact overrun with no following delimiter still names the real problem" {
@ -605,8 +605,8 @@ test "a compact overrun with no following delimiter still names the real problem
const all = try analyzeForTest("#!srfv1\nk:2:abc\n");
defer freeDiagnostics(testing.allocator, all);
try testing.expect(all.len >= 1);
try testing.expect(std.mem.indexOf(u8, all[0].message, "1 additional bytes") != null);
try testing.expect(std.mem.indexOf(u8, all[0].message, "restated as 3") != null);
try testing.expect(std.mem.find(u8, all[0].message, "1 additional bytes") != null);
try testing.expect(std.mem.find(u8, all[0].message, "restated as 3") != null);
}
test "a correct length prefix produces no diagnostic" {
const diags = try analyzeForTest("#!srfv1\n#!long\nk:5:hello\n");
@ -640,8 +640,8 @@ test "length diagnostics coexist with srf's own diagnostics" {
var from_srf = false;
var from_us = false;
for (diags) |d| {
if (std.mem.indexOf(u8, d.message, "duplicate magic") != null) from_srf = true;
if (std.mem.indexOf(u8, d.message, "length prefix") != null) from_us = true;
if (std.mem.find(u8, d.message, "duplicate magic") != null) from_srf = true;
if (std.mem.find(u8, d.message, "length prefix") != null) from_us = true;
}
try testing.expect(from_srf);
try testing.expect(from_us);
@ -653,7 +653,7 @@ test "a compact overrun does not produce the unlocated fallback" {
const diags = try analyzeForTest("#!srfv1\nk:2:abc\n");
defer freeDiagnostics(testing.allocator, diags);
for (diags) |d| {
try testing.expect(std.mem.indexOf(u8, d.message, "reported no location") == null);
try testing.expect(std.mem.find(u8, d.message, "reported no location") == null);
}
}
@ -669,20 +669,20 @@ test "a located length problem replaces the unlocated fallback" {
const diags = try analyzeForTest("#!srfv1\n#!long\nk:99:hello\n");
defer freeDiagnostics(testing.allocator, diags);
for (diags) |d| {
if (std.mem.indexOf(u8, d.message, "reported no location") != null) {
if (std.mem.find(u8, d.message, "reported no location") != null) {
std.debug.print("\nunlocated fallback should have been suppressed\n", .{});
return error.UnexpectedFallback;
}
}
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "declares 99 bytes but only 6 remain") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "declares 99 bytes but only 6 remain") != null);
}
test "the unlocated fallback still fires when nothing else explains the failure" {
const diags = try analyzeForTest("");
defer freeDiagnostics(testing.allocator, diags);
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "empty") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "empty") != null);
}
/// Only the type-consistency warnings, without srf's own diagnostics.
@ -804,7 +804,7 @@ test "a one against one tie flags the later occurrence" {
defer freeDiagnostics(testing.allocator, diags);
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expectEqual(@as(u32, 4), diags[0].range.start.line);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "is `num` in 1 other") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "is `num` in 1 other") != null);
}
test "the three string spellings are one kind, not three" {
@ -825,12 +825,12 @@ test "bool and binary inconsistencies are reported too" {
const bools = try inconsistenciesForTest("#!srfv1\n#!long\nk:bool:true\n\nk:bool:false\n\nk::true\n");
defer freeDiagnostics(testing.allocator, bools);
try testing.expectEqual(@as(usize, 1), bools.len);
try testing.expect(std.mem.indexOf(u8, bools[0].message, "`bool`") != null);
try testing.expect(std.mem.find(u8, bools[0].message, "`bool`") != null);
const bins = try inconsistenciesForTest("#!srfv1\n#!long\nk:binary:aGk=\n\nk:binary:aGk=\n\nk::aGk=\n");
defer freeDiagnostics(testing.allocator, bins);
try testing.expectEqual(@as(usize, 1), bins.len);
try testing.expect(std.mem.indexOf(u8, bins[0].message, "`binary`") != null);
try testing.expect(std.mem.find(u8, bins[0].message, "`binary`") != null);
}
test "keys are compared byte exactly, matching srf" {
@ -853,8 +853,8 @@ test "type warnings coexist with length warnings without duplication" {
var type_warnings: usize = 0;
var length_warnings: usize = 0;
for (diags) |d| {
if (std.mem.indexOf(u8, d.message, "other field(s) in this file") != null) type_warnings += 1;
if (std.mem.indexOf(u8, d.message, "length prefix") != null) length_warnings += 1;
if (std.mem.find(u8, d.message, "other field(s) in this file") != null) type_warnings += 1;
if (std.mem.find(u8, d.message, "length prefix") != null) length_warnings += 1;
}
try testing.expectEqual(@as(usize, 1), type_warnings);
try testing.expectEqual(@as(usize, 1), length_warnings);
@ -910,7 +910,7 @@ test "compact format does not allow trailing whitespace after a length prefix" {
const all = try analyzeForTest("#!srfv1\nk:5:hello \n");
defer freeDiagnostics(testing.allocator, all);
try testing.expect(all.len > 0);
try testing.expect(std.mem.indexOf(u8, all[0].message, "additional bytes") != null);
try testing.expect(std.mem.find(u8, all[0].message, "additional bytes") != null);
}
test "a bad length prefix followed by more lines does not crash the parser" {
@ -947,7 +947,7 @@ test "a recovered short prefix reports once when the rest of the line is valid"
defer freeDiagnostics(testing.allocator, diags);
try testing.expectEqual(@as(usize, 1), diags.len);
try testing.expectEqual(@as(u32, 1), diags[0].range.start.line);
try testing.expect(std.mem.indexOf(u8, diags[0].message, "restated as 5") != null);
try testing.expect(std.mem.find(u8, diags[0].message, "restated as 5") != null);
}
test "each bad length prefix in a document is reported once, on its own line" {
@ -956,7 +956,7 @@ test "each bad length prefix in a document is reported once, on its own line" {
try testing.expectEqual(@as(usize, 3), diags.len);
for (diags, 1..) |d, line| {
try testing.expectEqual(@as(u32, @intCast(line)), d.range.start.line);
try testing.expect(std.mem.indexOf(u8, d.message, "additional bytes") != null);
try testing.expect(std.mem.find(u8, d.message, "additional bytes") != null);
}
}

View file

@ -266,7 +266,7 @@ const Scanner = struct {
while (end < line_end and !isSpace(self.text[end])) end += 1;
const decl = self.text[body + 2 .. end];
const eq = std.mem.indexOfScalar(u8, decl, '=');
const eq = std.mem.findScalar(u8, decl, '=');
const name = if (eq) |i| decl[0..i] else decl;
// Mode directives take effect for everything below them.
@ -283,7 +283,7 @@ const Scanner = struct {
/// another field follows it on the same line.
fn scanField(self: *Scanner, field_start: usize) ?Item {
// Key runs to the first ':'.
const colon = std.mem.indexOfScalarPos(u8, self.text[0..self.line_end], field_start, ':') orelse
const colon = std.mem.findScalarPos(u8, self.text[0..self.line_end], field_start, ':') orelse
return self.abandonLine();
const head = self.splitHead(colon) orelse return self.abandonLine();
@ -336,7 +336,7 @@ const Scanner = struct {
}
// `key:hint:value`, where the hint may be padded with whitespace.
const second = std.mem.indexOfScalarPos(u8, self.text[0..self.line_end], colon + 1, ':') orelse
const second = std.mem.findScalarPos(u8, self.text[0..self.line_end], colon + 1, ':') orelse
return null;
var start = colon + 1;
var end = second;
@ -380,7 +380,7 @@ const Scanner = struct {
else if (end < new_line_end and self.text[end] == ',')
end + 1
else if (problem != null)
if (std.mem.indexOfScalarPos(u8, self.text[0..new_line_end], end, ',')) |comma|
if (std.mem.findScalarPos(u8, self.text[0..new_line_end], end, ',')) |comma|
comma + 1
else
null
@ -406,7 +406,7 @@ const Scanner = struct {
}
// Compact format: the next comma ends the value and starts a field.
if (std.mem.indexOfScalarPos(u8, self.text[0..self.line_end], value_start, ',')) |comma| {
if (std.mem.findScalarPos(u8, self.text[0..self.line_end], value_start, ',')) |comma| {
return .{
.end = comma,
.next_field = comma + 1,
@ -455,17 +455,9 @@ const Scanner = struct {
// and a range built from the first line's end would come out inverted.
//
// How far the surplus runs has to match how srf counts it, or hover ends
// up suggesting a different corrected length than the diagnostic does. srf
// stops at the next delimiter in compact format (`extra_bytes` in
// `checkShortPrefix`) and runs to end of line in long format, where a
// comma is ordinary text.
// up suggesting a different corrected length than the diagnostic does.
const line_end = self.lineEnd(after);
const surplus_end = if (self.long_mode)
line_end
else if (std.mem.indexOfScalarPos(u8, self.text[0..line_end], after, ',')) |comma|
comma
else
line_end;
const surplus_end = self.likelySurplusEnd(after, line_end);
return .{ .overrun = .{
.declared = declared,
@ -474,8 +466,30 @@ const Scanner = struct {
} };
}
/// Where the value most likely actually ends, mirroring srf's
/// `likelyExtraBytes`.
///
/// A comma is ambiguous in compact format: field separator, or content inside
/// a length-prefixed value, or even part of a key. The byte count normally
/// tells them apart, so with the count wrong the next ':' is the best anchor
/// left, being where the following field's key ends; the comma just before it
/// is therefore the separator. Falling back to the first comma covers a value
/// that contains a colon itself, such as a URL.
///
/// Long format has no comma delimiter, so the whole remainder of the line is
/// surplus there.
fn likelySurplusEnd(self: *const Scanner, after: usize, line_end: usize) usize {
if (self.long_mode) return line_end;
const remainder = self.text[after..line_end];
if (std.mem.findScalar(u8, remainder, ':')) |colon|
if (std.mem.findScalarLast(u8, remainder[0..colon], ',')) |separator|
return after + separator;
return after + (std.mem.findScalar(u8, remainder, ',') orelse remainder.len);
}
fn lineEnd(self: *const Scanner, from: usize) usize {
return std.mem.indexOfScalarPos(u8, self.text, from, '\n') orelse self.text.len;
return std.mem.findScalarPos(u8, self.text, from, '\n') orelse self.text.len;
}
fn nextLine(self: *const Scanner, line_end: usize) usize {
@ -502,7 +516,7 @@ const testing = std.testing;
/// Finds `needle` in `text` and returns the offset of its midpoint, which is a
/// stable way to say "hover here" without hand-counting byte offsets.
fn midpointOf(text: []const u8, needle: []const u8) usize {
const at = std.mem.indexOf(u8, text, needle).?;
const at = std.mem.find(u8, text, needle).?;
return at + needle.len / 2;
}
@ -706,7 +720,7 @@ test "a length prefix longer than the document swallows the rest of it" {
test "a blank line outside any value has nothing to hover" {
const text = "#!srfv1\n#!long\nname::alice\n\nname::bob\n";
const blank_at = std.mem.indexOf(u8, text, "alice\n\n").? + 6;
const blank_at = std.mem.find(u8, text, "alice\n\n").? + 6;
try testing.expectEqual(@as(?Token, null), tokenAt(text, blank_at));
}
@ -938,14 +952,27 @@ test "value kinds describe themselves for a message" {
try testing.expectEqualStrings("`binary`", ValueKind.bytes.describe());
}
test "in compact format the surplus stops at the next delimiter, as srf counts it" {
// srf's `checkShortPrefix` reports `indexOfScalar(past_val, ',')` bytes, so
// measuring to end of line here would make hover suggest a different
// corrected length than the diagnostic does.
test "in compact format the surplus runs to the delimiter before the next colon" {
// Mirrors srf's `likelyExtraBytes`. The nearest comma would give 2, making the
// value "23,000" and the next key "000,000,really" -- a comma in a key is the
// signature of a wrong guess. The colon after "really" is where the next
// field's key ends, so the comma just before it is the separator: 4 + 10 = 14.
const problem = problemAt("#!srfv1\nim_worth:4:23,000,000,000,really:bool:false\n", "im_worth").?;
try testing.expectEqual(@as(usize, 4), problem.overrun.declared);
// "23,0" then "00" then a comma: two surplus bytes, so 4 + 2 = 6.
try testing.expectEqual(@as(usize, 2), problem.overrun.surplus);
try testing.expectEqual(@as(usize, 10), problem.overrun.surplus);
}
test "in compact format a value holding both a delimiter and a colon" {
// Value "a:" declared, "a:b,c" most likely meant: 2 + 3 = 5.
const problem = problemAt("#!srfv1\nk:2:a:b,c,next::v\n", "k:2:").?;
try testing.expectEqual(@as(usize, 3), problem.overrun.surplus);
}
test "in compact format a colon with no delimiter before it falls back" {
// The first colon sits inside the value's URL, so there is no comma before it
// and the fallback to the first comma is what gets this right: 3 + 5 = 8.
const problem = problemAt("#!srfv1\nk:3:http://x,next::v\n", "k:3:").?;
try testing.expectEqual(@as(usize, 5), problem.overrun.surplus);
}
test "in compact format a surplus with no delimiter runs to end of line" {

View file

@ -312,11 +312,13 @@ fn writeLengthProblem(
// Hover is still where the detail belongs, so state the byte arithmetic
// and the rule rather than just repeating that something is wrong.
.overrun => |o| {
// Worded so that one verb form reads correctly for both a single
// surplus byte and many.
try w.print(
"**Length mismatch:** declares {d} byte{s}, but {d} more byte{s} follow before the end of the field.\n\n",
"**Length mismatch:** declares {d} byte{s}, but the field runs {d} byte{s} longer.\n\n",
.{ o.declared, plural(o.declared), o.surplus, plural(o.surplus) },
);
try w.print("Restating the length as {d} would cover them. ", .{o.declared + o.surplus});
try w.print("Restating the length as {d} would cover it. ", .{o.declared + o.surplus});
if (f.long_mode) {
try w.writeAll(
"A length-prefixed value must be followed by end of line, optionally after whitespace or a comment.\n\n",
@ -442,7 +444,7 @@ const testing = std.testing;
/// Hovers at the midpoint of `needle` and returns the markdown.
fn hoverOn(text: []const u8, needle: []const u8) ![]const u8 {
const at = std.mem.indexOf(u8, text, needle) orelse return error.NeedleNotInText;
const at = std.mem.find(u8, text, needle) orelse return error.NeedleNotInText;
const position = positions.positionFromByte(text, at + needle.len / 2, .@"utf-8");
// An error beats a panic here: hovering punctuation legitimately returns
// null, and a test that lands there should say so rather than abort.
@ -454,7 +456,7 @@ fn hoverOn(text: []const u8, needle: []const u8) ![]const u8 {
fn expectHoverContains(text: []const u8, needle: []const u8, expected: []const u8) !void {
const markdown = try hoverOn(text, needle);
defer testing.allocator.free(markdown);
if (std.mem.indexOf(u8, markdown, expected) == null) {
if (std.mem.find(u8, markdown, expected) == null) {
std.debug.print("\nhovering \"{s}\"\nexpected to contain: {s}\ngot:\n{s}\n", .{ needle, expected, markdown });
return error.MissingFromHover;
}
@ -566,8 +568,8 @@ test "currency in a num is judged by srf, not by us" {
const text = "#!srfv1\n#!long\ncost:num:¥15,000\n";
const markdown = try hoverOn(text, "15,000");
defer testing.allocator.free(markdown);
const parsed = std.mem.indexOf(u8, markdown, "Parsed as a number") != null;
const rejected = std.mem.indexOf(u8, markdown, "Does not parse") != null;
const parsed = std.mem.find(u8, markdown, "Parsed as a number") != null;
const rejected = std.mem.find(u8, markdown, "Does not parse") != null;
try testing.expect(parsed or rejected);
}
@ -578,7 +580,7 @@ test "long mode is preserved when re-parsing, so commas stay in the value" {
test "hovering a comment gives nothing" {
const text = "#!srfv1\n# a comment\nk::v\n";
const at = std.mem.indexOf(u8, text, "comment").?;
const at = std.mem.find(u8, text, "comment").?;
const position = positions.positionFromByte(text, at, .@"utf-8");
try testing.expectEqual(
@as(?Hover, null),
@ -605,7 +607,7 @@ test "hover reports the range of the thing it described" {
test "hover ranges use the negotiated encoding" {
const text = "#!srfv1\n#!long\ncost:num:¥15,000\n";
// Hover the value, whose line contains a two-byte character before it.
const at = std.mem.indexOf(u8, text, "15,000").?;
const at = std.mem.find(u8, text, "15,000").?;
const utf8 = (try hoverAt(
testing.allocator,
@ -684,14 +686,14 @@ test "a long base64 text preview is truncated" {
const markdown = try hoverOn(text, b64);
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "decoding to 60 bytes") != null);
try testing.expect(std.mem.indexOf(u8, markdown, "...") != null);
try testing.expect(std.mem.find(u8, markdown, "decoding to 60 bytes") != null);
try testing.expect(std.mem.find(u8, markdown, "...") != null);
}
test "empty base64 decodes to nothing without a preview" {
const markdown = try hoverOn("#!srfv1\nblob:binary:\n", "binary");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "No value") != null);
try testing.expect(std.mem.find(u8, markdown, "No value") != null);
}
test "looksLikeText accepts text and rejects control bytes" {
@ -705,7 +707,7 @@ test "looksLikeText accepts text and rejects control bytes" {
test "an overrun length in long format suggests the corrected count" {
const text = "#!srfv1\n#!long\nk:3:hello\n";
try expectHoverContains(text, "k:3:", "declares 3 bytes, but 2 more bytes follow");
try expectHoverContains(text, "k:3:", "declares 3 bytes, but the field runs 2 bytes longer");
try expectHoverContains(text, "k:3:", "Restating the length as 5");
try expectHoverContains(text, "k:3:", "optionally after whitespace or a comment");
}
@ -724,20 +726,20 @@ test "an overrun length in compact format explains the terminator rule" {
test "a trailing comment in long format is not reported as a mismatch" {
const markdown = try hoverOn("#!srfv1\n#!long\nk:5:hello # fine\n", "hello");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "Length mismatch") == null);
try testing.expect(std.mem.find(u8, markdown, "Length mismatch") == null);
}
test "a correct length prefix reports no mismatch" {
const markdown = try hoverOn("#!srfv1\n#!long\nk:5:hello\n", "hello");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "Length mismatch") == null);
try testing.expect(std.mem.find(u8, markdown, "Length mismatch") == null);
}
test "a correct length prefix with commas in compact format reports no mismatch" {
const markdown = try hoverOn("#!srfv1\nk:5:a,b,c,next::x\n", "a,b,c");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "Length mismatch") == null);
try testing.expect(std.mem.indexOf(u8, markdown, "a,b,c") != null);
try testing.expect(std.mem.find(u8, markdown, "Length mismatch") == null);
try testing.expect(std.mem.find(u8, markdown, "a,b,c") != null);
}
test "an untyped numeric-looking value carries the coercion caveat" {
@ -752,13 +754,13 @@ test "the caveat fires on the shape that broke a real consumer" {
test "a non-numeric string carries no caveat" {
const markdown = try hoverOn("#!srfv1\nname::alice\n", "alice");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "strings_to_numbers") == null);
try testing.expect(std.mem.find(u8, markdown, "strings_to_numbers") == null);
}
test "an explicitly typed number carries no caveat" {
const markdown = try hoverOn("#!srfv1\nage:num:30\n", "30");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "strings_to_numbers") == null);
try testing.expect(std.mem.find(u8, markdown, "strings_to_numbers") == null);
}
test "an explicit string hint gets the declared wording" {
@ -773,7 +775,7 @@ test "a length-prefixed numeric value gets the caveat too" {
test "a bool value carries no caveat" {
const markdown = try hoverOn("#!srfv1\nk:bool:true\n", "true");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "strings_to_numbers") == null);
try testing.expect(std.mem.find(u8, markdown, "strings_to_numbers") == null);
}
test "the caveat covers negative and floating point values" {
@ -784,7 +786,7 @@ test "the caveat covers negative and floating point values" {
test "a multi-line length-prefixed value carries no caveat" {
const markdown = try hoverOn("#!srfv1\n#!long\nbio:7:foo\nbar\n", "foo");
defer testing.allocator.free(markdown);
try testing.expect(std.mem.indexOf(u8, markdown, "strings_to_numbers") == null);
try testing.expect(std.mem.find(u8, markdown, "strings_to_numbers") == null);
}
test "the caveat appears on the key as well as the value" {
@ -803,9 +805,28 @@ test "hovering the punctuation between key and value gives nothing" {
test "the caveat is one line, not a paragraph" {
const markdown = try hoverOn("#!srfv1\nk::32\n", "k::");
defer testing.allocator.free(markdown);
const at = std.mem.indexOf(u8, markdown, "Untyped, so this").?;
const at = std.mem.find(u8, markdown, "Untyped, so this").?;
const rest = markdown[at..];
const line_end = std.mem.indexOfScalar(u8, rest, '\n').?;
const line_end = std.mem.findScalar(u8, rest, '\n').?;
// A single sentence on a single line.
try testing.expect(line_end < 120);
}
test "hover's suggested length matches srf's diagnostic exactly" {
// The two are computed independently -- srf's `likelyExtraBytes` and our
// `likelySurplusEnd` -- so they can drift. These are the numbers srf emits for
// the same documents; if either side changes its rule, this fails.
try expectHoverContains(
"#!srfv1\nim_worth:4:23,000,000,000,really:bool:false\n",
"im_worth",
"Restating the length as 14",
);
try expectHoverContains("#!srfv1\nk:2:a:b,c,next::v\n", "k:2:", "Restating the length as 5");
try expectHoverContains("#!srfv1\nk:3:http://x,next::v\n", "k:3:", "Restating the length as 8");
try expectHoverContains("#!srfv1\nk:2:abc\n", "k:2:", "Restating the length as 3");
try expectHoverContains("#!srfv1\n#!long\nim_worth:4:23,000,000\n", "im_worth", "Restating the length as 10");
}
test "a single surplus byte reads grammatically" {
try expectHoverContains("#!srfv1\nk:2:abc\n", "k:2:", "the field runs 1 byte longer");
}

View file

@ -41,7 +41,7 @@ pub fn lineAt(text: []const u8, index: u32) []const u8 {
var remaining = text;
var i: u32 = 0;
while (true) {
const nl = std.mem.indexOfScalar(u8, remaining, '\n');
const nl = std.mem.findScalar(u8, remaining, '\n');
if (i == index) {
const line = if (nl) |n| remaining[0..n] else remaining;
return std.mem.trimEnd(u8, line, "\r");
@ -61,7 +61,7 @@ pub fn lineStart(text: []const u8, index: u32) usize {
var offset: usize = 0;
var i: u32 = 0;
while (i < index) : (i += 1) {
const nl = std.mem.indexOfScalarPos(u8, text, offset, '\n') orelse return text.len;
const nl = std.mem.findScalarPos(u8, text, offset, '\n') orelse return text.len;
offset = nl + 1;
}
return @min(offset, text.len);
@ -131,7 +131,7 @@ pub fn positionFromByte(text: []const u8, offset: usize, encoding: Encoding) Pos
const limit = @min(offset, text.len);
var line: u32 = 0;
var i: usize = 0;
while (std.mem.indexOfScalarPos(u8, text, i, '\n')) |nl| {
while (std.mem.findScalarPos(u8, text, i, '\n')) |nl| {
if (nl >= limit) break;
line += 1;
i = nl + 1;
@ -269,7 +269,7 @@ test "positionFromByte reports the right line" {
test "rangeFromSpan converts both ends" {
const text = "#!srfv1\nname::alice\n";
const at = std.mem.indexOf(u8, text, "alice").?;
const at = std.mem.find(u8, text, "alice").?;
const range = rangeFromSpan(text, at, at + 5, .@"utf-8");
try testing.expectEqual(@as(u32, 1), range.start.line);
try testing.expectEqual(@as(u32, 6), range.start.character);