diff --git a/build.zig.zon b/build.zig.zon index bf92742..f1fbebd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 = .{ diff --git a/nix/package.nix b/nix/package.nix index 4bac32c..2abd28b 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -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 = '' diff --git a/src/Server.zig b/src/Server.zig index 1ae2301..c8ebb5b 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -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; } diff --git a/src/analysis.zig b/src/analysis.zig index ffe530d..bcb3e3c 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -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); } } diff --git a/src/document.zig b/src/document.zig index e50baea..ecb308a 100644 --- a/src/document.zig +++ b/src/document.zig @@ -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" { diff --git a/src/hover.zig b/src/hover.zig index ff1856e..eac5aab 100644 --- a/src/hover.zig +++ b/src/hover.zig @@ -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"); +} diff --git a/src/positions.zig b/src/positions.zig index 1e55aaf..0fe9396 100644 --- a/src/positions.zig +++ b/src/positions.zig @@ -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);