From 2b90e7217f6596e3a20de6de3d6d7fa62f09d2be Mon Sep 17 00:00:00 2001 From: marcusw0 <54700785+marcusw0@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:50:46 -0400 Subject: [PATCH] Add hover explanations for captures and labels --- src/features/hover.zig | 108 +++++++++++++++- tests/lsp_features/hover.zig | 243 +++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 7 deletions(-) diff --git a/src/features/hover.zig b/src/features/hover.zig index cfcf715605..43186a30cb 100644 --- a/src/features/hover.zig +++ b/src/features/hover.zig @@ -79,14 +79,108 @@ fn hoverSymbolWithType( else => unreachable, }, .function_parameter => |payload| ast.paramSlice(tree, payload.get(tree).?, false), - .optional_payload, - .error_union_payload, - .error_union_error, - .for_loop_payload, + .optional_payload => |opt_payload| def: { + const name = tree.tokenSlice(opt_payload.identifier); + const condition = tree.getNodeSource(opt_payload.condition); + + var doc: []u8 = undefined; + if (decl_handle.isCaptureByRef()) { + doc = try arena.print( + "`{s}` captures a pointer to the unwrapped value of `{s}` when it is not null.", + .{ name, condition }, + ); + } else { + doc = try arena.print( + "`{s}` captures the unwrapped value of `{s}` when it is not null.", + .{ name, condition }, + ); + } + try doc_strings.append(arena, doc); + break :def tree.tokenSlice(decl_handle.nameToken()); + }, + .error_union_payload => |payload| def: { + const name = tree.tokenSlice(payload.identifier); + const condition = tree.getNodeSource(payload.condition); + + var doc: []u8 = undefined; + if (decl_handle.isCaptureByRef()) { + doc = try arena.print( + "`{s}` captures a pointer to the successful value of `{s}` when it is not an error.", + .{ name, condition }, + ); + } else { + doc = try arena.print( + "`{s}` captures the successful value of `{s}` when it is not an error.", + .{ name, condition }, + ); + } + try doc_strings.append(arena, doc); + break :def tree.tokenSlice(decl_handle.nameToken()); + }, + .error_union_error => |payload| def: { + const name = tree.tokenSlice(payload.identifier); + const doc = try arena.print( + "`{s}` captures the error handled by this construct.", + .{name}, + ); + try doc_strings.append(arena, doc); + break :def tree.tokenSlice(decl_handle.nameToken()); + }, + .for_loop_payload => |payload| def: { + const name = tree.tokenSlice(payload.identifier); + + var doc: []u8 = undefined; + if (decl_handle.isCaptureByRef()) { + doc = try arena.print( + "`{s}` captures a pointer to the current element for this iteration.", + .{name}, + ); + } else { + doc = try arena.print( + "`{s}` captures the current element or range value for this iteration.", + .{name}, + ); + } + try doc_strings.append(arena, doc); + break :def tree.tokenSlice(decl_handle.nameToken()); + }, + .switch_payload => def: { + const name = tree.tokenSlice(decl_handle.nameToken()); + + var doc: []u8 = undefined; + if (decl_handle.isCaptureByRef()) { + doc = try arena.print( + "`{s}` captures a pointer to the value associated with the matching switch case.", + .{name}, + ); + } else { + doc = try arena.print( + "`{s}` captures the value associated with the matching switch case.", + .{name}, + ); + } + try doc_strings.append(arena, doc); + break :def name; + }, + .switch_inline_tag_payload => def: { + const name = tree.tokenSlice(decl_handle.nameToken()); + const doc = try arena.print( + "`{s}` captures the matched tag for this inline switch case.", + .{name}, + ); + try doc_strings.append(arena, doc); + break :def name; + }, + .label => |label| def: { + const name = tree.tokenSlice(label.identifier); + const doc = try arena.print( + "`{s}` labels this block or loop so control-flow statements can target it.", + .{name}, + ); + try doc_strings.append(arena, doc); + break :def name; + }, .assign_destructure, - .switch_payload, - .switch_inline_tag_payload, - .label, .error_token, => tree.tokenSlice(decl_handle.nameToken()), }; diff --git a/tests/lsp_features/hover.zig b/tests/lsp_features/hover.zig index 8e34e474fd..ac725cdcab 100644 --- a/tests/lsp_features/hover.zig +++ b/tests/lsp_features/hover.zig @@ -8,6 +8,245 @@ const offsets = zls.offsets; const allocator: std.mem.Allocator = std.testing.allocator; +test "optional_payload" { + try testHover( + \\fn bar() void { + \\ const foo: ?u8 = 8; + \\ while (foo) |num| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(u8) + \\``` + \\ + \\`num` captures the unwrapped value of `foo` when it is not null. + ); + try testHover( + \\fn bar() void { + \\ const foo: ?u8 = 8; + \\ if (foo) |*num| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(*u8) + \\``` + \\ + \\`num` captures a pointer to the unwrapped value of `foo` when it is not null. + ); +} + +test "error_union_payload" { + try testHover( + \\fn func() !u8 { return 8; } + \\ + \\fn bar() void { + \\ const foo = func(); + \\ while (foo) |num| {} else |_| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(u8) + \\``` + \\ + \\`num` captures the successful value of `foo` when it is not an error. + ); + try testHover( + \\fn func() !u8 { return 8; } + \\ + \\fn bar() void { + \\ const foo = func(); + \\ if (foo) |*num| {} else |_| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(*u8) + \\``` + \\ + \\`num` captures a pointer to the successful value of `foo` when it is not an error. + ); +} + +test "error_union_error" { + try testHover( + \\fn func() anyerror!u8 { return 8; } + \\ + \\fn bar() void { + \\ const foo = func(); + \\ while (foo) |_| {} else |failed| {} + \\} + , + \\```zig + \\failed + \\``` + \\```zig + \\(anyerror) + \\``` + \\ + \\`failed` captures the error handled by this construct. + ); + try testHover( + \\fn func() anyerror!u8 { return 8; } + \\ + \\fn bar() void { + \\ const foo = func(); + \\ if (foo) |_| {} else |failed| {} + \\} + , + \\```zig + \\failed + \\``` + \\```zig + \\(anyerror) + \\``` + \\ + \\`failed` captures the error handled by this construct. + ); + try testHover( + \\fn func() anyerror!u8 { return 8; } + \\ + \\fn bar() void { + \\ const foo = func() catch |failed| {}; + \\} + , + \\```zig + \\failed + \\``` + \\```zig + \\(anyerror) + \\``` + \\ + \\`failed` captures the error handled by this construct. + ); +} + +test "for_loop_payload" { + try testHover( + \\fn foo() void { + \\ const numbers = [_]u8{ 1, 2, 3 }; + \\ for (numbers) |num| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(u8) + \\``` + \\ + \\`num` captures the current element or range value for this iteration. + ); + try testHover( + \\fn foo(numbers: []u8) void { + \\ for (numbers) |*num| {} + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(*u8) + \\``` + \\ + \\`num` captures a pointer to the current element for this iteration. + ); +} + +test "switch_payload" { + try testHover( + \\const letter = enum { a, b }; + \\const Bar = union(letter) { a: u8, b: u16, }; + \\ + \\fn foo(bar: Bar) void { + \\ switch (bar) { + \\ .a => |num| {}, + \\ else => {}, + \\ } + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(u8) + \\``` + \\ + \\`num` captures the value associated with the matching switch case. + ); + try testHover( + \\const letter = enum { a, b }; + \\const Bar = union(letter) { a: u8, b: u16, }; + \\ + \\fn foo(bar: *Bar) void { + \\ switch (bar.*) { + \\ .a => |*num| {}, + \\ else => {}, + \\ } + \\} + , + \\```zig + \\num + \\``` + \\```zig + \\(*u8) + \\``` + \\ + \\`num` captures a pointer to the value associated with the matching switch case. + ); +} + +test "switch_inline_tag_payload" { + try testHover( + \\const letter = enum { a, b }; + \\const Bar = union(letter) { a: u8, b: u16, }; + \\ + \\fn foo(bar: Bar) void { + \\ switch (bar) { inline .a => |_, tag| {}, else => {}, } + \\} + , + \\```zig + \\tag + \\``` + \\```zig + \\(letter) + \\``` + \\ + \\Go to [letter](untitled:///Untitled-0.zig#L1) + \\ + \\`tag` captures the matched tag for this inline switch case. + ); + try testHover( + \\const letter = enum { a, b }; + \\const Bar = union(letter) { a: u8, b: u16, }; + \\ + \\fn foo(bar: Bar) void { + \\ switch (bar) { inline else => |_, tag| {} } + \\} + , + \\```zig + \\tag + \\``` + \\```zig + \\(letter) + \\``` + \\ + \\Go to [letter](untitled:///Untitled-0.zig#L1) + \\ + \\`tag` captures the matched tag for this inline switch case. + ); +} + test "primitive" { try testHover( \\const foo = bool; @@ -675,6 +914,8 @@ test "block label" { \\```zig \\(i32) \\``` + \\ + \\`baz` labels this block or loop so control-flow statements can target it. ); try testHover( \\const foo: i32 = undefined; @@ -688,6 +929,8 @@ test "block label" { \\```zig \\(i32) \\``` + \\ + \\`baz` labels this block or loop so control-flow statements can target it. ); }