Skip to content

Commit

Permalink
add inlay type hints in variable declarations (zigtools#1444)
Browse files Browse the repository at this point in the history
Co-authored-by: Techatrix <19954306+Techatrix@users.noreply.github.com>
  • Loading branch information
2 people authored and KoltPenny committed Oct 18, 2023
1 parent c991000 commit 7b55716
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 37 deletions.
69 changes: 56 additions & 13 deletions src/features/inlay_hints.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

Expand All @@ -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 = .{
Expand All @@ -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;
Expand Down Expand Up @@ -188,29 +187,67 @@ 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,
);
}
}

/// 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());
Expand Down Expand Up @@ -286,7 +323,13 @@ fn writeNodeInlayHint(
const call = tree.fullCall(&params, 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,
Expand Down
102 changes: 78 additions & 24 deletions tests/lsp_features/inlay_hints.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,83 +12,109 @@ 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(<alpha>5);
);
, .Parameter);
try testInlayHints(
\\fn foo(alpha: u32, beta: u64) void {}
\\const _ = foo(<alpha>5,<beta>4);
);
, .Parameter);
try testInlayHints(
\\fn foo(alpha: u32, beta: u64) void {}
\\const _ = foo( <alpha>3 + 2 , <beta>(3 - 2));
);
, .Parameter);
try testInlayHints(
\\fn foo(alpha: u32, beta: u64) void {}
\\const _ = foo(
\\ <alpha>3 + 2,
\\ <beta>(3 - 2),
\\);
);
, .Parameter);
}

test "inlayhints - function self parameter" {
try testInlayHints(
\\const Foo = struct { pub fn bar(self: *Foo, alpha: u32) void {} };
\\const foo: Foo = .{};
\\const _ = foo.bar(<alpha>5);
);
, .Parameter);
try testInlayHints(
\\const Foo = struct { pub fn bar(_: Foo, alpha: u32, beta: []const u8) void {} };
\\const foo: Foo = .{};
\\const _ = foo.bar(<alpha>5,<beta>"");
);
, .Parameter);
try testInlayHints(
\\const Foo = struct { pub fn bar(self: Foo, alpha: u32, beta: anytype) void {} };
\\const foo: Foo = .{};
\\const _ = foo.bar(<alpha>5,<beta>4);
);
, .Parameter);
try testInlayHints(
\\const Foo = struct { pub fn bar(self: Foo, alpha: u32, beta: []const u8) void {} };
\\const _ = Foo.bar(<self>undefined,<alpha>5,<beta>"");
);
, .Parameter);
try testInlayHints(
\\const Foo = struct {
\\ pub fn bar(self: Foo, alpha: u32, beta: []const u8) void {}
\\ pub fn foo() void {
\\ bar(<self>undefined,<alpha>5,<beta>"");
\\ }
\\};
);
, .Parameter);
}

test "inlayhints - resolve alias" {
try testInlayHints(
\\fn foo(alpha: u32) void {}
\\const bar = foo;
\\const _ = bar(<alpha>5);
);
, .Parameter);
}

test "inlayhints - builtin call" {
try testInlayHints(
\\const _ = @memcpy(<dest>"",<source>"");
);

, .Parameter);
try testInlayHints(
\\const _ = @sizeOf(u32);
);
\\const _ = @sizeOf(<T>u32);
, .Parameter);
try testInlayHints(
\\const _ = @TypeOf(5);
);
, .Parameter);
}

test "inlayhints - var decl" {
try testInlayHints(
\\const foo<comptime_int> = 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<type> = struct { bar: u32 };
\\const Error<type> = 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<Foo> = (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);

Expand All @@ -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();
Expand All @@ -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;
}

0 comments on commit 7b55716

Please sign in to comment.