Skip to content

Switch to using byte ranges for highlighting in html snapshot generation #7935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 15 additions & 21 deletions src/base/SExpr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ pub const Color = enum {
node_name,
string,
number,
region,
punctuation,
};

Expand Down Expand Up @@ -52,10 +51,10 @@ const PlainTextSExprWriter = struct {
// No-op for plain text
}

pub fn beginSourceRange(self: *@This(), start_token: u32, end_token: u32) !void {
pub fn beginSourceRange(self: *@This(), start_byte: u32, end_byte: u32) !void {
_ = self;
_ = start_token;
_ = end_token;
_ = start_byte;
_ = end_byte;
// No-op for plain text
}

Expand Down Expand Up @@ -92,7 +91,6 @@ const HtmlSExprWriter = struct {
.node_name => "token-keyword", // Node names are like keywords in S-expressions
.string => "token-string",
.number => "token-number",
.region => "token-comment", // Regions are metadata, similar to comments
.punctuation => "token-punctuation",
};
try self.writer.print("<span class=\"{s}\">", .{css_class});
Expand All @@ -102,8 +100,8 @@ const HtmlSExprWriter = struct {
self.current_color = color;
}

pub fn beginSourceRange(self: *@This(), start_token: u32, end_token: u32) !void {
try self.writer.print("<span class=\"source-range\" data-start-token=\"{d}\" data-end-token=\"{d}\" >", .{ start_token, end_token });
pub fn beginSourceRange(self: *@This(), start_byte: u32, end_byte: u32) !void {
try self.writer.print("<span class=\"source-range\" data-start-byte=\"{d}\" data-end-byte=\"{d}\" >", .{ start_byte, end_byte });
}

pub fn endSourceRange(self: *@This()) !void {
Expand All @@ -125,10 +123,10 @@ pub const AttributeValue = union(enum) {
node_idx: u32,
region: RegionInfo,
raw_string: []const u8, // for unquoted strings
tokens_range: struct {
bytes_range: struct {
region: RegionInfo,
start_token: u32,
end_token: u32,
start_byte: u32,
end_byte: u32,
},
};

Expand Down Expand Up @@ -168,7 +166,7 @@ pub fn deinit(self: *SExpr, gpa: Allocator) void {
gpa.free(r.line_text);
}
},
.tokens_range => |tr| {
.bytes_range => |tr| {
// Free the region line text if it's not empty
if (tr.region.line_text.len > 0) {
gpa.free(tr.region.line_text);
Expand Down Expand Up @@ -242,18 +240,18 @@ pub fn appendBoolAttr(self: *SExpr, gpa: Allocator, key: []const u8, value: bool
}

/// Append a token range attribute with region information
pub fn appendTokenRange(self: *SExpr, gpa: Allocator, region: RegionInfo, start_token: u32, end_token: u32) void {
pub fn appendByteRange(self: *SExpr, gpa: Allocator, region: RegionInfo, start_byte: u32, end_byte: u32) void {
const owned_value = gpa.dupe(u8, region.line_text) catch |err| exitOnOom(err);
self.addAttribute(gpa, "tokens", .{ .tokens_range = .{
self.addAttribute(gpa, "tokens", .{ .bytes_range = .{
.region = RegionInfo{
.start_line_idx = region.start_line_idx,
.start_col_idx = region.start_col_idx,
.end_line_idx = region.end_line_idx,
.end_col_idx = region.end_col_idx,
.line_text = owned_value,
},
.start_token = start_token,
.end_token = end_token,
.start_byte = start_byte,
.end_byte = end_byte,
} });
}

Expand Down Expand Up @@ -325,28 +323,24 @@ fn toStringImpl(node: SExpr, writer_impl: anytype, indent: usize) !void {
},
.region => |r| {
try writer_impl.print(" ", .{});
try writer_impl.setColor(.region);
try writer_impl.print("@{d}.{d}-{d}.{d}", .{
// add one to display numbers instead of index
r.start_line_idx + 1,
r.start_col_idx + 1,
r.end_line_idx + 1,
r.end_col_idx + 1,
});
try writer_impl.setColor(.default);
},
.tokens_range => |tr| {
.bytes_range => |tr| {
try writer_impl.print(" ", .{});
try writer_impl.beginSourceRange(tr.start_token, tr.end_token);
try writer_impl.setColor(.region);
try writer_impl.beginSourceRange(tr.start_byte, tr.end_byte);
try writer_impl.print("@{d}.{d}-{d}.{d}", .{
// add one to display numbers instead of index
tr.region.start_line_idx + 1,
tr.region.start_col_idx + 1,
tr.region.end_line_idx + 1,
tr.region.end_col_idx + 1,
});
try writer_impl.setColor(.default);
try writer_impl.endSourceRange();
},
}
Expand Down
27 changes: 16 additions & 11 deletions src/check/canonicalize/CIR.zig
Original file line number Diff line number Diff line change
Expand Up @@ -930,25 +930,19 @@ pub const IntValue = struct {
}
};

/// Helper function to convert the entire Canonical IR to a string in S-expression format
/// and write it to the given writer.
///
/// If a single expression is provided we only print that expression
pub fn toSExprStr(ir: *CIR, writer: std.io.AnyWriter, maybe_expr_idx: ?Expr.Idx, source: []const u8) !void {
/// Helper function to generate the S-expression node for the entire Canonical IR.
/// If a single expression is provided, only that expression is returned.
pub fn toSExpr(ir: *CIR, maybe_expr_idx: ?Expr.Idx, source: []const u8) SExpr {
// Set temporary source for region info calculation during SExpr generation
ir.temp_source_for_sexpr = source;
defer ir.temp_source_for_sexpr = null;
const gpa = ir.env.gpa;

if (maybe_expr_idx) |expr_idx| {
// Get the expression from the store
var expr_node = ir.store.getExpr(expr_idx).toSExpr(ir);
defer expr_node.deinit(gpa);

expr_node.toStringPretty(writer);
return ir.store.getExpr(expr_idx).toSExpr(ir);
} else {
var root_node = SExpr.init(gpa, "can-ir");
defer root_node.deinit(gpa);

// Iterate over all the definitions in the file and convert each to an S-expression
const defs_slice = ir.store.sliceDefs(ir.all_defs);
Expand All @@ -968,10 +962,21 @@ pub fn toSExprStr(ir: *CIR, writer: std.io.AnyWriter, maybe_expr_idx: ?Expr.Idx,
root_node.appendNode(gpa, &stmt_node);
}

root_node.toStringPretty(writer);
return root_node;
}
}

/// Helper function to convert the entire Canonical IR to a string in S-expression format
/// and write it to the given writer.
///
/// If a single expression is provided we only print that expression
pub fn toSExprStr(ir: *CIR, env: *ModuleEnv, writer: std.io.AnyWriter, maybe_expr_idx: ?Expr.Idx, source: []const u8) !void {
const gpa = ir.env.gpa;
var node = toSExpr(ir, env, maybe_expr_idx, source);
defer node.deinit(gpa);
node.toStringPretty(writer);
}

test "NodeStore - init and deinit" {
var store = CIR.NodeStore.init(testing.allocator);
defer store.deinit();
Expand Down
27 changes: 18 additions & 9 deletions src/check/parse/AST.zig
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,20 @@ pub fn calcRegionInfo(self: *AST, region: TokenizedRegion, line_starts: []const

/// Append region information to an S-expression node for diagnostics
pub fn appendRegionInfoToSexprNode(self: *AST, env: *base.ModuleEnv, node: *SExpr, region: TokenizedRegion) void {
node.appendTokenRange(
const start = self.tokens.resolve(region.start);
const end = self.tokens.resolve(region.end);
const info: base.RegionInfo = base.RegionInfo.position(self.source, env.line_starts.items, start.start.offset, end.end.offset) catch .{
.start_line_idx = 0,
.start_col_idx = 0,
.end_line_idx = 0,
.end_col_idx = 0,
.line_text = "",
};
node.appendByteRange(
env.gpa,
self.calcRegionInfo(region, env.line_starts.items),
region.start,
region.end,
info,
start.start.offset,
end.end.offset,
);
}

Expand Down Expand Up @@ -756,13 +765,13 @@ pub const Statement = union(enum) {
const header_node = ast.store.nodes.get(@enumFromInt(@intFromEnum(a.header)));
if (header_node.tag == .malformed) {
// Handle malformed type header by creating a placeholder
header.appendRegion(env.gpa, ast.calcRegionInfo(header_node.region, env.line_starts.items));
ast.appendRegionInfoToSexprNode(env, &header, header_node.region);
header.appendStringAttr(env.gpa, "name", "<malformed>");
var args_node = SExpr.init(env.gpa, "args");
header.appendNode(env.gpa, &args_node);
} else {
const ty_header = ast.store.getTypeHeader(a.header);
header.appendRegion(env.gpa, ast.calcRegionInfo(ty_header.region, env.line_starts.items));
ast.appendRegionInfoToSexprNode(env, &header, ty_header.region);
header.appendStringAttr(env.gpa, "name", ast.resolve(ty_header.name));

var args_node = SExpr.init(env.gpa, "args");
Expand Down Expand Up @@ -830,7 +839,7 @@ pub const Statement = union(enum) {
.@"return" => |a| {
var node = SExpr.init(env.gpa, "s-return");

node.appendRegion(env.gpa, ast.calcRegionInfo(a.region, env.line_starts.items));
ast.appendRegionInfoToSexprNode(env, &node, a.region);

var child = ast.store.getExpr(a.expr).toSExpr(env, ast);
node.appendNode(env.gpa, &child);
Expand Down Expand Up @@ -1074,7 +1083,7 @@ pub const Pattern = union(enum) {
},
.as => |a| {
var node = SExpr.init(env.gpa, "p-as");
node.appendRegion(env.gpa, ast.calcRegionInfo(a.region, env.line_starts.items));
ast.appendRegionInfoToSexprNode(env, &node, a.region);

var pattern_node = ast.store.getPattern(a.pattern).toSExpr(env, ast);
node.appendStringAttr(env.gpa, "name", ast.resolve(a.name));
Expand Down Expand Up @@ -1419,7 +1428,7 @@ pub const ExposedItem = union(enum) {
.malformed => |m| {
var node = SExpr.init(env.gpa, "exposed-malformed");
node.appendStringAttr(env.gpa, "reason", @tagName(m.reason));
node.appendRegion(env.gpa, ast.calcRegionInfo(m.region, env.line_starts.items));
ast.appendRegionInfoToSexprNode(env, &node, m.region);
return node;
},
}
Expand Down
36 changes: 27 additions & 9 deletions src/snapshot.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,40 @@ body {
background-color: #f0f0f0;
}
.highlighted {
background-color: #ffffcc !important;
outline: 2px solid #ffd700 !important;
background-color: #ffffcc;
outline: 2px solid #ffd700;
}

/* Source range highlighting for PARSE tree */
.source-range {
cursor: pointer;
transition: background-color 0.2s ease;
}
.source-range:hover {
background-color: #f0f0f0;
text-decoration: underline;
}

/* Byte range highlighting */
.highlight {
background-color: #ffffcc;
border-bottom: 2px solid #ffd700;
}

/* Flash animation for click highlighting */
@keyframes flash {
@keyframes flash-underline {
0%,
50%,
100% {
background-color: #ffffcc;
outline: 2px solid #ffd700;
border-bottom-color: #ffd700;
}
25%,
75% {
50% {
background-color: #ffeb3b;
outline: 2px solid #ff9800;
border-bottom-color: #ff9800;
}
}
.flash-highlight {
animation: flash 0.3s ease-in-out 2;
animation: flash-underline 0.3s ease-in-out 2;
}

/* Syntax highlighting */
Expand Down Expand Up @@ -153,6 +167,10 @@ body {
.token-default {
color: #000000;
}
.source-range {
color: #008000;
font-style: italic;
}

/* Hidden data storage */
.hidden {
Expand Down
Loading
Loading