mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-21 02:59:36 -06:00
feat(dx): lsp repositioning using sm
commit 1c89576ef4932f511a040aeca723745487d8a73c
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 16:58:19 2026 +0600
sync
commit 0ee4cb79e3bcbe9990411f74b8b90402d1b57ffd
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 16:06:20 2026 +0600
sync
commit 06630e1565d920ee91432174a91903086a315950
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 15:41:39 2026 +0600
sync
commit f4c17b80756c8cfe1183ec2b3057893afbd78889
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 15:07:17 2026 +0600
sync
commit bb534f820d8e00f4e476a1c6b0fae545f361f8c4
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 14:52:08 2026 +0600
sync
commit 424b1219bf469688bd6d7e56ebd0b4428bef6255
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 14:15:29 2026 +0600
sync
commit 7c0a3705fc6127aadb5db79dfb79ea38c4d2a6b8
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 13:53:16 2026 +0600
sync
commit 1ef5afc45465eab4d4400e3b2f4521a766c42a5a
Author: Nurul Huda (Apon) <me@nurulhudaapon.com>
Date: Sun Mar 22 13:34:32 2026 +0600
sync
This commit is contained in:
parent
1b285ab93d
commit
6e23df23c3
@ -94,6 +94,8 @@ pub const TranspileContext = struct {
|
||||
zx_initialized: bool = false,
|
||||
/// Track client components (components with @rendering attribute)
|
||||
client_components: std.ArrayList(ClientComponentMetadata),
|
||||
/// Optional position of an open parenthesis in source to map the next generated parenthesis to
|
||||
paren_byte: ?u32 = null,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub const TranspileOptions = struct {
|
||||
@ -143,6 +145,17 @@ pub const TranspileContext = struct {
|
||||
try self.writeWithMapping(bytes, pos.line, pos.column);
|
||||
}
|
||||
|
||||
fn addMapping(self: *TranspileContext, source_byte: u32, ast: *const Ast) !void {
|
||||
if (!self.track_mappings) return;
|
||||
const pos = ast.getLineColumn(source_byte);
|
||||
try self.sourcemap_builder.addMapping(.{
|
||||
.generated_line = self.current_line,
|
||||
.generated_column = self.current_column,
|
||||
.source_line = pos.line,
|
||||
.source_column = pos.column,
|
||||
});
|
||||
}
|
||||
|
||||
fn updatePosition(self: *TranspileContext, bytes: []const u8) void {
|
||||
for (bytes) |byte| {
|
||||
if (byte == '\n') {
|
||||
@ -448,6 +461,24 @@ pub fn transpileReturn(self: *Ast, node: ts.Node, ctx: *TranspileContext) !void
|
||||
// This should NOT initialize _zx here - that's done in the parent block
|
||||
const zx_block_node = findZxBlockInReturn(node);
|
||||
|
||||
// Find the parenthesis position if present
|
||||
var paren_byte: ?u32 = null;
|
||||
const child_count = node.childCount();
|
||||
var i: u32 = 0;
|
||||
while (i < child_count) : (i += 1) {
|
||||
const child = node.child(i) orelse continue;
|
||||
if (NodeKind.fromNode(child) == .parenthesized_expression) {
|
||||
// The first child of parenthesized_expression is '('
|
||||
const open_paren = child.child(0);
|
||||
if (open_paren) |p| {
|
||||
if (std.mem.eql(u8, p.kind(), "(")) {
|
||||
paren_byte = p.startByte();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (zx_block_node) |zx_node| {
|
||||
// Find the element inside the zx_block
|
||||
const zx_child_count = zx_node.childCount();
|
||||
@ -461,8 +492,8 @@ pub fn transpileReturn(self: *Ast, node: ts.Node, ctx: *TranspileContext) !void
|
||||
// Check if we need to initialize _zx with allocator
|
||||
const allocator_value = try getAllocatorAttribute(self, child);
|
||||
|
||||
try ctx.writeM("var", node.startByte(), self);
|
||||
try ctx.write(" _zx = @import(\"zx\").");
|
||||
// Synthesized _zx init — no source mapping (it's boilerplate)
|
||||
try ctx.write("var _zx = @import(\"zx\").");
|
||||
if (allocator_value) |alloc| {
|
||||
try ctx.write("allocInit(");
|
||||
try ctx.write(alloc);
|
||||
@ -474,8 +505,13 @@ pub fn transpileReturn(self: *Ast, node: ts.Node, ctx: *TranspileContext) !void
|
||||
// Mark that _zx is now initialized for nested ZX blocks
|
||||
ctx.zx_initialized = true;
|
||||
try ctx.writeIndent();
|
||||
// Map generated `return` to the source `return` keyword
|
||||
try ctx.writeM("return", node.startByte(), self);
|
||||
try ctx.write(" ");
|
||||
|
||||
// Set the paren position for the upcoming element transpilation
|
||||
ctx.paren_byte = paren_byte;
|
||||
|
||||
try transpileElement(self, child, ctx, true);
|
||||
// Reset the flag after processing the return statement
|
||||
ctx.zx_initialized = false;
|
||||
@ -636,22 +672,39 @@ pub fn transpileFragment(self: *Ast, node: ts.Node, ctx: *TranspileContext, is_r
|
||||
var children = std.ArrayList(ts.Node){};
|
||||
defer children.deinit(ctx.output.allocator);
|
||||
|
||||
var end_tag_start_byte: u32 = node.endByte();
|
||||
var end_tag_end_byte: u32 = node.endByte();
|
||||
const child_count = node.childCount();
|
||||
var i: u32 = 0;
|
||||
while (i < child_count) : (i += 1) {
|
||||
const child = node.child(i) orelse continue;
|
||||
if (NodeKind.fromNode(child) == .zx_child) {
|
||||
const kind = NodeKind.fromNode(child);
|
||||
if (kind == .zx_child) {
|
||||
try children.append(ctx.output.allocator, child);
|
||||
} else if (kind == .zx_end_tag) {
|
||||
end_tag_start_byte = child.startByte();
|
||||
end_tag_end_byte = child.endByte();
|
||||
}
|
||||
}
|
||||
|
||||
// Fragment is just like a regular element but with .fragment tag and no attributes
|
||||
try ctx.writeM("_zx.ele", node.startByte(), self);
|
||||
try ctx.write("(\n");
|
||||
try ctx.write("_zx.ele");
|
||||
if (ctx.paren_byte) |p| {
|
||||
try ctx.writeM("(", p, self);
|
||||
ctx.paren_byte = null;
|
||||
} else {
|
||||
try ctx.write("(");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".fragment,\n");
|
||||
// Map both start and end tag to the fragment name for tooltips
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM(".", node.startByte(), self);
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM("fragment", node.startByte(), self);
|
||||
try ctx.writeM(",\n", end_tag_start_byte, self);
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{\n");
|
||||
@ -687,7 +740,7 @@ pub fn transpileFragment(self: *Ast, node: ts.Node, ctx: *TranspileContext, is_r
|
||||
ctx.indent_level -= 1;
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(")");
|
||||
try ctx.writeM(")", end_tag_end_byte, self);
|
||||
}
|
||||
|
||||
pub fn isCustomComponent(tag: []const u8) bool {
|
||||
@ -727,6 +780,7 @@ pub fn transpileSelfClosing(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
_ = is_root;
|
||||
|
||||
var tag_name: ?[]const u8 = null;
|
||||
var tag_name_byte: u32 = node.startByte();
|
||||
var attributes = std.ArrayList(ZxAttribute){};
|
||||
defer attributes.deinit(ctx.output.allocator);
|
||||
|
||||
@ -737,7 +791,10 @@ pub fn transpileSelfClosing(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
const child = node.child(i) orelse continue;
|
||||
|
||||
switch (NodeKind.fromNode(child)) {
|
||||
.zx_tag_name => tag_name = try self.getNodeText(child),
|
||||
.zx_tag_name => {
|
||||
tag_name = try self.getNodeText(child);
|
||||
tag_name_byte = child.startByte();
|
||||
},
|
||||
.zx_attribute, .zx_builtin_attribute, .zx_regular_attribute, .zx_shorthand_attribute, .zx_builtin_shorthand_attribute, .zx_spread_attribute => {
|
||||
const attr = try parseAttribute(self, child);
|
||||
if (attr.isValid()) {
|
||||
@ -751,9 +808,9 @@ pub fn transpileSelfClosing(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
const tag = tag_name orelse return;
|
||||
|
||||
if (isCustomComponent(tag)) {
|
||||
try writeCustomComponent(self, node, tag, attributes.items, &.{}, ctx);
|
||||
try writeCustomComponent(self, node, tag, node.startByte(), node.endByte(), node.endByte(), attributes.items, &.{}, ctx);
|
||||
} else {
|
||||
try writeHtmlElement(self, node, tag, attributes.items, &.{}, ctx, false);
|
||||
try writeHtmlElement(self, node, tag, node.startByte(), node.endByte(), node.endByte(), attributes.items, &.{}, ctx, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -762,11 +819,15 @@ pub fn transpileFullElement(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
|
||||
// Parse element structure
|
||||
var tag_name: ?[]const u8 = null;
|
||||
var tag_name_byte: u32 = node.startByte();
|
||||
var attributes = std.ArrayList(ZxAttribute){};
|
||||
defer attributes.deinit(ctx.output.allocator);
|
||||
var children = std.ArrayList(ts.Node){};
|
||||
defer children.deinit(ctx.output.allocator);
|
||||
|
||||
var end_tag_start_byte: u32 = node.endByte();
|
||||
var end_tag_end_byte: u32 = node.endByte();
|
||||
|
||||
const child_count = node.childCount();
|
||||
var i: u32 = 0;
|
||||
while (i < child_count) : (i += 1) {
|
||||
@ -781,7 +842,10 @@ pub fn transpileFullElement(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
const tag_child = child.child(j) orelse continue;
|
||||
|
||||
switch (NodeKind.fromNode(tag_child)) {
|
||||
.zx_tag_name => tag_name = try self.getNodeText(tag_child),
|
||||
.zx_tag_name => {
|
||||
tag_name = try self.getNodeText(tag_child);
|
||||
tag_name_byte = tag_child.startByte();
|
||||
},
|
||||
.zx_attribute, .zx_builtin_attribute, .zx_regular_attribute, .zx_shorthand_attribute, .zx_builtin_shorthand_attribute, .zx_spread_attribute => {
|
||||
const attr = try parseAttribute(self, tag_child);
|
||||
if (attr.isValid()) {
|
||||
@ -793,6 +857,10 @@ pub fn transpileFullElement(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
}
|
||||
},
|
||||
.zx_child => try children.append(ctx.output.allocator, child),
|
||||
.zx_end_tag => {
|
||||
end_tag_start_byte = child.startByte();
|
||||
end_tag_end_byte = child.endByte();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@ -801,7 +869,7 @@ pub fn transpileFullElement(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
|
||||
// Custom component with children
|
||||
if (isCustomComponent(tag)) {
|
||||
try writeCustomComponent(self, node, tag, attributes.items, children.items, ctx);
|
||||
try writeCustomComponent(self, node, tag, node.startByte(), end_tag_start_byte, end_tag_end_byte, attributes.items, children.items, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -810,11 +878,11 @@ pub fn transpileFullElement(self: *Ast, node: ts.Node, ctx: *TranspileContext, i
|
||||
const preserve_whitespace = parent_preserve_whitespace or isPreElement(tag);
|
||||
|
||||
// Regular HTML element (with optional whitespace preservation for <pre>)
|
||||
try writeHtmlElement(self, node, tag, attributes.items, children.items, ctx, preserve_whitespace);
|
||||
try writeHtmlElement(self, node, tag, node.startByte(), end_tag_start_byte, end_tag_end_byte, attributes.items, children.items, ctx, preserve_whitespace);
|
||||
}
|
||||
|
||||
/// Write a custom component: _zx.cmp(Component, .{ .prop = value }) or _zx.client(...) for React CSR
|
||||
fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes: []const ZxAttribute, children: []const ts.Node, ctx: *TranspileContext) error{OutOfMemory}!void {
|
||||
fn writeCustomComponent(self: *Ast, _: ts.Node, tag: []const u8, tag_name_byte: u32, end_tag_start_byte: u32, end_tag_end_byte: u32, attributes: []const ZxAttribute, children: []const ts.Node, ctx: *TranspileContext) error{OutOfMemory}!void {
|
||||
// Check if this is a client-side rendered component (@rendering={.react} or @rendering={.client})
|
||||
var rendering_value: ?[]const u8 = null;
|
||||
for (attributes) |attr| {
|
||||
@ -891,14 +959,27 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
try ctx.client_components.append(ctx.allocator, client_cmp);
|
||||
|
||||
// Write _zx.client(.{ .name = "Name", .path = "path", .id = "id" }, .{ props })
|
||||
try ctx.writeM("_zx.client", node.startByte(), self);
|
||||
try ctx.write("(.{ .name = \"");
|
||||
try ctx.write(componentDisplayName(tag));
|
||||
try ctx.write("_zx.client");
|
||||
if (ctx.paren_byte) |p| {
|
||||
try ctx.writeM("(", p, self);
|
||||
ctx.paren_byte = null;
|
||||
} else {
|
||||
try ctx.write("(");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{ .name = \"");
|
||||
try ctx.writeM(componentDisplayName(tag), tag_name_byte, self);
|
||||
try ctx.write("\", .path = \"");
|
||||
try ctx.write(full_path);
|
||||
try ctx.write("\", .id = \"");
|
||||
try ctx.write(client_cmp.id);
|
||||
try ctx.write("\" }, .{");
|
||||
try ctx.write("\" },\n");
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{");
|
||||
|
||||
// Write props (non-builtin attributes)
|
||||
var first_prop = true;
|
||||
@ -907,8 +988,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
if (!first_prop) try ctx.write(",");
|
||||
first_prop = false;
|
||||
|
||||
try ctx.write(" .");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".");
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write(" = ");
|
||||
// Handle template strings, zx_blocks, and regular values
|
||||
if (attr.template_string_node) |template_node| {
|
||||
@ -918,9 +1002,18 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
} else {
|
||||
try ctx.writeM(attr.value, attr.value_byte_offset, self);
|
||||
}
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
try ctx.write(" })");
|
||||
if (!first_prop) {
|
||||
try ctx.write("\n");
|
||||
try ctx.writeIndent();
|
||||
}
|
||||
try ctx.write("}\n");
|
||||
|
||||
ctx.indent_level -= 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.writeM(")", end_tag_end_byte, self);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -956,10 +1049,22 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
try ctx.client_components.append(ctx.allocator, client_cmp);
|
||||
|
||||
// Write _zx.cmp(Component, .{ .name = ..., .client = .{ .name = ..., .id = ... } }, .{ props })
|
||||
try ctx.writeM("_zx.cmp", node.startByte(), self);
|
||||
try ctx.write("(");
|
||||
try ctx.write(tag);
|
||||
try ctx.write(", ");
|
||||
try ctx.write("_zx.cmp");
|
||||
if (ctx.paren_byte) |p| {
|
||||
try ctx.writeM("(", p, self);
|
||||
ctx.paren_byte = null;
|
||||
} else {
|
||||
try ctx.write("(");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM(tag, tag_name_byte, self);
|
||||
try ctx.writeM(",\n", end_tag_start_byte, self);
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{ .name = \"");
|
||||
try ctx.write(componentDisplayName(tag));
|
||||
try ctx.write("\", .client = .{ .name = \"");
|
||||
@ -968,7 +1073,10 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
// try ctx.write(full_path);
|
||||
try ctx.write("\", .id = \"");
|
||||
try ctx.write(client_cmp.id);
|
||||
try ctx.write("\" } }, .{");
|
||||
try ctx.write("\" } },\n");
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{");
|
||||
|
||||
// Write props (non-builtin attributes)
|
||||
var first_prop = true;
|
||||
@ -977,8 +1085,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
if (!first_prop) try ctx.write(",");
|
||||
first_prop = false;
|
||||
|
||||
try ctx.write(" .");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".");
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write(" = ");
|
||||
// Handle template strings, zx_blocks, and regular values
|
||||
if (attr.template_string_node) |template_node| {
|
||||
@ -988,18 +1099,37 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
} else {
|
||||
try ctx.writeM(attr.value, attr.value_byte_offset, self);
|
||||
}
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
try ctx.write(" },)");
|
||||
if (!first_prop) {
|
||||
try ctx.write("\n");
|
||||
try ctx.writeIndent();
|
||||
}
|
||||
try ctx.write("},\n");
|
||||
|
||||
ctx.indent_level -= 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.writeM(")", end_tag_end_byte, self);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
// Regular cmp component: _zx.cmp(Func, .{ .name = ..., options }, .{ props })
|
||||
try ctx.writeM("_zx.cmp", node.startByte(), self);
|
||||
try ctx.write("(");
|
||||
try ctx.write(tag);
|
||||
try ctx.write(", ");
|
||||
try ctx.write("_zx.cmp");
|
||||
if (ctx.paren_byte) |p| {
|
||||
try ctx.writeM("(", p, self);
|
||||
ctx.paren_byte = null;
|
||||
} else {
|
||||
try ctx.write("(");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM(tag, tag_name_byte, self);
|
||||
try ctx.writeM(",\n", end_tag_start_byte, self);
|
||||
|
||||
var spreads = std.ArrayList(ZxAttribute){};
|
||||
defer spreads.deinit(ctx.output.allocator);
|
||||
@ -1026,18 +1156,22 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
const has_children = children.len > 0;
|
||||
|
||||
// Write options parameter first (name + builtin attributes)
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{ .name = \"");
|
||||
try ctx.write(componentDisplayName(tag));
|
||||
try ctx.write("\"");
|
||||
try writeComponentBuiltinOptions(self, builtin_attrs.items, ctx, true);
|
||||
try ctx.write(" }, ");
|
||||
try ctx.write(" },\n");
|
||||
|
||||
// Case 1: Single spread
|
||||
if (spreads.items.len == 1 and !has_regular_props and !has_children) {
|
||||
try ctx.writeIndent();
|
||||
try ctx.writeM(spreads.items[0].value, spreads.items[0].value_byte_offset, self);
|
||||
try ctx.write("\n");
|
||||
}
|
||||
// Case 2: Multiple spreads with other props or children - use propsM
|
||||
else if (has_spread) {
|
||||
try ctx.writeIndent();
|
||||
var need_merge = false;
|
||||
if (spreads.items.len > 0) {
|
||||
try ctx.write("_zx.propsM(");
|
||||
@ -1059,8 +1193,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
if (!first_prop) try ctx.write(",");
|
||||
first_prop = false;
|
||||
|
||||
try ctx.write(" .");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".");
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write(" = ");
|
||||
// Handle template strings, zx_blocks, and regular values
|
||||
if (attr.template_string_node) |template_node| {
|
||||
@ -1070,22 +1207,33 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
} else {
|
||||
try ctx.writeM(attr.value, attr.value_byte_offset, self);
|
||||
}
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
// Add children prop
|
||||
if (has_children) {
|
||||
if (!first_prop) try ctx.write(",");
|
||||
try ctx.write(" .children = ");
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".children = ");
|
||||
try writeChildrenValue(self, children, ctx);
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
try ctx.write(" }");
|
||||
if (!first_prop) {
|
||||
try ctx.write("\n");
|
||||
try ctx.writeIndent();
|
||||
}
|
||||
try ctx.write("}");
|
||||
}
|
||||
|
||||
if (need_merge) try ctx.write(")");
|
||||
try ctx.write("\n");
|
||||
}
|
||||
// Case 3: Regular attrs
|
||||
else {
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".{");
|
||||
|
||||
var first_prop = true;
|
||||
@ -1093,8 +1241,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
if (!first_prop) try ctx.write(",");
|
||||
first_prop = false;
|
||||
|
||||
try ctx.write(" .");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".");
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write(" = ");
|
||||
// Handle template strings, zx_blocks, and regular values
|
||||
if (attr.template_string_node) |template_node| {
|
||||
@ -1104,19 +1255,30 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
} else {
|
||||
try ctx.writeM(attr.value, attr.value_byte_offset, self);
|
||||
}
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
// Add children prop
|
||||
if (has_children) {
|
||||
if (!first_prop) try ctx.write(",");
|
||||
try ctx.write(" .children = ");
|
||||
try ctx.write("\n");
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(".children = ");
|
||||
try writeChildrenValue(self, children, ctx);
|
||||
ctx.indent_level -= 1;
|
||||
}
|
||||
|
||||
try ctx.write(" }");
|
||||
if (!first_prop) {
|
||||
try ctx.write("\n");
|
||||
try ctx.writeIndent();
|
||||
}
|
||||
try ctx.write("}\n");
|
||||
}
|
||||
|
||||
try ctx.write(",)");
|
||||
ctx.indent_level -= 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.writeM(",)", end_tag_end_byte, self);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1150,7 +1312,8 @@ fn writeComponentBuiltinOptions(self: *Ast, builtin_attrs: []const ZxAttribute,
|
||||
}
|
||||
} else {
|
||||
try ctx.write(" .");
|
||||
try ctx.write(attr.name[1..]); // Skip @ prefix
|
||||
const name = if (attr.name[0] == '@') attr.name[1..] else attr.name;
|
||||
try ctx.writeM(name, attr.name_byte_offset, self);
|
||||
try ctx.write(" = ");
|
||||
}
|
||||
|
||||
@ -1188,15 +1351,26 @@ fn writeChildrenValue(self: *Ast, children: []const ts.Node, ctx: *TranspileCont
|
||||
|
||||
/// Write a regular HTML element: _zx.ele(.tag, .{ ... })
|
||||
/// When preserve_whitespace is true (e.g. for <pre>), text nodes won't be trimmed
|
||||
fn writeHtmlElement(self: *Ast, node: ts.Node, tag: []const u8, attributes: []const ZxAttribute, children: []const ts.Node, ctx: *TranspileContext, preserve_whitespace: bool) !void {
|
||||
try ctx.writeM("_zx.ele", node.startByte(), self);
|
||||
try ctx.write("(\n");
|
||||
fn writeHtmlElement(self: *Ast, node: ts.Node, tag: []const u8, tag_name_byte: u32, end_tag_start_byte: u32, end_tag_end_byte: u32, attributes: []const ZxAttribute, children: []const ts.Node, ctx: *TranspileContext, preserve_whitespace: bool) !void {
|
||||
_ = node;
|
||||
// _zx.ele( is synthesized — no source mapping
|
||||
try ctx.write("_zx.ele");
|
||||
if (ctx.paren_byte) |p| {
|
||||
try ctx.writeM("(", p, self);
|
||||
ctx.paren_byte = null;
|
||||
} else {
|
||||
try ctx.write("(");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
|
||||
ctx.indent_level += 1;
|
||||
try ctx.writeIndent();
|
||||
try ctx.writeM(".", node.startByte(), self);
|
||||
try ctx.write(tag);
|
||||
try ctx.write(",\n");
|
||||
// Map both start and end tags to the tag name for tooltips
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM(".", tag_name_byte, self);
|
||||
try ctx.addMapping(end_tag_start_byte, self);
|
||||
try ctx.writeM(tag, tag_name_byte, self);
|
||||
try ctx.writeM(",\n", end_tag_start_byte, self);
|
||||
|
||||
// Write options struct
|
||||
try ctx.writeIndent();
|
||||
@ -1235,7 +1409,7 @@ fn writeHtmlElement(self: *Ast, node: ts.Node, tag: []const u8, attributes: []co
|
||||
ctx.indent_level -= 1;
|
||||
|
||||
try ctx.writeIndent();
|
||||
try ctx.write(")");
|
||||
try ctx.writeM(")", end_tag_end_byte, self);
|
||||
}
|
||||
|
||||
/// Transpile a child node. When preserve_whitespace is true (e.g. inside <pre>),
|
||||
@ -1909,6 +2083,7 @@ fn findSpecialChild(node: ts.Node) ?ts.Node {
|
||||
|
||||
pub const ZxAttribute = struct {
|
||||
name: []const u8,
|
||||
name_byte_offset: u32,
|
||||
value: []const u8,
|
||||
value_byte_offset: u32,
|
||||
is_builtin: bool,
|
||||
@ -2007,17 +2182,17 @@ fn writeAttributes(self: *Ast, attributes: []const ZxAttribute, ctx: *TranspileC
|
||||
|
||||
// Handle template strings with _zx.attrf
|
||||
if (attr.template_string_node) |template_node| {
|
||||
try transpileTemplateStringAttr(self, attr.name, template_node, ctx);
|
||||
try transpileTemplateStringAttr(self, attr, template_node, ctx);
|
||||
} else if (attr.zx_block_node) |zx_node| {
|
||||
// If value contains a zx_block, transpile it instead of writing raw text
|
||||
try ctx.write("_zx.attr(\"");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write("\", ");
|
||||
try transpileBlock(self, zx_node, ctx);
|
||||
try ctx.write("),\n");
|
||||
} else {
|
||||
try ctx.write("_zx.attr(\"");
|
||||
try ctx.write(attr.name);
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write("\", ");
|
||||
try ctx.writeM(attr.value, attr.value_byte_offset, self);
|
||||
try ctx.write("),\n");
|
||||
@ -2110,7 +2285,7 @@ fn transpileTemplateStringProp(self: *Ast, template_node: ts.Node, ctx: *Transpi
|
||||
}
|
||||
|
||||
/// Transpile a template string attribute to _zx.attrf("name", "format", .{ values })
|
||||
fn transpileTemplateStringAttr(self: *Ast, attr_name: []const u8, template_node: ts.Node, ctx: *TranspileContext) error{OutOfMemory}!void {
|
||||
fn transpileTemplateStringAttr(self: *Ast, attr: ZxAttribute, template_node: ts.Node, ctx: *TranspileContext) error{OutOfMemory}!void {
|
||||
// Collect template content and substitutions
|
||||
var format_parts = std.ArrayList(u8){};
|
||||
defer format_parts.deinit(ctx.output.allocator);
|
||||
@ -2176,7 +2351,7 @@ fn transpileTemplateStringAttr(self: *Ast, attr_name: []const u8, template_node:
|
||||
|
||||
// Write _zx.attrf("name", "format", .{ values })
|
||||
try ctx.write("_zx.attrf(\"");
|
||||
try ctx.write(attr_name);
|
||||
try ctx.writeM(attr.name, attr.name_byte_offset, self);
|
||||
try ctx.write("\", \"");
|
||||
try ctx.write(format_parts.items);
|
||||
try ctx.write("\", .{\n");
|
||||
@ -2202,6 +2377,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
const attr_node = switch (node_kind) {
|
||||
.zx_attribute => node.child(0) orelse return ZxAttribute{
|
||||
.name = "",
|
||||
.name_byte_offset = node.startByte(),
|
||||
.value = "\"\"",
|
||||
.value_byte_offset = node.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2220,6 +2396,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
const clean_name = extractCleanIdentifierName(full_name);
|
||||
return ZxAttribute{
|
||||
.name = clean_name,
|
||||
.name_byte_offset = n.startByte(),
|
||||
.value = full_name,
|
||||
.value_byte_offset = n.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2228,6 +2405,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
}
|
||||
return ZxAttribute{
|
||||
.name = "",
|
||||
.name_byte_offset = node.startByte(),
|
||||
.value = "\"\"",
|
||||
.value_byte_offset = node.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2243,6 +2421,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
const attr_name = try std.fmt.allocPrint(self.allocator, "@{s}", .{var_name});
|
||||
return ZxAttribute{
|
||||
.name = attr_name,
|
||||
.name_byte_offset = n.startByte(),
|
||||
.value = var_name,
|
||||
.value_byte_offset = n.startByte(),
|
||||
.is_builtin = true,
|
||||
@ -2251,6 +2430,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
}
|
||||
return ZxAttribute{
|
||||
.name = "",
|
||||
.name_byte_offset = node.startByte(),
|
||||
.value = "\"\"",
|
||||
.value_byte_offset = node.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2264,6 +2444,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
const expr_text = try self.getNodeText(e);
|
||||
return ZxAttribute{
|
||||
.name = "",
|
||||
.name_byte_offset = e.startByte(),
|
||||
.value = expr_text,
|
||||
.value_byte_offset = e.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2272,6 +2453,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
}
|
||||
return ZxAttribute{
|
||||
.name = "",
|
||||
.name_byte_offset = node.startByte(),
|
||||
.value = "",
|
||||
.value_byte_offset = node.startByte(),
|
||||
.is_builtin = false,
|
||||
@ -2297,6 +2479,7 @@ pub fn parseAttribute(self: *Ast, node: ts.Node) !ZxAttribute {
|
||||
|
||||
return ZxAttribute{
|
||||
.name = name,
|
||||
.name_byte_offset = if (name_node) |n| n.startByte() else node.startByte(),
|
||||
.value = value,
|
||||
.value_byte_offset = value_offset,
|
||||
.is_builtin = is_builtin,
|
||||
|
||||
@ -21,9 +21,24 @@ pub const DecodedMap = struct {
|
||||
/// Returns the closest mapping at or before the given source position.
|
||||
pub fn sourceToGenerated(self: DecodedMap, line: i32, column: i32) ?Mapping {
|
||||
var best: ?Mapping = null;
|
||||
var is_exact = false;
|
||||
|
||||
for (self.entries) |m| {
|
||||
if (m.source_line == line and m.source_column == column) {
|
||||
if (!is_exact or m.generated_line < best.?.generated_line or
|
||||
(m.generated_line == best.?.generated_line and m.generated_column < best.?.generated_column))
|
||||
{
|
||||
best = m;
|
||||
is_exact = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_exact) continue; // Exact matches are always better than "at or before"
|
||||
|
||||
if (m.source_line > line) continue;
|
||||
if (m.source_line == line and m.source_column > column) continue;
|
||||
|
||||
if (best) |b| {
|
||||
if (m.source_line > b.source_line or
|
||||
(m.source_line == b.source_line and m.source_column > b.source_column))
|
||||
@ -34,6 +49,7 @@ pub const DecodedMap = struct {
|
||||
best = m;
|
||||
}
|
||||
}
|
||||
|
||||
if (best) |b| {
|
||||
// Adjust the generated column by the offset from the exact mapping point
|
||||
const col_offset = if (b.source_line == line) column - b.source_column else 0;
|
||||
@ -50,20 +66,22 @@ pub const DecodedMap = struct {
|
||||
/// Map a generated (.zig) position back to the source (original .zx) position.
|
||||
/// Returns the closest mapping at or before the given generated position.
|
||||
pub fn generatedToSource(self: DecodedMap, line: i32, column: i32) ?Mapping {
|
||||
var low: usize = 0;
|
||||
var high: usize = self.entries.len;
|
||||
var best: ?Mapping = null;
|
||||
for (self.entries) |m| {
|
||||
if (m.generated_line > line) continue;
|
||||
if (m.generated_line == line and m.generated_column > column) continue;
|
||||
if (best) |b| {
|
||||
if (m.generated_line > b.generated_line or
|
||||
(m.generated_line == b.generated_line and m.generated_column > b.generated_column))
|
||||
{
|
||||
best = m;
|
||||
}
|
||||
} else {
|
||||
|
||||
while (low < high) {
|
||||
const mid = low + (high - low) / 2;
|
||||
const m = self.entries[mid];
|
||||
|
||||
if (m.generated_line < line or (m.generated_line == line and m.generated_column <= column)) {
|
||||
best = m;
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
if (best) |b| {
|
||||
const col_offset = if (b.generated_line == line) column - b.generated_column else 0;
|
||||
return .{
|
||||
|
||||
118
src/lsp/main.zig
118
src/lsp/main.zig
@ -1,9 +1,3 @@
|
||||
//! Implements a language server using the `lsp.basic_server` abstraction.
|
||||
//! This server forwards all requests and notifications to zls (Zig Language Server).
|
||||
//! For .zx files, it transpiles to Zig via zx.Ast and remaps positions using sourcemaps.
|
||||
|
||||
// Increase comptime branch quota BEFORE importing libraries
|
||||
// This is needed because basic_server analyzes all handler methods at comptime
|
||||
comptime {
|
||||
@setEvalBranchQuota(100_000);
|
||||
}
|
||||
@ -28,13 +22,10 @@ pub fn main() !void {
|
||||
|
||||
@setEvalBranchQuota(100_000);
|
||||
|
||||
// LSP implementations typically communicate over stdio (stdin and stdout)
|
||||
var read_buffer: [256]u8 = undefined;
|
||||
var stdio_transport: lsp.Transport.Stdio = .init(&read_buffer, .stdin(), .stdout());
|
||||
const transport: *lsp.Transport = &stdio_transport.transport;
|
||||
|
||||
// Resolve global_cache_path (e.g. ~/Library/Caches/zls on macOS, ~/.cache/zls on Linux)
|
||||
// ZLS needs this to resolve build_runner_path and builtin_path
|
||||
const global_cache_path: ?[]const u8 = blk: {
|
||||
const home = std.process.getEnvVarOwned(gpa, "HOME") catch break :blk null;
|
||||
const cache_suffix = if (builtin.os.tag == .macos) "Library/Caches/zls" else ".cache/zls";
|
||||
@ -50,8 +41,6 @@ pub fn main() !void {
|
||||
.config = &config,
|
||||
}) catch unreachable;
|
||||
|
||||
// The handler is a user provided type that stores the state of the
|
||||
// language server and provides callbacks for the desired LSP messages.
|
||||
var handler: Handler = .init(gpa, zls_server);
|
||||
defer handler.deinit();
|
||||
|
||||
@ -66,12 +55,14 @@ pub fn main() !void {
|
||||
const ZxFileState = struct {
|
||||
decoded_map: sourcemap.DecodedMap,
|
||||
zig_uri: []const u8,
|
||||
source: []const u8, // current .zx source (needed for incremental didChange)
|
||||
source: []const u8,
|
||||
zig_source: []const u8,
|
||||
|
||||
fn deinit(self: *ZxFileState, allocator: std.mem.Allocator) void {
|
||||
self.decoded_map.deinit();
|
||||
allocator.free(self.zig_uri);
|
||||
allocator.free(self.source);
|
||||
allocator.free(self.zig_source);
|
||||
}
|
||||
};
|
||||
|
||||
@ -139,6 +130,7 @@ pub const Handler = struct {
|
||||
.decoded_map = decoded,
|
||||
.zig_uri = zig_uri,
|
||||
.source = try handler.allocator.dupe(u8, source),
|
||||
.zig_source = try handler.allocator.dupe(u8, result.zx_source),
|
||||
});
|
||||
|
||||
return try handler.allocator.dupe(u8, result.zx_source);
|
||||
@ -147,10 +139,16 @@ pub const Handler = struct {
|
||||
/// Remap a position from source (.zx) to generated (.zig) coordinates.
|
||||
fn remapPositionToGenerated(handler: *Handler, uri: []const u8, pos: lsp.types.Position) struct { uri: []const u8, pos: lsp.types.Position } {
|
||||
const state = handler.zx_files.get(uri) orelse return .{ .uri = uri, .pos = pos };
|
||||
if (state.decoded_map.sourceToGenerated(@intCast(pos.line), @intCast(pos.character))) |m| {
|
||||
|
||||
// Convert UTF-16 column to byte column for mapping lookup
|
||||
const byte_col = utf16ToByteOnLine(state.source, @intCast(pos.line), @intCast(pos.character));
|
||||
|
||||
if (state.decoded_map.sourceToGenerated(@intCast(pos.line), @intCast(byte_col))) |m| {
|
||||
// Mapping returns byte column, convert back to UTF-16 for LSP
|
||||
const gen_utf16 = byteToUtf16OnLine(state.zig_source, @intCast(m.generated_line), @intCast(m.generated_column));
|
||||
return .{
|
||||
.uri = state.zig_uri,
|
||||
.pos = .{ .line = @intCast(m.generated_line), .character = @intCast(m.generated_column) },
|
||||
.pos = .{ .line = @intCast(m.generated_line), .character = @intCast(gen_utf16) },
|
||||
};
|
||||
}
|
||||
return .{ .uri = state.zig_uri, .pos = pos };
|
||||
@ -158,12 +156,15 @@ pub const Handler = struct {
|
||||
|
||||
/// Remap a position from generated (.zig) back to source (.zx) coordinates.
|
||||
fn remapPositionToSource(handler: *Handler, uri: []const u8, pos: lsp.types.Position) lsp.types.Position {
|
||||
// Find the zx_uri that maps to this zig_uri
|
||||
var it = handler.zx_files.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (std.mem.eql(u8, entry.value_ptr.zig_uri, uri)) {
|
||||
if (entry.value_ptr.decoded_map.generatedToSource(@intCast(pos.line), @intCast(pos.character))) |m| {
|
||||
return .{ .line = @intCast(m.source_line), .character = @intCast(m.source_column) };
|
||||
const state = entry.value_ptr;
|
||||
const byte_col = utf16ToByteOnLine(state.zig_source, @intCast(pos.line), @intCast(pos.character));
|
||||
|
||||
if (state.decoded_map.generatedToSource(@intCast(pos.line), @intCast(byte_col))) |m| {
|
||||
const src_utf16 = byteToUtf16OnLine(state.source, @intCast(m.source_line), @intCast(m.source_column));
|
||||
return .{ .line = @intCast(m.source_line), .character = @intCast(src_utf16) };
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
@ -279,15 +280,12 @@ pub const Handler = struct {
|
||||
for (params.contentChanges) |change| {
|
||||
switch (change) {
|
||||
.literal_1 => |full| {
|
||||
// Full document sync — use the text directly
|
||||
if (needs_free) handler.allocator.free(full_text);
|
||||
full_text = full.text;
|
||||
needs_free = false;
|
||||
},
|
||||
.literal_0 => |inc| {
|
||||
// Incremental change — apply to current source
|
||||
const new_text = applyIncrementalChange(handler.allocator, full_text, inc.range, inc.text) catch {
|
||||
// On failure, skip this change
|
||||
continue;
|
||||
};
|
||||
if (needs_free) handler.allocator.free(full_text);
|
||||
@ -304,7 +302,6 @@ pub const Handler = struct {
|
||||
defer handler.allocator.free(zig_source);
|
||||
|
||||
const state = handler.zx_files.get(params.textDocument.uri).?;
|
||||
// Send full document replacement to ZLS
|
||||
handler.zls.sendNotificationSync(arena, "textDocument/didChange", .{
|
||||
.textDocument = .{
|
||||
.uri = state.zig_uri,
|
||||
@ -596,8 +593,39 @@ pub const Handler = struct {
|
||||
arena: std.mem.Allocator,
|
||||
params: lsp.types.DocumentFormattingParams,
|
||||
) error{OutOfMemory}!?[]const lsp.types.TextEdit {
|
||||
const result = handler.zls.sendRequestSync(arena, "textDocument/formatting", params) catch null;
|
||||
return result;
|
||||
if (isZxUri(params.textDocument.uri)) {
|
||||
if (handler.zx_files.get(params.textDocument.uri)) |state| {
|
||||
const source_z = try handler.allocator.dupeZ(u8, state.source);
|
||||
defer handler.allocator.free(source_z);
|
||||
|
||||
const format_result = zx.Ast.fmt(handler.allocator, source_z) catch |err| {
|
||||
std.log.err("zx.Ast.fmt failed for {s}: {s}", .{ params.textDocument.uri, @errorName(err) });
|
||||
return null;
|
||||
};
|
||||
var format_result_cl = format_result;
|
||||
defer format_result_cl.deinit(handler.allocator);
|
||||
|
||||
if (std.mem.eql(u8, format_result.source, state.source)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const line_count = std.mem.count(u8, state.source, "\n");
|
||||
const last_line_start = if (std.mem.lastIndexOfScalar(u8, state.source, '\n')) |idx| idx + 1 else 0;
|
||||
const last_line_len = state.source.len - last_line_start;
|
||||
const utf16_col = byteToUtf16OnLine(state.source, @intCast(line_count), @intCast(last_line_len));
|
||||
|
||||
const edits = try arena.alloc(lsp.types.TextEdit, 1);
|
||||
edits[0] = .{
|
||||
.range = .{
|
||||
.start = .{ .line = 0, .character = 0 },
|
||||
.end = .{ .line = @intCast(line_count), .character = @intCast(utf16_col) },
|
||||
},
|
||||
.newText = try arena.dupe(u8, format_result.source),
|
||||
};
|
||||
return edits;
|
||||
}
|
||||
}
|
||||
return handler.zls.sendRequestSync(arena, "textDocument/formatting", params) catch null;
|
||||
}
|
||||
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
|
||||
@ -684,4 +712,48 @@ pub const Handler = struct {
|
||||
std.log.err("zls handleResponse failed: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert a byte offset on a specific line to a UTF-16 code unit offset.
|
||||
fn byteToUtf16OnLine(source: []const u8, line_idx: i32, byte_col: i32) i32 {
|
||||
var it = std.mem.splitScalar(u8, source, '\n');
|
||||
var curr_line: i32 = 0;
|
||||
while (it.next()) |line| : (curr_line += 1) {
|
||||
if (curr_line == line_idx) {
|
||||
const limit = if (byte_col > line.len) line.len else @as(usize, @intCast(byte_col));
|
||||
var utf16_count: i32 = 0;
|
||||
var i: usize = 0;
|
||||
while (i < limit) {
|
||||
const byte_len = std.unicode.utf8ByteSequenceLength(line[i]) catch 1;
|
||||
const char = std.unicode.utf8Decode(line[i..@min(line.len, i + byte_len)]) catch line[i];
|
||||
utf16_count += if (char > 0xFFFF) @as(i32, 2) else @as(i32, 1);
|
||||
i += byte_len;
|
||||
}
|
||||
return utf16_count;
|
||||
}
|
||||
}
|
||||
return byte_col;
|
||||
}
|
||||
|
||||
// TODO: Remove neccessity of conversions
|
||||
/// Convert a UTF-16 code unit offset on a specific line to a byte offset.
|
||||
fn utf16ToByteOnLine(source: []const u8, line_idx: i32, utf16_col: i32) i32 {
|
||||
var it = std.mem.splitScalar(u8, source, '\n');
|
||||
var curr_line: i32 = 0;
|
||||
while (it.next()) |line| : (curr_line += 1) {
|
||||
if (curr_line == line_idx) {
|
||||
var utf16_count: i32 = 0;
|
||||
var byte_count: i32 = 0;
|
||||
var i: usize = 0;
|
||||
while (i < line.len and utf16_count < utf16_col) {
|
||||
const byte_len = std.unicode.utf8ByteSequenceLength(line[i]) catch 1;
|
||||
const char = std.unicode.utf8Decode(line[i..@min(line.len, i + byte_len)]) catch line[i];
|
||||
utf16_count += if (char > 0xFFFF) @as(i32, 2) else @as(i32, 1);
|
||||
byte_count += @intCast(byte_len);
|
||||
i += byte_len;
|
||||
}
|
||||
return byte_count;
|
||||
}
|
||||
}
|
||||
return utf16_col;
|
||||
}
|
||||
};
|
||||
|
||||
394
test/core/dx.zig
Normal file
394
test/core/dx.zig
Normal file
@ -0,0 +1,394 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const zx = @import("zx");
|
||||
const sourcemap = zx.sourcemap;
|
||||
|
||||
test "sm > VLQ encode/decode roundtrip" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
// Build a sourcemap with known mappings
|
||||
var builder = sourcemap.Builder.init(allocator);
|
||||
defer builder.deinit();
|
||||
|
||||
const mappings = [_]sourcemap.Mapping{
|
||||
.{ .generated_line = 0, .generated_column = 0, .source_line = 0, .source_column = 0 },
|
||||
.{ .generated_line = 0, .generated_column = 10, .source_line = 0, .source_column = 5 },
|
||||
.{ .generated_line = 1, .generated_column = 4, .source_line = 1, .source_column = 4 },
|
||||
.{ .generated_line = 2, .generated_column = 8, .source_line = 3, .source_column = 12 },
|
||||
.{ .generated_line = 5, .generated_column = 0, .source_line = 10, .source_column = 0 },
|
||||
};
|
||||
|
||||
for (&mappings) |m| {
|
||||
try builder.addMapping(m);
|
||||
}
|
||||
|
||||
var sm = try builder.build();
|
||||
defer sm.deinit(@constCast(&allocator).*);
|
||||
|
||||
// Decode and verify roundtrip
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
try testing.expectEqual(mappings.len, decoded.entries.len);
|
||||
|
||||
for (&mappings, decoded.entries) |expected, actual| {
|
||||
try testing.expectEqual(expected.generated_line, actual.generated_line);
|
||||
try testing.expectEqual(expected.generated_column, actual.generated_column);
|
||||
try testing.expectEqual(expected.source_line, actual.source_line);
|
||||
try testing.expectEqual(expected.source_column, actual.source_column);
|
||||
}
|
||||
}
|
||||
|
||||
test "sm > sourceToGenerated exact match" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var builder = sourcemap.Builder.init(allocator);
|
||||
defer builder.deinit();
|
||||
|
||||
// Simulate: source line 2, col 4 maps to generated line 5, col 8
|
||||
try builder.addMapping(.{ .generated_line = 5, .generated_column = 8, .source_line = 2, .source_column = 4 });
|
||||
// source line 2, col 10 maps to generated line 5, col 20
|
||||
try builder.addMapping(.{ .generated_line = 5, .generated_column = 20, .source_line = 2, .source_column = 10 });
|
||||
|
||||
var sm = try builder.build();
|
||||
defer sm.deinit(@constCast(&allocator).*);
|
||||
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Exact match
|
||||
const result = decoded.sourceToGenerated(2, 4).?;
|
||||
try testing.expectEqual(@as(i32, 5), result.generated_line);
|
||||
try testing.expectEqual(@as(i32, 8), result.generated_column);
|
||||
|
||||
// Position between two mappings on same source line — should use closest before
|
||||
const between = decoded.sourceToGenerated(2, 7).?;
|
||||
try testing.expectEqual(@as(i32, 5), between.generated_line);
|
||||
// col 7 is 3 past the mapping at col 4, so generated col = 8 + 3 = 11
|
||||
try testing.expectEqual(@as(i32, 11), between.generated_column);
|
||||
}
|
||||
|
||||
test "sm > generatedToSource exact match" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var builder = sourcemap.Builder.init(allocator);
|
||||
defer builder.deinit();
|
||||
|
||||
try builder.addMapping(.{ .generated_line = 3, .generated_column = 0, .source_line = 1, .source_column = 0 });
|
||||
try builder.addMapping(.{ .generated_line = 3, .generated_column = 15, .source_line = 1, .source_column = 8 });
|
||||
|
||||
var sm = try builder.build();
|
||||
defer sm.deinit(@constCast(&allocator).*);
|
||||
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Exact match
|
||||
const result = decoded.generatedToSource(3, 0).?;
|
||||
try testing.expectEqual(@as(i32, 1), result.source_line);
|
||||
try testing.expectEqual(@as(i32, 0), result.source_column);
|
||||
|
||||
// Position between mappings
|
||||
const between = decoded.generatedToSource(3, 5).?;
|
||||
try testing.expectEqual(@as(i32, 1), between.source_line);
|
||||
try testing.expectEqual(@as(i32, 5), between.source_column);
|
||||
}
|
||||
|
||||
test "sm > lookup returns null for unmapped position" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var builder = sourcemap.Builder.init(allocator);
|
||||
defer builder.deinit();
|
||||
|
||||
try builder.addMapping(.{ .generated_line = 5, .generated_column = 0, .source_line = 3, .source_column = 0 });
|
||||
|
||||
var sm = try builder.build();
|
||||
defer sm.deinit(@constCast(&allocator).*);
|
||||
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Line before any mapping — should return null
|
||||
const result = decoded.sourceToGenerated(0, 0);
|
||||
try testing.expectEqual(@as(?sourcemap.Mapping, null), result);
|
||||
}
|
||||
|
||||
test "sm > e2e simple element transpilation" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const source =
|
||||
\\pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
\\ return (
|
||||
\\ <div>Hello</div>
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\const zx = @import("zx");
|
||||
;
|
||||
const source_z = try allocator.dupeZ(u8, source);
|
||||
defer allocator.free(source_z);
|
||||
|
||||
var result = try zx.Ast.parse(allocator, source_z, .{ .map = .inlined });
|
||||
defer result.deinit(allocator);
|
||||
|
||||
// Sourcemap should be present
|
||||
try testing.expect(result.sourcemap != null);
|
||||
|
||||
const sm = result.sourcemap.?;
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Should have mappings
|
||||
try testing.expect(decoded.entries.len > 0);
|
||||
const raw_zig = result.zx_source;
|
||||
|
||||
// "pub" at source line 0, col 0 should map to generated line 0, col 0
|
||||
const pub_mapping = decoded.sourceToGenerated(0, 0).?;
|
||||
try testing.expectEqual(@as(i32, 0), pub_mapping.generated_line);
|
||||
try testing.expectEqual(@as(i32, 0), pub_mapping.generated_column);
|
||||
|
||||
// Verify the generated position actually contains "pub"
|
||||
const gen_offset = lineColToOffset(raw_zig, pub_mapping.generated_line, pub_mapping.generated_column);
|
||||
try testing.expect(gen_offset != null);
|
||||
if (gen_offset) |off| {
|
||||
try testing.expect(std.mem.startsWith(u8, raw_zig[off..], "pub"));
|
||||
}
|
||||
|
||||
// "const zx" at source line 6, col 0 should map to a generated position containing "const zx"
|
||||
const const_mapping = decoded.sourceToGenerated(6, 0).?;
|
||||
const const_offset = lineColToOffset(raw_zig, const_mapping.generated_line, const_mapping.generated_column);
|
||||
try testing.expect(const_offset != null);
|
||||
if (const_offset) |off| {
|
||||
try testing.expect(std.mem.startsWith(u8, raw_zig[off..], "const"));
|
||||
}
|
||||
}
|
||||
|
||||
test "sm > e2e generatedToSource roundtrip for zig code" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const source =
|
||||
\\const std = @import("std");
|
||||
\\
|
||||
\\pub fn hello() void {
|
||||
\\ std.debug.print("hello\n", .{});
|
||||
\\}
|
||||
;
|
||||
const source_z = try allocator.dupeZ(u8, source);
|
||||
defer allocator.free(source_z);
|
||||
|
||||
var result = try zx.Ast.parse(allocator, source_z, .{ .map = .inlined });
|
||||
defer result.deinit(allocator);
|
||||
|
||||
try testing.expect(result.sourcemap != null);
|
||||
|
||||
const sm = result.sourcemap.?;
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Pure zig code should map 1:1 (source == generated for passthrough code)
|
||||
// "const" at line 0, col 0
|
||||
const m = decoded.sourceToGenerated(0, 0).?;
|
||||
try testing.expectEqual(@as(i32, 0), m.generated_line);
|
||||
try testing.expectEqual(@as(i32, 0), m.generated_column);
|
||||
|
||||
// Reverse lookup should also work
|
||||
const rev = decoded.generatedToSource(m.generated_line, m.generated_column).?;
|
||||
try testing.expectEqual(@as(i32, 0), rev.source_line);
|
||||
try testing.expectEqual(@as(i32, 0), rev.source_column);
|
||||
}
|
||||
|
||||
test "sm > e2e expression in element" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const source =
|
||||
\\pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
\\ const name = "world";
|
||||
\\ return (
|
||||
\\ <p>Hello {name}</p>
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\const zx = @import("zx");
|
||||
;
|
||||
const source_z = try allocator.dupeZ(u8, source);
|
||||
defer allocator.free(source_z);
|
||||
|
||||
var result = try zx.Ast.parse(allocator, source_z, .{ .map = .inlined });
|
||||
defer result.deinit(allocator);
|
||||
|
||||
try testing.expect(result.sourcemap != null);
|
||||
|
||||
const sm = result.sourcemap.?;
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
const raw_zig = result.zx_source;
|
||||
|
||||
// "const name" at source line 1, col 4 should map to a valid generated position
|
||||
const name_mapping = decoded.sourceToGenerated(1, 4).?;
|
||||
const name_offset = lineColToOffset(raw_zig, name_mapping.generated_line, name_mapping.generated_column);
|
||||
try testing.expect(name_offset != null);
|
||||
if (name_offset) |off| {
|
||||
try testing.expect(std.mem.startsWith(u8, raw_zig[off..], "const"));
|
||||
}
|
||||
|
||||
// The expression {name} at source line 3 should map somewhere in generated that contains "name"
|
||||
// Find "name" in the source — it's at line 3, the expression is after "Hello "
|
||||
// In .zx, line 3 is: " <p>Hello {name}</p>"
|
||||
// "name" starts at col 16 (after 8 spaces + "<p>Hello {")
|
||||
const expr_mapping = decoded.sourceToGenerated(3, 17).?;
|
||||
const expr_offset = lineColToOffset(raw_zig, expr_mapping.generated_line, expr_mapping.generated_column);
|
||||
try testing.expect(expr_offset != null);
|
||||
}
|
||||
|
||||
test "sm > all test files produce valid sourcemaps" {
|
||||
const allocator = testing.allocator;
|
||||
const test_files = [_][]const u8{
|
||||
"test/data/element/nested.zx",
|
||||
"test/data/expression/text.zx",
|
||||
"test/data/control_flow/if.zx",
|
||||
"test/data/control_flow/for.zx",
|
||||
"test/data/component/basic.zx",
|
||||
"test/data/attribute/dynamic.zx",
|
||||
};
|
||||
|
||||
for (test_files) |path| {
|
||||
const source = std.fs.cwd().readFileAlloc(allocator, path, std.math.maxInt(usize)) catch continue;
|
||||
defer allocator.free(source);
|
||||
|
||||
const source_z = try allocator.dupeZ(u8, source);
|
||||
defer allocator.free(source_z);
|
||||
|
||||
var result = zx.Ast.parse(allocator, source_z, .{ .map = .inlined, .path = path }) catch continue;
|
||||
defer result.deinit(allocator);
|
||||
|
||||
// Sourcemap must be present
|
||||
if (result.sourcemap) |sm| {
|
||||
// Must decode without error
|
||||
var decoded = try sm.decode(allocator);
|
||||
defer decoded.deinit();
|
||||
|
||||
// Should have at least some mappings
|
||||
try testing.expect(decoded.entries.len > 0);
|
||||
|
||||
// All generated positions should be within the raw zig source bounds
|
||||
const raw_zig = result.zx_source;
|
||||
for (decoded.entries) |entry| {
|
||||
const offset = lineColToOffset(raw_zig, entry.generated_line, entry.generated_column);
|
||||
if (offset == null) {
|
||||
std.debug.print("FAIL: {s}: mapping gen {d}:{d} is out of bounds (raw_zig len={d})\n", .{
|
||||
path,
|
||||
entry.generated_line,
|
||||
entry.generated_column,
|
||||
raw_zig.len,
|
||||
});
|
||||
return error.MappingOutOfBounds;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std.debug.print("FAIL: {s}: no sourcemap generated\n", .{path});
|
||||
return error.NoSourceMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "sm > generate sourcemap debug files" {
|
||||
if (!shouldGenerateDebugFiles()) return;
|
||||
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const test_files = [_]struct { zx_path: []const u8, name: []const u8 }{
|
||||
.{ .zx_path = "test/data/element/nested.zx", .name = "nested" },
|
||||
.{ .zx_path = "test/data/expression/text.zx", .name = "text" },
|
||||
.{ .zx_path = "test/data/control_flow/if.zx", .name = "if" },
|
||||
.{ .zx_path = "test/data/control_flow/for.zx", .name = "for" },
|
||||
.{ .zx_path = "test/data/component/basic.zx", .name = "basic" },
|
||||
.{ .zx_path = "test/data/attribute/dynamic.zx", .name = "dynamic" },
|
||||
};
|
||||
|
||||
// Ensure output directory exists
|
||||
std.fs.cwd().makePath(".zig-cache/tmp/.zx/sourcemap-debug") catch {};
|
||||
|
||||
for (test_files) |tf| {
|
||||
const source = std.fs.cwd().readFileAlloc(allocator, tf.zx_path, std.math.maxInt(usize)) catch continue;
|
||||
defer allocator.free(source);
|
||||
|
||||
const source_z = try allocator.dupeZ(u8, source);
|
||||
defer allocator.free(source_z);
|
||||
|
||||
var result = zx.Ast.parse(allocator, source_z, .{ .map = .inlined, .path = tf.zx_path }) catch continue;
|
||||
defer result.deinit(allocator);
|
||||
|
||||
const sm = result.sourcemap orelse continue;
|
||||
|
||||
// Generate the sourcemap JSON
|
||||
const gen_file = try std.fmt.allocPrint(allocator, "{s}.zig", .{tf.name});
|
||||
defer allocator.free(gen_file);
|
||||
const src_file = try std.fmt.allocPrint(allocator, "{s}.zx", .{tf.name});
|
||||
defer allocator.free(src_file);
|
||||
const map_json = try sm.toJSON(allocator, gen_file, src_file, source, result.zx_source);
|
||||
defer allocator.free(map_json);
|
||||
|
||||
// Write the .map JSON file
|
||||
const map_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/.zx/sourcemap-debug/{s}.zig.map", .{tf.name});
|
||||
defer allocator.free(map_path);
|
||||
try writeFile(map_path, map_json);
|
||||
|
||||
// Write the raw transpiled .zig with inline sourcemap comment
|
||||
// Format: //# sourceMappingURL=data:application/json;base64,<base64-encoded-json>
|
||||
const b64_len = std.base64.standard.Encoder.calcSize(map_json.len);
|
||||
const b64_buf = try allocator.alloc(u8, b64_len);
|
||||
defer allocator.free(b64_buf);
|
||||
_ = std.base64.standard.Encoder.encode(b64_buf, map_json);
|
||||
|
||||
const inline_zig = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"{s}\n//# sourceMappingURL=data:application/json;base64,{s}\n",
|
||||
.{ result.zx_source, b64_buf },
|
||||
);
|
||||
defer allocator.free(inline_zig);
|
||||
|
||||
const zig_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/.zx/sourcemap-debug/{s}.zig", .{tf.name});
|
||||
defer allocator.free(zig_path);
|
||||
try writeFile(zig_path, inline_zig);
|
||||
|
||||
// Also write the original .zx source for reference
|
||||
const zx_out_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/.zx/sourcemap-debug/{s}.zx", .{tf.name});
|
||||
defer allocator.free(zx_out_path);
|
||||
try writeFile(zx_out_path, source);
|
||||
|
||||
std.debug.print(" wrote: {s} + .map + .zx\n", .{zig_path});
|
||||
}
|
||||
|
||||
std.debug.print("\nSourcemap debug files written to .zig-cache/tmp/.zx/sourcemap-debug/\n", .{});
|
||||
std.debug.print("Visualize at: https://evanw.github.io/source-map-visualization/\n", .{});
|
||||
std.debug.print(" - Paste the .zig content as 'generated'\n", .{});
|
||||
std.debug.print(" - Paste the .map content as 'source map'\n", .{});
|
||||
}
|
||||
|
||||
fn shouldGenerateDebugFiles() bool {
|
||||
const val = std.process.getEnvVarOwned(testing.allocator, "SM_DEBUG") catch return false;
|
||||
testing.allocator.free(val);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn writeFile(path: []const u8, content: []const u8) !void {
|
||||
const file = try std.fs.cwd().createFile(path, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(content);
|
||||
}
|
||||
|
||||
fn lineColToOffset(source: []const u8, line: i32, col: i32) ?usize {
|
||||
var current_line: i32 = 0;
|
||||
var i: usize = 0;
|
||||
|
||||
while (current_line < line and i < source.len) {
|
||||
if (source[i] == '\n') current_line += 1;
|
||||
i += 1;
|
||||
}
|
||||
if (current_line != line) return null;
|
||||
|
||||
const offset = i + @as(usize, @intCast(col));
|
||||
if (offset > source.len) return null;
|
||||
return offset;
|
||||
}
|
||||
@ -9,6 +9,7 @@ test {
|
||||
_ = @import("core/html.zig");
|
||||
_ = @import("core/routing.zig");
|
||||
_ = @import("core/vdom.zig");
|
||||
_ = @import("core/dx.zig");
|
||||
}
|
||||
|
||||
pub const std_options = std.Options{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user