Skip to content

Commit 57c5638

Browse files
committed
lsp: introduce full element completions
when selecting a completion list entry for an html element, we now also generate "></elem>". this commit also fixes a regression in nesting validation where we failed to run validations for certain nested elements
1 parent 90a78b6 commit 57c5638

File tree

18 files changed

+170
-199
lines changed

18 files changed

+170
-199
lines changed

editors/vscode/.vscode/test.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/cli/logging.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ pub fn logFn(
2222
comptime format: []const u8,
2323
args: anytype,
2424
) void {
25-
switch (scope) {
26-
.root, .super_lsp, .@"html/ast/fmt" => {},
27-
else => return,
28-
}
29-
// inline for (build_options.enabled_scopes) |es| {
30-
// if (comptime std.mem.eql(u8, es, @tagName(scope))) {
31-
// break;
32-
// }
33-
// } else return;
25+
// switch (scope) {
26+
// .root, .super_lsp, .@"html/ast" => {},
27+
// else => return,
28+
// }
29+
inline for (build_options.enabled_scopes) |es| {
30+
if (comptime std.mem.eql(u8, es, @tagName(scope))) {
31+
break;
32+
}
33+
} else return;
3434

3535
const l = log_file orelse return;
3636
const scope_prefix = "(" ++ @tagName(scope) ++ "): ";

src/cli/lsp.zig

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,23 @@ pub fn @"textDocument/completion"(
523523
const completions = try doc.html.completions(arena, doc.src, @intCast(offset));
524524
const items = try arena.alloc(lsp.types.CompletionItem, completions.len);
525525
for (items, completions) |*it, cpl| {
526+
log.debug("label = '{s}' desc = '{s}'", .{ cpl.label, cpl.desc });
527+
const insert_text = if (cpl.value) |v| blk: {
528+
if (cpl.kind != .element_open) break :blk v;
529+
var idx = offset;
530+
const has_closing_bracket = while (idx < doc.src.len) : (idx += 1) {
531+
switch (doc.src[idx]) {
532+
else => {},
533+
'\n' => break false,
534+
'>' => break true,
535+
}
536+
} else false;
537+
if (has_closing_bracket) break :blk v[0..cpl.label.len];
538+
break :blk v;
539+
} else null;
526540
it.* = .{
527541
.label = cpl.label,
528-
.insertText = cpl.value,
542+
.insertText = insert_text,
529543
.documentation = if (cpl.desc.len == 0) null else .{
530544
.MarkupContent = .{
531545
.kind = .markdown,
@@ -534,6 +548,7 @@ pub fn @"textDocument/completion"(
534548
},
535549
.commitCharacters = &.{" >"},
536550
.preselect = cpl.label[0] == '/',
551+
.insertTextFormat = if (cpl.kind == .element_open) .Snippet else null,
537552
};
538553
}
539554

src/html/Ast.zig

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ pub const Error = struct {
344344
.first_or_last => "first or last",
345345
}},
346346
),
347-
.missing_ancestor => |e| w.print("missing ancestor: {t}", .{e}),
347+
.missing_ancestor => |e| w.print("missing ancestor: <{t}>", .{e}),
348348
.missing_child => |e| w.print("missing child: <{t}>", .{e}),
349349
.duplicate_child => |dc| {
350350
try w.print("duplicate child", .{});
@@ -1439,11 +1439,6 @@ pub fn validateNesting(
14391439
.text,
14401440
.doctype,
14411441
=> {
1442-
if (std.debug.runtime_safety and n.kind.isElement()) {
1443-
const element: Element = elements.get(n.kind);
1444-
assert(element.attributes != .manual);
1445-
}
1446-
14471442
var next = n;
14481443
node_idx = while (true) {
14491444
if (next.next_idx != 0) break next.next_idx;
@@ -1468,6 +1463,11 @@ pub fn validateNesting(
14681463

14691464
if (n.kind == .template) try seen_ids_stack.append(gpa, .empty);
14701465

1466+
if (n.first_child_idx != 0) {
1467+
node_idx = n.first_child_idx;
1468+
continue;
1469+
}
1470+
14711471
var next = n;
14721472
node_idx = while (true) {
14731473
if (next.kind == .template) {
@@ -1485,6 +1485,9 @@ pub const Completion = struct {
14851485
label: []const u8,
14861486
desc: []const u8,
14871487
value: ?[]const u8 = null,
1488+
// This value is used by the lsp to know how to interpret
1489+
// the value field of this list of suggestions.
1490+
kind: enum { attribute, element_open, element_close } = .attribute,
14881491
};
14891492

14901493
pub fn completions(
@@ -2117,23 +2120,24 @@ test "fuzz" {
21172120
const generator = @import("../generator/html.zig");
21182121
const Context = struct {
21192122
arena: *std.heap.ArenaAllocator,
2120-
gpa: Allocator,
21212123
out: *Writer,
21222124
fn testOne(ctx: @This(), input: []const u8) anyerror!void {
21232125
_ = ctx.arena.reset(.retain_capacity);
2126+
const gpa = ctx.arena.allocator();
21242127

21252128
var in: Reader = .fixed(input);
2126-
var out: Writer.Allocating = .init(ctx.gpa);
2127-
generator.generate(ctx.gpa, &in, &out.writer) catch |err| {
2129+
var out: Writer.Allocating = .init(gpa);
2130+
generator.generate(gpa, &in, &out.writer) catch |err| {
21282131
if (err == error.Skip) return;
21292132
return err;
21302133
};
21312134

2132-
std.debug.print("--begin--\n{s}\n\n", .{out.written()});
2135+
log.debug("--begin--\n{s}\n\n", .{out.written()});
2136+
2137+
const ast: Ast = try .init(gpa, out.written(), .html, false);
21332138

2134-
const ast: Ast = try .init(ctx.gpa, out.written(), .html, false);
2135-
// _ = ast;
2136-
var devnull: Writer.Discarding = .init(&.{});
2139+
var bufnull: [4096]u8 = undefined;
2140+
var devnull: Writer.Discarding = .init(&bufnull);
21372141
if (!ast.has_syntax_errors) {
21382142
try ast.render(out.written(), &devnull.writer);
21392143
}
@@ -2149,7 +2153,6 @@ test "fuzz" {
21492153
var out = std.fs.File.stdout().writer(&buf);
21502154
try std.testing.fuzz(Context{
21512155
.arena = &arena,
2152-
.gpa = arena.allocator(),
21532156
.out = &out.interface,
21542157
}, Context.testOne, .{});
21552158
}

src/html/Element.zig

Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -575,71 +575,85 @@ pub inline fn completions(
575575
offset: u32,
576576
mode: CompletionMode,
577577
) ![]const Ast.Completion {
578-
const children = switch (mode) {
579-
.attrs => blk: {
578+
switch (mode) {
579+
.attrs => {
580580
var stt = ast.nodes[node_idx].startTagIterator(src, ast.language);
581-
break :blk try Attribute.completions(
581+
return Attribute.completions(
582582
arena,
583583
src,
584584
&stt,
585585
element.tag,
586586
offset,
587587
);
588588
},
589-
.content => content: switch (element.content) {
590-
.custom => |custom| try custom.completions(
591-
arena,
592-
ast,
593-
src,
594-
node_idx,
595-
offset,
596-
),
597-
.model => continue :content .{ .simple = .{} },
598-
.simple => |simple| try simpleCompletions(
599-
arena,
600-
&.{},
601-
ast.nodes[node_idx].model.content,
602-
element.meta.content_reject,
603-
simple,
604-
),
605-
.anything => blk: {
606-
const start: usize = @intFromEnum(Kind.___) + 1;
607-
const all_elems = all.values[start..];
608-
const anything: [all_elems.len]Ast.Completion = comptime a: {
609-
var anything: [all_elems.len]Ast.Completion = undefined;
610-
for (all_elems, &anything) |in, *out| out.* = .{
611-
.label = @tagName(in.tag),
612-
.desc = in.desc,
613-
};
614-
break :a anything;
615-
};
589+
.content => {
590+
const children = content: switch (element.content) {
591+
.custom => |custom| try custom.completions(
592+
arena,
593+
ast,
594+
src,
595+
node_idx,
596+
offset,
597+
),
598+
.model => continue :content .{ .simple = .{} },
599+
.simple => |simple| try simpleCompletions(
600+
arena,
601+
&.{},
602+
ast.nodes[node_idx].model.content,
603+
element.meta.content_reject,
604+
simple,
605+
),
606+
.anything => {
607+
// const start: usize = @intFromEnum(Kind.___) + 1;
608+
// const all_elems = all.values[start..];
609+
// const anything: [all_elems.len]Ast.Completion = comptime a: {
610+
// var anything: [all_elems.len]Ast.Completion = undefined;
611+
// for (all_elems, &anything) |in, *out| out.* = .{
612+
// .label = @tagName(in.tag),
613+
// .desc = in.desc,
614+
// };
615+
// break :a anything;
616+
// };
617+
618+
// break :content &anything;
619+
break :content all_completions.values[8..];
620+
},
621+
};
616622

617-
break :blk &anything;
618-
},
619-
},
620-
};
623+
var result: std.ArrayList(Ast.Completion) = .empty;
624+
625+
var ancestor_idx = node_idx;
626+
while (ancestor_idx != 0) {
627+
const ancestor = ast.nodes[ancestor_idx];
628+
if (!ancestor.isClosed()) {
629+
const name = ancestor.span(src).slice(src);
630+
log.debug("open ancestor = {any}, open = '{s}'\n", .{ ancestor, name });
631+
const slashed = try std.fmt.allocPrint(arena, "/{s}>", .{name});
632+
var idx = offset;
633+
const has_slash: u32 = @intFromBool(src[offset -| 1] == '/');
634+
const has_closing_bracket: u32 = while (idx < src.len) : (idx += 1) {
635+
switch (src[idx]) {
636+
else => {},
637+
'\n' => break 0,
638+
'>' => break 1,
639+
}
640+
} else 1;
641+
try result.append(arena, .{
642+
.label = slashed[0 .. slashed.len - 1],
643+
.value = slashed[has_slash .. slashed.len - has_closing_bracket],
644+
.desc = "Close the last open element.",
645+
.kind = .element_close,
646+
});
647+
break;
648+
}
649+
ancestor_idx = ancestor.parent_idx;
650+
}
621651

622-
var result: std.ArrayList(Ast.Completion) = .empty;
623-
624-
var ancestor_idx = node_idx;
625-
while (ancestor_idx != 0) {
626-
const ancestor = ast.nodes[ancestor_idx];
627-
if (!ancestor.isClosed()) {
628-
const name = ancestor.span(src).slice(src);
629-
const slashed = try std.fmt.allocPrint(arena, "/{s}", .{name});
630-
try result.append(arena, .{
631-
.label = slashed,
632-
.value = if (src[offset -| 1] == '/') name else slashed,
633-
.desc = "",
634-
});
635-
break;
636-
}
637-
ancestor_idx = ancestor.parent_idx;
652+
try result.ensureTotalCapacityPrecise(arena, result.items.len + children.len);
653+
result.appendSliceAssumeCapacity(children);
654+
return result.items;
655+
},
638656
}
639-
640-
try result.ensureTotalCapacityPrecise(arena, result.items.len + children.len);
641-
result.appendSliceAssumeCapacity(children);
642-
return result.items;
643657
}
644658

645659
pub fn simpleCompletions(
@@ -655,10 +669,7 @@ pub fn simpleCompletions(
655669
all.values.len - @intFromEnum(Kind.___),
656670
);
657671

658-
for (prefix) |p| list.appendAssumeCapacity(.{
659-
.label = @tagName(p),
660-
.desc = all.get(p).desc,
661-
});
672+
for (prefix) |p| list.appendAssumeCapacity(all_completions.get(p));
662673

663674
const start: usize = @intFromEnum(Kind.___) + 1;
664675
outer: for (all.values[start..], start..) |e, idx| {
@@ -680,19 +691,13 @@ pub fn simpleCompletions(
680691

681692
const child_cs = e.meta.categories_superset;
682693
if (parent_content.overlaps(child_cs)) {
683-
list.appendAssumeCapacity(.{
684-
.label = @tagName(child_kind),
685-
.desc = e.desc,
686-
});
694+
list.appendAssumeCapacity(all_completions.get(child_kind));
687695
continue :outer;
688696
}
689697

690698
for (simple.extra_children) |ec| {
691699
if (ec == child_kind) {
692-
list.appendAssumeCapacity(.{
693-
.label = @tagName(child_kind),
694-
.desc = e.desc,
695-
});
700+
list.appendAssumeCapacity(all_completions.get(child_kind));
696701
continue :outer;
697702
}
698703
}
@@ -720,6 +725,22 @@ pub const elements: KindMap = blk: {
720725
break :blk .initComptime(keys);
721726
};
722727

728+
pub const all_completions = blk: {
729+
var ac: std.EnumArray(Ast.Kind, Ast.Completion) = undefined;
730+
731+
const fields = std.meta.fields(Ast.Kind)[8..];
732+
assert(std.mem.eql(u8, fields[0].name, "a"));
733+
for (ac.values[8..], fields, all.values[8..]) |*completion, f, elem| {
734+
completion.* = .{
735+
.label = f.name,
736+
.value = f.name ++ "$1>$0</" ++ f.name ++ ">",
737+
.desc = elem.desc,
738+
.kind = .element_open,
739+
};
740+
}
741+
break :blk ac;
742+
};
743+
723744
const temp: Element = .{
724745
.tag = .div,
725746
.model = .{

src/html/elements/audio_video.zig

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -634,18 +634,13 @@ fn completionsContent(
634634
}
635635
}
636636

637-
const source = comptime Element.all.get(.source);
638-
const track = comptime Element.all.get(.track);
637+
const source = comptime Element.all_completions.get(.source);
638+
const track = comptime Element.all_completions.get(.track);
639639

640640
switch (state) {
641641
.source => switch (kind_after_cursor) {
642-
.source => return &.{
643-
.{ .label = @tagName(source.tag), .desc = source.desc },
644-
},
645-
.track => return &.{
646-
.{ .label = @tagName(source.tag), .desc = source.desc },
647-
.{ .label = @tagName(track.tag), .desc = track.desc },
648-
},
642+
.source => return &.{source},
643+
.track => return &.{ source, track },
649644
else => return Element.simpleCompletions(
650645
arena,
651646
&.{ .source, .track },
@@ -656,9 +651,7 @@ fn completionsContent(
656651
},
657652

658653
.track => switch (kind_after_cursor) {
659-
.track => return &.{
660-
.{ .label = @tagName(track.tag), .desc = track.desc },
661-
},
654+
.track => return &.{track},
662655
else => return Element.simpleCompletions(
663656
arena,
664657
&.{.track},

src/html/elements/colgroup.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ fn completionsContent(
111111
if (has_span) return &.{};
112112

113113
return &.{
114-
.{ .label = "col", .desc = comptime Element.all.get(.col).desc },
115-
.{ .label = "template", .desc = comptime Element.all.get(.template).desc },
114+
comptime Element.all_completions.get(.col),
115+
comptime Element.all_completions.get(.template),
116116
};
117117
}

0 commit comments

Comments
 (0)