diff --git a/src/features/inlay_hints.zig b/src/features/inlay_hints.zig index c822c79bf..d3e05e991 100644 --- a/src/features/inlay_hints.zig +++ b/src/features/inlay_hints.zig @@ -19,13 +19,13 @@ const data = @import("version_data"); pub const inlay_hints_exclude_builtins: []const u8 = &.{}; pub const InlayHint = struct { - token_index: Ast.TokenIndex, + index: usize, label: []const u8, kind: types.InlayHintKind, - tooltip: types.MarkupContent, + tooltip: ?types.MarkupContent, fn lessThan(_: void, lhs: InlayHint, rhs: InlayHint) bool { - return lhs.token_index < rhs.token_index; + return lhs.index < rhs.index; } }; @@ -51,7 +51,7 @@ const Builder = struct { }; try self.hints.append(self.arena, .{ - .token_index = token_index, + .index = offsets.tokenToIndex(self.handle.tree, token_index), .label = try std.fmt.allocPrint(self.arena, "{s}:", .{label}), .kind = .Parameter, .tooltip = .{ @@ -69,23 +69,22 @@ const Builder = struct { var converted_hints = try self.arena.alloc(types.InlayHint, self.hints.items.len); for (converted_hints, self.hints.items) |*converted_hint, hint| { - const index = offsets.tokenToIndex(self.handle.tree, hint.token_index); const position = offsets.advancePosition( self.handle.tree.source, last_position, last_index, - index, + hint.index, offset_encoding, ); - defer last_index = index; + defer last_index = hint.index; defer last_position = position; converted_hint.* = types.InlayHint{ .position = position, .label = .{ .string = hint.label }, .kind = hint.kind, - .tooltip = .{ .MarkupContent = hint.tooltip }, + .tooltip = if (hint.tooltip) |tooltip| .{ .MarkupContent = tooltip } else null, .paddingLeft = false, - .paddingRight = true, + .paddingRight = hint.kind == .Parameter, }; } return converted_hints; @@ -188,22 +187,25 @@ fn writeBuiltinHint(builder: *Builder, parameters: []const Ast.Node.Index, argum const colonIndex = std.mem.indexOfScalar(u8, arg, ':'); const type_expr: []const u8 = if (colonIndex) |index| arg[index + 1 ..] else &.{}; - var label: ?[]const u8 = null; + var maybe_label: ?[]const u8 = null; var no_alias = false; var comp_time = false; var it = std.mem.splitScalar(u8, arg[0 .. colonIndex orelse arg.len], ' '); while (it.next()) |item| { if (item.len == 0) continue; - label = item; + maybe_label = item; no_alias = no_alias or std.mem.eql(u8, item, "noalias"); comp_time = comp_time or std.mem.eql(u8, item, "comptime"); } + const label = maybe_label orelse return; + if (label.len == 0 or std.mem.eql(u8, label, "...")) return; + try builder.appendParameterHint( tree.firstToken(parameter), - label orelse "", + label, std.mem.trim(u8, type_expr, " \t\n"), no_alias, comp_time, @@ -211,6 +213,41 @@ fn writeBuiltinHint(builder: *Builder, parameters: []const Ast.Node.Index, argum } } +/// Takes a variable declaration AST node. If the type is inferred, attempt to infer it and display it as a hint. +fn writeVariableDeclHint(builder: *Builder, decl_node: Ast.Node.Index) !void { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + const handle = builder.handle; + const tree = handle.tree; + + const hint = tree.fullVarDecl(decl_node) orelse return; + if (hint.ast.type_node != 0) return; + + const resolved_type = try builder.analyser.resolveTypeOfNode(.{ .handle = handle, .node = decl_node }) orelse return; + + var type_references = Analyser.ReferencedType.Set.init(builder.arena); + var reference_collector = Analyser.ReferencedType.Collector.init(&type_references); + + var type_str: []const u8 = ""; + try builder.analyser.referencedTypes( + resolved_type, + &type_str, + &reference_collector, + ); + if (type_str.len == 0) return; + + try builder.hints.append(builder.arena, .{ + .index = offsets.tokenToLoc(tree, hint.ast.mut_token + 1).end, + .label = try std.fmt.allocPrint(builder.arena, ": {s}", .{ + type_str, + }), + // TODO: Implement on-hover stuff. + .tooltip = null, + .kind = .Type, + }); +} + /// takes a Ast.full.Call (a function call), analysis its function expression, finds its declaration and writes parameter hints into `builder.hints` fn writeCallNodeHint(builder: *Builder, call: Ast.full.Call) !void { const tracy_zone = tracy.trace(@src()); @@ -286,7 +323,13 @@ fn writeNodeInlayHint( const call = tree.fullCall(¶ms, node).?; try writeCallNodeHint(builder, call); }, - + .local_var_decl, + .simple_var_decl, + .global_var_decl, + .aligned_var_decl, + => { + try writeVariableDeclHint(builder, node); + }, .builtin_call_two, .builtin_call_two_comma, .builtin_call, diff --git a/tests/lsp_features/inlay_hints.zig b/tests/lsp_features/inlay_hints.zig index 16dd681e7..bb4a68e71 100644 --- a/tests/lsp_features/inlay_hints.zig +++ b/tests/lsp_features/inlay_hints.zig @@ -12,29 +12,30 @@ const offsets = zls.offsets; const allocator: std.mem.Allocator = std.testing.allocator; test "inlayhints - empty" { - try testInlayHints(""); + try testInlayHints("", .Parameter); + try testInlayHints("", .Type); } test "inlayhints - function call" { try testInlayHints( \\fn foo(alpha: u32) void {} \\const _ = foo(5); - ); + , .Parameter); try testInlayHints( \\fn foo(alpha: u32, beta: u64) void {} \\const _ = foo(5,4); - ); + , .Parameter); try testInlayHints( \\fn foo(alpha: u32, beta: u64) void {} \\const _ = foo( 3 + 2 , (3 - 2)); - ); + , .Parameter); try testInlayHints( \\fn foo(alpha: u32, beta: u64) void {} \\const _ = foo( \\ 3 + 2, \\ (3 - 2), \\); - ); + , .Parameter); } test "inlayhints - function self parameter" { @@ -42,21 +43,21 @@ test "inlayhints - function self parameter" { \\const Foo = struct { pub fn bar(self: *Foo, alpha: u32) void {} }; \\const foo: Foo = .{}; \\const _ = foo.bar(5); - ); + , .Parameter); try testInlayHints( \\const Foo = struct { pub fn bar(_: Foo, alpha: u32, beta: []const u8) void {} }; \\const foo: Foo = .{}; \\const _ = foo.bar(5,""); - ); + , .Parameter); try testInlayHints( \\const Foo = struct { pub fn bar(self: Foo, alpha: u32, beta: anytype) void {} }; \\const foo: Foo = .{}; \\const _ = foo.bar(5,4); - ); + , .Parameter); try testInlayHints( \\const Foo = struct { pub fn bar(self: Foo, alpha: u32, beta: []const u8) void {} }; \\const _ = Foo.bar(undefined,5,""); - ); + , .Parameter); try testInlayHints( \\const Foo = struct { \\ pub fn bar(self: Foo, alpha: u32, beta: []const u8) void {} @@ -64,7 +65,7 @@ test "inlayhints - function self parameter" { \\ bar(undefined,5,""); \\ } \\}; - ); + , .Parameter); } test "inlayhints - resolve alias" { @@ -72,23 +73,48 @@ test "inlayhints - resolve alias" { \\fn foo(alpha: u32) void {} \\const bar = foo; \\const _ = bar(5); - ); + , .Parameter); } test "inlayhints - builtin call" { try testInlayHints( \\const _ = @memcpy("",""); - ); - + , .Parameter); try testInlayHints( - \\const _ = @sizeOf(u32); - ); + \\const _ = @sizeOf(u32); + , .Parameter); try testInlayHints( \\const _ = @TypeOf(5); - ); + , .Parameter); +} + +test "inlayhints - var decl" { + try testInlayHints( + \\const foo = 5; + , .Type); + try testInlayHints( + \\const foo<**const [3:0]u8> = &"Bar"; + , .Type); + try testInlayHints( + \\const foo: *[]const u8 = &"Bar"; + \\const baz<**[]const u8> = &foo; + , .Type); + try testInlayHints( + \\const Foo = struct { bar: u32 }; + \\const Error = error{e}; + \\fn test_context() !void { + \\ const baz: ?Foo = Foo{ .bar = 42 }; + \\ if (baz) |b| { + \\ const d: Error!?Foo = b; + \\ const e<*Error!?Foo> = &d; + \\ const f = (try e.*).?; + \\ _ = f; + \\ } + \\} + , .Type); } -fn testInlayHints(source: []const u8) !void { +fn testInlayHints(source: []const u8, kind: types.InlayHintKind) !void { var phr = try helper.collectClearPlaceholders(allocator, source); defer phr.deinit(allocator); @@ -113,6 +139,9 @@ fn testInlayHints(source: []const u8) !void { return error.InvalidResponse; }; + var visited = try std.DynamicBitSetUnmanaged.initEmpty(allocator, hints.len); + defer visited.deinit(allocator); + var error_builder = ErrorBuilder.init(allocator); defer error_builder.deinit(); errdefer error_builder.writeDebug(); @@ -125,25 +154,50 @@ fn testInlayHints(source: []const u8) !void { const position = offsets.indexToPosition(phr.new_source, new_loc.start, ctx.server.offset_encoding); - for (hints) |hint| { + for (hints, 0..) |hint, i| { if (position.line != hint.position.line or position.character != hint.position.character) continue; + if (hint.kind.? != kind) continue; - if (!std.mem.endsWith(u8, hint.label.string, ":")) { - try error_builder.msgAtLoc("label `{s}` must end with a colon!", test_uri, new_loc, .err, .{hint.label.string}); + if (visited.isSet(i)) { + try error_builder.msgAtIndex("duplicate inlay hint here!", test_uri, new_loc.start, .err, .{}); + continue :outer; + } else { + visited.set(i); } - const actual_label = hint.label.string[0 .. hint.label.string.len - 1]; + + const actual_label = switch (kind) { + .Parameter => blk: { + if (!std.mem.endsWith(u8, hint.label.string, ":")) { + try error_builder.msgAtLoc("label `{s}` must end with a colon!", test_uri, new_loc, .err, .{hint.label.string}); + continue :outer; + } + break :blk hint.label.string[0 .. hint.label.string.len - 1]; + }, + .Type => blk: { + if (!std.mem.startsWith(u8, hint.label.string, ": ")) { + try error_builder.msgAtLoc("label `{s}` must start with \": \"!", test_uri, new_loc, .err, .{hint.label.string}); + continue :outer; + } + break :blk hint.label.string[2..hint.label.string.len]; + }, + }; if (!std.mem.eql(u8, expected_label, actual_label)) { try error_builder.msgAtLoc("expected label `{s}` here but got `{s}`!", test_uri, new_loc, .err, .{ expected_label, actual_label }); } - if (hint.kind.? != types.InlayHintKind.Parameter) { - try error_builder.msgAtLoc("hint kind should be `{s}` but got `{s}`!", test_uri, new_loc, .err, .{ @tagName(types.InlayHintKind.Parameter), @tagName(hint.kind.?) }); - } continue :outer; } try error_builder.msgAtLoc("expected hint `{s}` here", test_uri, new_loc, .err, .{expected_label}); } + var it = visited.iterator(.{ .kind = .unset }); + while (it.next()) |index| { + const hint = hints[index]; + if (hint.kind.? != kind) continue; + const source_index = offsets.positionToIndex(phr.new_source, hint.position, ctx.server.offset_encoding); + try error_builder.msgAtIndex("unexpected inlay hint `{s}` here!", test_uri, source_index, .err, .{hint.label.string}); + } + if (error_builder.hasMessages()) return error.InvalidResponse; }