diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/dump.zig | 2 | ||||
| -rw-r--r-- | src/locations.zig | 121 | ||||
| -rw-r--r-- | src/locations_config.zig | 8 | ||||
| -rw-r--r-- | src/panes.zig | 69 | ||||
| -rw-r--r-- | src/pardes.zig | 2 | ||||
| -rw-r--r-- | src/syntax.zig | 111 |
6 files changed, 230 insertions, 83 deletions
diff --git a/src/dump.zig b/src/dump.zig index 0c538a1a..89abd995 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -75,7 +75,7 @@ pub const Terminal = struct { }; pub const LocationRow = struct { - kind: enum { match, context }, + kind: enum { match, context, preview }, declaration: bool = false, path: []const u8, line: usize, diff --git a/src/locations.zig b/src/locations.zig index 0f580e34..97caf1d9 100644 --- a/src/locations.zig +++ b/src/locations.zig @@ -15,7 +15,7 @@ pub fn isContextLine(line: []const u8) bool { } pub const Row = struct { - kind: enum { match, context }, + kind: enum { match, context, preview }, declaration: bool = false, path: []u8, at: look.Spot, @@ -44,15 +44,16 @@ pub fn equalRows(a: []const Row, b: []const Row) bool { /// Compare native producer rows with an already formatted, context-free view. /// Identical refreshes retain allocations, syntax and selection state. -pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) bool { +pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row, layout: Config.Layout) bool { if (rows.len == 0) return std.mem.eql(u8, input, rendered); - if (std.mem.endsWith(u8, input, "\n") != std.mem.endsWith(u8, rendered, "\n")) return false; var raw_lines = std.mem.splitScalar(u8, input, '\n'); var shown_lines = std.mem.splitScalar(u8, rendered, '\n'); var width: usize = 0; - for (rows, 0..) |row, index| { + var index: usize = 0; + while (index < rows.len) { + const row = rows[index]; if (row.kind != .match) return false; - if (index % alignment_entries == 0) { + if (layout == .@"inline" and index % alignment_entries == 0) { width = 0; var chunk = raw_lines; for (0..alignment_entries) |_| { @@ -64,9 +65,19 @@ pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) boo const raw = raw_lines.next() orelse return false; const shown = shown_lines.next() orelse return false; const target = parseRaw(raw); - if (target.end != row.location_end or row.code_start > shown.len or - !std.mem.eql(u8, raw[0..target.end], shown[0..row.location_end]) or - !std.mem.eql(u8, raw[target.code_start..], shown[row.code_start..])) return false; + if (target.end != row.location_end or row.location_end > shown.len or row.code_start > shown.len or + !std.mem.eql(u8, raw[0..target.end], shown[0..row.location_end])) return false; + if (layout == .stacked) { + if (shown.len != row.location_end or row.code_start != shown.len or index + 1 >= rows.len) return false; + const preview = rows[index + 1]; + if (preview.kind != .preview or preview.location_end != 0 or preview.code_start != 0 or + !std.meta.eql(row.at, preview.at) or !std.mem.eql(u8, row.path, preview.path)) return false; + const code = shown_lines.next() orelse return false; + if (!std.mem.eql(u8, raw[target.code_start..], code)) return false; + index += 2; + continue; + } + if (!std.mem.eql(u8, raw[target.code_start..], shown[row.code_start..])) return false; // A restored result may still have the old pane-wide padding. Do not // let the identical-input shortcut retain that layout when rerun. if (row.code_start <= row.location_end) return false; @@ -74,9 +85,14 @@ pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) boo if (padding.len != width - pardes.panes.File.displayWidth(raw[0..target.end]) + 1 or padding[padding.len - 1] != '\t') return false; for (padding[0 .. padding.len - 1]) |byte| if (byte != ' ') return false; + index += 1; } - if (raw_lines.next()) |tail| if (tail.len != 0) return false; - return raw_lines.next() == null; + const raw_tail = raw_lines.next(); + const shown_tail = shown_lines.next(); + if ((raw_tail == null) != (shown_tail == null)) return false; + if (raw_tail) |tail| if (tail.len != 0) return false; + if (shown_tail) |tail| if (tail.len != 0) return false; + return raw_lines.next() == null and shown_lines.next() == null; } pub const Parsed = struct { @@ -246,44 +262,73 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us rows.deinit(p.gpa); } var mapped = anchor; - first = 0; - while (first < pending.items.len) { - const end = alignmentEnd(pending.items, first); - var width: usize = 0; - for (pending.items[first..end]) |row| width = @max(width, labelWidth(row)); - for (pending.items[first..end], first..) |row, index| { - const before = out.written().len; + if (p.locations_config.layout == .stacked) { + for (pending.items, 0..) |row, index| { const context = row.original == null; - try out.writer.writeAll(row.label); - const location_end = if (row.hidden) 0 else out.written().len - before; - if (row.depth > 0) { - try out.writer.writeByte(' '); - try out.writer.splatByteAll('*', row.depth); - } - try out.writer.splatByteAll(' ', width - labelWidth(row)); - try out.writer.writeByte('\t'); - const code_start = out.written().len - before; - try out.writer.writeAll(row.code); - if (row.declaration and index + 1 < pending.items.len) { - const next = pending.items[index + 1]; - if (std.mem.eql(u8, row.path, next.path) and next.at.line -| row.at.line > 1) - try out.writer.writeAll(" ..."); + if (anchor != null and row.original == anchor) mapped = rows.items.len; + if (!row.hidden) { + const before = out.written().len; + try out.writer.writeAll(row.label); + if (row.depth > 0) { + try out.writer.writeByte(' '); + try out.writer.splatByteAll('*', row.depth); + } + try appendMetadata(p.gpa, &rows, row, if (context) .context else .match, row.label.len, out.written().len - before, false); + try out.writer.writeByte('\n'); } + try writeCode(&out.writer, pending.items, index); + try appendMetadata(p.gpa, &rows, row, if (context) .context else .preview, 0, 0, true); if (index + 1 < pending.items.len or std.mem.endsWith(u8, input, "\n")) try out.writer.writeByte('\n'); - const owned_path = try p.gpa.dupe(u8, row.path); - errdefer p.gpa.free(owned_path); - const owned_colors = try p.gpa.dupe(u8, row.colors); - errdefer p.gpa.free(owned_colors); - try rows.append(p.gpa, .{ .kind = if (context) .context else .match, .declaration = row.declaration, .path = owned_path, .at = row.at, .location_end = location_end, .code_start = code_start, .colors = owned_colors }); - if (anchor != null and row.original == anchor) mapped = index; } - first = end; + } else { + first = 0; + while (first < pending.items.len) { + const end = alignmentEnd(pending.items, first); + var width: usize = 0; + for (pending.items[first..end]) |row| width = @max(width, labelWidth(row)); + for (pending.items[first..end], first..) |row, index| { + const before = out.written().len; + const context = row.original == null; + try out.writer.writeAll(row.label); + const location_end = if (row.hidden) 0 else out.written().len - before; + if (row.depth > 0) { + try out.writer.writeByte(' '); + try out.writer.splatByteAll('*', row.depth); + } + try out.writer.splatByteAll(' ', width - labelWidth(row)); + try out.writer.writeByte('\t'); + const code_start = out.written().len - before; + try writeCode(&out.writer, pending.items, index); + if (index + 1 < pending.items.len or std.mem.endsWith(u8, input, "\n")) try out.writer.writeByte('\n'); + try appendMetadata(p.gpa, &rows, row, if (context) .context else .match, location_end, code_start, true); + if (anchor != null and row.original == anchor) mapped = index; + } + first = end; + } } const owned_rows = try rows.toOwnedSlice(p.gpa); errdefer freeRows(p.gpa, owned_rows); return .{ .content = try out.toOwnedSlice(), .rows = owned_rows, .anchor = mapped }; } +fn writeCode(writer: *std.Io.Writer, rows: []const Pending, index: usize) !void { + const row = rows[index]; + try writer.writeAll(row.code); + if (row.declaration and index + 1 < rows.len) { + const next = rows[index + 1]; + if (std.mem.eql(u8, row.path, next.path) and next.at.line -| row.at.line > 1) + try writer.writeAll(" ..."); + } +} + +fn appendMetadata(gpa: std.mem.Allocator, rows: *std.ArrayList(Row), pending: Pending, kind: @FieldType(Row, "kind"), location_end: usize, code_start: usize, source: bool) !void { + const path = try gpa.dupe(u8, pending.path); + errdefer gpa.free(path); + const colors = try gpa.dupe(u8, if (source) pending.colors else &.{}); + errdefer gpa.free(colors); + try rows.append(gpa, .{ .kind = kind, .declaration = pending.declaration, .path = path, .at = pending.at, .location_end = location_end, .code_start = code_start, .colors = colors }); +} + const alignment_entries = 8; /// Keep neighboring context with the nearer match at a chunk boundary. Shared diff --git a/src/locations_config.zig b/src/locations_config.zig index ed67f990..6b23575a 100644 --- a/src/locations_config.zig +++ b/src/locations_config.zig @@ -1,9 +1,11 @@ const std = @import("std"); pub const Config = struct { + pub const Layout = enum { @"inline", stacked }; context: u16 = 0, tscontext: bool = false, tslocations: bool = true, + layout: Layout = .stacked, /// Apply only supplied fields. A malformed token rejects the whole update. /// Repeating a field uses its last supplied value. @@ -20,6 +22,7 @@ pub const Config = struct { @field(result, field.name) = switch (@typeInfo(field.type)) { .bool => if (std.mem.eql(u8, value, "on")) true else if (std.mem.eql(u8, value, "off")) false else return error.ExpectedOnOrOff, .int => std.fmt.parseInt(field.type, value, 10) catch return error.InvalidContext, + .@"enum" => std.meta.stringToEnum(field.type, value) orelse return error.InvalidLayout, else => @compileError("unsupported location setting type"), }; found = true; @@ -38,6 +41,7 @@ pub const Config = struct { switch (@typeInfo(field.type)) { .bool => try writer.writeAll(if (value) "on" else "off"), .int => try writer.print("{d}", .{value}), + .@"enum" => try writer.writeAll(@tagName(value)), else => @compileError("unsupported location setting type"), } } @@ -52,10 +56,11 @@ test "LocationsConfig reflection round trip and partial updates" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit(); try config.write(&out.writer); - try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off", out.written()); + try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off layout:stacked", out.written()); try std.testing.expectEqual(config, try (Config{}).parse(out.written())); try std.testing.expectEqual(config, try config.parse(" \n\t")); try std.testing.expectEqual(@as(u16, 2), (try config.parse("context:1 context:2")).context); + try std.testing.expectEqual(Config.Layout.@"inline", (try config.parse("layout:inline")).layout); } test "LocationsConfig rejects invalid updates atomically" { @@ -63,6 +68,7 @@ test "LocationsConfig rejects invalid updates atomically" { try std.testing.expectError(error.UnknownLocationSetting, config.parse("context:5 unknown:on")); try std.testing.expectError(error.ExpectedOnOrOff, config.parse("context:5 tscontext:true")); try std.testing.expectError(error.ExpectedKeyValue, config.parse("context")); + try std.testing.expectError(error.InvalidLayout, config.parse("context:5 layout:sideways")); for ([_][]const u8{ "context:", "context:-1", "context:65536", "context:five" }) |input| try std.testing.expectError(error.InvalidContext, config.parse(input)); try std.testing.expectEqual(@as(u16, 3), config.context); diff --git a/src/panes.zig b/src/panes.zig index 7855866d..59993148 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -1082,6 +1082,11 @@ pub const File = struct { return saved; } + fn sameSavedLocation(a: dump.LocationRow, b: dump.LocationRow) bool { + return std.mem.eql(u8, a.path, b.path) and a.line == b.line and a.col == b.col and + a.end_line == b.end_line and a.end_col == b.end_col; + } + fn restoreLocationRows(gpa: std.mem.Allocator, content: []const u8, saved: []const dump.LocationRow) ![]locations.Row { if (saved.len == 0) return &.{}; if (saved.len > std.mem.count(u8, content, "\n") + 1) return error.InvalidLocationRows; @@ -1095,16 +1100,33 @@ pub const File = struct { gpa.free(rows); } var lines = std.mem.splitScalar(u8, content, '\n'); - for (saved, rows) |row, *out| { + var previous_line: []const u8 = ""; + for (saved, rows, 0..) |row, *out, index| { const line = lines.next() orelse return error.InvalidLocationRows; if (row.path.len == 0 or row.path.len >= 4096 or - row.location_end > row.code_start or row.code_start == 0 or row.code_start > line.len or - line[row.code_start - 1] != '\t' or + row.location_end > row.code_start or row.code_start > line.len or (row.kind == .match and row.location_end == 0) or (row.declaration and row.kind != .context) or row.line > std.math.maxInt(i32) or row.col > std.math.maxInt(i32) or row.end_line > std.math.maxInt(i32) or row.end_col > std.math.maxInt(i32)) return error.InvalidLocationRows; + const inline_row = row.code_start > row.location_end and row.code_start > 0 and line[row.code_start - 1] == '\t'; + const address_row = !inline_row and row.location_end > 0 and row.code_start == line.len; + const source_row = row.location_end == 0 and row.code_start == 0; + if (!inline_row and !address_row and !source_row) return error.InvalidLocationRows; + if (row.kind == .preview) { + if (!source_row or index == 0) return error.InvalidLocationRows; + const previous = saved[index - 1]; + if (previous.kind != .match or previous.location_end == 0 or previous.code_start != previous_line.len or previous_line[previous_line.len - 1] == '\t' or + !sameSavedLocation(previous, row)) return error.InvalidLocationRows; + } + if (address_row) { + if (index + 1 >= saved.len) return error.InvalidLocationRows; + const following = saved[index + 1]; + const expected: @TypeOf(row.kind) = if (row.kind == .match) .preview else .context; + if (following.kind != expected or following.code_start != 0 or following.location_end != 0 or + following.declaration != row.declaration or !sameSavedLocation(row, following)) return error.InvalidLocationRows; + } const code_len = line.len - row.code_start; if (row.colors_b64.len > std.base64.standard.Encoder.calcSize(code_len)) return error.InvalidLocationRows; const colors = try dump.decodeBytes(gpa, row.colors_b64); @@ -1122,6 +1144,7 @@ pub const File = struct { .colors = colors, }; initialized += 1; + previous_line = line; } if (lines.next()) |remaining| if (remaining.len != 0 or lines.next() != null) return error.InvalidLocationRows; return rows; @@ -1149,6 +1172,32 @@ pub const File = struct { try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "| \tx\n", &.{row})); } + test "stacked location metadata requires matching adjacent preview ownership" { + const gpa = std.testing.allocator; + const header = "x.c:2:1-3"; + const content = header ++ "\nfoo\n"; + const match: dump.LocationRow = .{ .kind = .match, .path = "x.c", .line = 2, .col = 1, .end_line = 2, .end_col = 3, .location_end = header.len, .code_start = header.len }; + const preview: dump.LocationRow = .{ .kind = .preview, .path = "x.c", .line = 2, .col = 1, .end_line = 2, .end_col = 3, .location_end = 0, .code_start = 0 }; + const restored = try restoreLocationRows(gpa, content, &.{ match, preview }); + defer locations.freeRows(gpa, restored); + try std.testing.expectEqual(.preview, restored[1].kind); + try std.testing.expectEqual(restored[0].at, restored[1].at); + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "foo\n", &.{preview})); + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, header ++ "\n", &.{match})); + var inline_match = match; + inline_match.code_start += 1; + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, header ++ "\t\nfoo\n", &.{ inline_match, preview })); + var wrong = preview; + wrong.path = "other.c"; + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong })); + wrong = preview; + wrong.line = 3; + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong })); + wrong = preview; + wrong.code_start = 1; + try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong })); + } + pub fn graphemeDisplayWidth(grapheme: []const u8) usize { if (std.mem.eql(u8, grapheme, "\t")) return config.tab_width; if (grapheme.len == 1 and grapheme[0] >= 0x20 and grapheme[0] < 0x7f) return 1; @@ -2258,6 +2307,18 @@ pub const Output = struct { } } + test "stacked empty previews distinguish separators from trailing newlines" { + var path = [_]u8{ 'x', '.', 'c' }; + const rows = [_]locations.Row{ + .{ .kind = .match, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 7, .code_start = 7 }, + .{ .kind = .preview, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 0, .code_start = 0 }, + }; + try std.testing.expect(locations.sameInput("x.c:1:1 ", "x.c:1:1\n", &rows, .stacked)); + try std.testing.expect(locations.sameInput("x.c:1:1 \n", "x.c:1:1\n\n", &rows, .stacked)); + try std.testing.expect(!locations.sameInput("x.c:1:1 ", "x.c:1:1\n\n", &rows, .stacked)); + try std.testing.expect(!locations.sameInput("x.c:1:1 \n", "x.c:1:1\n", &rows, .stacked)); + } + test "plain output does not opt into location decoration" { const report = rowDecoration(.{ .from = .{ .cmd = .LocationsConfig } }, "LocationsConfig context:5 tscontext:on"); try std.testing.expectEqualDeep(RowDecoration{}, report); @@ -2527,7 +2588,7 @@ pub const Output = struct { if (!std.meta.eql(output.from, from)) continue; if (by_arg and !std.mem.eql(u8, output.arg(), arg)) continue; if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue; - if (!locations.sameInput(content, rf.content, rf.location_rows)) continue; + if (!locations.sameInput(content, rf.content, rf.location_rows, p.locations_config.layout)) continue; try setArg(output, arg); p.gpa.free(content); p.active = id; diff --git a/src/pardes.zig b/src/pardes.zig index d115bf2d..2729ef93 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1121,7 +1121,7 @@ test "LocationsConfig command reports partial updates and survives restore" { const out = file.output orelse continue; if (!std.meta.eql(out.from, panes.Output.Origin{ .cmd = .LocationsConfig })) continue; reports += 1; - try std.testing.expectEqualStrings("LocationsConfig context:5 tscontext:on tslocations:off\n", file.content); + try std.testing.expectEqualStrings("LocationsConfig context:5 tscontext:on tslocations:off layout:stacked\n", file.content); } try std.testing.expectEqual(@as(usize, 1), reports); try p.dumpState(); diff --git a/src/syntax.zig b/src/syntax.zig index bd6a64a0..9e31669f 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -630,48 +630,58 @@ pub fn highlightLocationRows(gpa: std.mem.Allocator, content: []const u8, rows: defer gpa.free(code); const colors = try gpa.alloc(u8, content.len); defer gpa.free(colors); - var painted = false; - var row_index: usize = 0; + const SourceRow = struct { index: usize, start: usize, len: usize }; + const sources = try gpa.alloc(SourceRow, rows.len); + defer gpa.free(sources); + var source_count: usize = 0; var offset: usize = 0; - while (row_index < rows.len and offset < content.len) { - const first = row_index; - const group_start = offset; - const selected = (forExt(std.fs.path.extension(rows[first].path)) catch null); - var code_len: usize = 0; - while (row_index < rows.len and offset < content.len) { - const row = rows[row_index]; - if (row_index > first and (!std.mem.eql(u8, row.path, rows[first].path) or - row.at.line != rows[row_index - 1].at.line +| 1)) break; - const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len; - const source_start = offset + @min(row.code_start, end - offset); - if (row_index > first) { - code[code_len] = '\n'; - code_len += 1; - } - @memcpy(code[code_len..][0 .. end - source_start], content[source_start..end]); - code_len += end - source_start; - offset = @min(end + 1, content.len); - row_index += 1; + for (rows, 0..) |row, index| { + if (offset >= content.len) break; + const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len; + const source_start = offset + @min(row.code_start, end - offset); + var address_only = false; + if (comptime @hasField(@TypeOf(row), "location_end")) { + address_only = row.location_end > 0 and source_start == end; } - const language = selected orelse continue; - if (offset > 0 and content[offset - 1] == '\n') { - code[code_len] = '\n'; - code_len += 1; + if (!address_only) { + sources[source_count] = .{ .index = index, .start = source_start, .len = end - source_start }; + source_count += 1; } - if (code_len == 0) continue; - @memset(colors[0..code_len], 0); - paint(colors[0..code_len], code[0..code_len], language); - painted = true; - var target = group_start; - var source_offset: usize = 0; - for (rows[first..row_index]) |row| { - const end = std.mem.indexOfScalarPos(u8, content, target, '\n') orelse content.len; - const source_start = target + @min(row.code_start, end - target); - const len = end - source_start; - @memcpy(styles[source_start..end], colors[source_offset..][0..len]); - source_offset += len + 1; - target = @min(end + 1, content.len); + offset = @min(end + 1, content.len); + } + var painted = false; + var first: usize = 0; + while (first < source_count) { + const first_row = rows[sources[first].index]; + var end = first + 1; + while (end < source_count) : (end += 1) { + const row = rows[sources[end].index]; + const previous = rows[sources[end - 1].index]; + if (!std.mem.eql(u8, row.path, first_row.path) or row.at.line != previous.at.line +| 1) break; } + const language = (forExt(std.fs.path.extension(first_row.path)) catch null); + if (language) |selected| { + var code_len: usize = 0; + for (sources[first..end]) |source| { + @memcpy(code[code_len..][0..source.len], content[source.start..][0..source.len]); + code_len += source.len; + if (code_len < code.len) { + code[code_len] = '\n'; + code_len += 1; + } + } + if (code_len > 0) { + @memset(colors[0..code_len], 0); + paint(colors[0..code_len], code[0..code_len], selected); + painted = true; + var at: usize = 0; + for (sources[first..end]) |source| { + @memcpy(styles[source.start..][0..source.len], colors[at..][0..source.len]); + at += source.len + 1; + } + } + } + first = end; } // A shown context range can begin inside a comment/string whose opener was // omitted. Formatter snapshots from the complete source take precedence, @@ -1244,3 +1254,28 @@ test "syntax declaration locations clear snapshots without muting ordinary conte for (styles[3..][0..header.len]) |style| try std.testing.expectEqual(@as(u8, 0), style); try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "42").?]); } + +test "syntax stacked addresses do not split multiline source groups" { + if (!enabled or (!minimal_grammars and !full_grammars)) return; + const gpa = std.testing.allocator; + start(gpa); + defer stop(); + const Row = struct { path: []const u8 = "a.c", code_start: usize, location_end: usize, at: look.Spot }; + const content = "a.c:1:1\n/* open\na.c:2:1\nint value = 42;\na.c:3:1\n*/ int other = 7;\n"; + const rows = [_]Row{ + .{ .code_start = 7, .location_end = 7, .at = .{ .line = 1 } }, + .{ .code_start = 0, .location_end = 0, .at = .{ .line = 1 } }, + .{ .code_start = 7, .location_end = 7, .at = .{ .line = 2 } }, + .{ .code_start = 0, .location_end = 0, .at = .{ .line = 2 } }, + .{ .code_start = 7, .location_end = 7, .at = .{ .line = 3 } }, + .{ .code_start = 0, .location_end = 0, .at = .{ .line = 3 } }, + }; + const styles = try highlightLocationRows(gpa, content, &rows); + defer gpa.free(styles); + try std.testing.expectEqual(@intFromEnum(Syn.comment), styles[std.mem.indexOf(u8, content, "42").?]); + try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "7;").?]); + for ([_][]const u8{ "a.c:1:1", "a.c:2:1", "a.c:3:1" }) |label| { + const at = std.mem.indexOf(u8, content, label).?; + for (styles[at..][0..label.len]) |style| try std.testing.expectEqual(@intFromEnum(Syn.none), style); + } +} |
