Skip to content

Commit

Permalink
Fix/improve union(enum) completions
Browse files Browse the repository at this point in the history
  • Loading branch information
llogick committed Feb 29, 2024
1 parent 7d6a9e2 commit 06e87d2
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 13 deletions.
45 changes: 32 additions & 13 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -701,10 +701,7 @@ fn completeDot(builder: *Builder, loc: offsets.Loc) error{OutOfMemory}!void {
dot_context,
);
for (containers) |container| {
if (dot_context.likely == .enum_arg and !container.isEnumType()) continue;
if (dot_context.likely != .struct_field)
if (!container.isEnumType() and !container.isUnionType()) continue;
try collectContainerFields(builder, container);
try collectContainerFields(builder, dot_context.likely, container);
}
}

Expand Down Expand Up @@ -917,6 +914,7 @@ pub fn completionAtIndex(
else
.{ .TextEdit = .{ .newText = item.insertText orelse item.label, .range = insert_range } };
}
item.insertText = null;
}
}

Expand Down Expand Up @@ -1041,11 +1039,12 @@ fn globalSetCompletions(builder: *Builder, kind: enum { error_set, enum_set }) e

const EnumLiteralContext = struct {
const Likely = enum { // TODO: better name, tagged union?
/// `mye: Enum = .`, `abc.field = .` or `f(.{.field = .` if typeof(field) is enumlike)
/// `== .`, `!= .`, switch case
/// `mye: Enum = .`, `abc.field = .`, `f(.{.field = .`, `switch` case
enum_literal,
/// Same as above, but`f() = .` or `identifier.f() = .` are ignored, ie lhs of `=` is a fn call
enum_assignment,
// `==`, `!=`
enum_comparison,
/// the enum is a fn arg, eg `f(.`
enum_arg,
/// `S{.`, `var s:S = .{.`, `f(.{.` or `a.f(.{.`
Expand Down Expand Up @@ -1078,12 +1077,15 @@ fn getEnumLiteralContext(
var dot_context = EnumLiteralContext{ .likely = .enum_literal };

switch (token_tags[token_index]) {
.equal, .equal_equal, .bang_equal => |tok_tag| {
.equal => {
token_index -= 1;
if (tok_tag == .equal) {
if ((token_tags[token_index] == .r_paren)) return null; // `..) = .`, ie lhs is a fn call
dot_context.likely = .enum_assignment;
}
if ((token_tags[token_index] == .r_paren)) return null; // `..) = .`, ie lhs is a fn call
dot_context.likely = .enum_assignment;
dot_context.identifier_token_index = token_index;
},
.equal_equal, .bang_equal => {
token_index -= 1;
dot_context.likely = .enum_comparison;
dot_context.identifier_token_index = token_index;
},
.l_brace, .comma, .l_paren => {
Expand Down Expand Up @@ -1227,8 +1229,10 @@ fn getSwitchOrStructInitContext(
/// Given a Type that is a container, adds it's `.container_field*`s to completions
pub fn collectContainerFields(
builder: *Builder,
likely: EnumLiteralContext.Likely,
container: Analyser.Type,
) error{OutOfMemory}!void {
const use_snippets = builder.server.config.enable_snippets and builder.server.client_capabilities.supports_snippets;
const node_handle = switch (container.data) {
.other => |n| n,
else => return,
Expand All @@ -1240,10 +1244,25 @@ pub fn collectContainerFields(
for (container_decl.ast.members) |member| {
const field = handle.tree.fullContainerField(member) orelse continue;
const name = handle.tree.tokenSlice(field.ast.main_token);
try builder.completions.append(builder.arena, .{
if (likely != .struct_field and likely != .enum_comparison and !field.ast.tuple_like) {
try builder.completions.append(builder.arena, .{
.label = name,
.kind = if (field.ast.tuple_like) .EnumMember else .Field,
.detail = Analyser.getContainerFieldSignature(handle.tree, field),
.insertText = if (use_snippets)
try std.fmt.allocPrint(builder.arena, "{{ .{s} = $1 }}$0", .{name})
else
try std.fmt.allocPrint(builder.arena, "{{ .{s} = ", .{name}),
.insertTextFormat = if (use_snippets) .Snippet else .PlainText,
});
} else try builder.completions.append(builder.arena, .{
.label = name,
.kind = if (field.ast.tuple_like) .EnumMember else .Field,
.detail = Analyser.getContainerFieldSignature(handle.tree, field),
.insertText = if (field.ast.tuple_like or likely == .enum_comparison)
name
else
try std.fmt.allocPrint(builder.arena, "{s} = ", .{name}),
});
}
}
Expand Down Expand Up @@ -1289,7 +1308,7 @@ fn collectVarAccessContainerNodes(
const fn_proto_node_handle = type_expr.data.other; // this assumes that function types can only be Ast nodes
const fn_proto_node = fn_proto_node_handle.node;
const fn_proto_handle = fn_proto_node_handle.handle;
if (dot_context.likely == .enum_literal or dot_context.need_ret_type) { // => we need f()'s return type
if (dot_context.likely == .enum_comparison or dot_context.need_ret_type) { // => we need f()'s return type
var buf: [1]Ast.Node.Index = undefined;
const full_fn_proto = fn_proto_handle.tree.fullFnProto(&buf, fn_proto_node).?;
const has_body = fn_proto_handle.tree.nodes.items(.tag)[fn_proto_node] == .fn_decl;
Expand Down
100 changes: 100 additions & 0 deletions tests/lsp_features/completion.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,26 @@ test "enum" {
});
}

test "tagged union" {
try testCompletion(
\\const Birdie = enum {
\\ canary,
\\};
\\const Ue = union(enum) {
\\ alpha,
\\ beta: []const u8,
\\};
\\const S = struct{ foo: Ue };
\\test {
\\ const s = S{};
\\ s.foo = .<cursor>
\\}
, &.{
.{ .label = "alpha", .kind = .EnumMember },
.{ .label = "beta", .kind = .Field },
});
}

test "global enum set" {
try testCompletion(
\\const SomeError = error{ e };
Expand Down Expand Up @@ -1691,6 +1711,30 @@ test "struct init" {
.{ .label = "beta", .kind = .Field, .detail = "u32" },
.{ .label = "alpha", .kind = .Field, .detail = "*const S" },
});
try testCompletion(
\\const S = struct {
\\ alpha: *const S,
\\ beta: u32,
\\ gamma: ?S = null,
\\};
\\test {
\\ const foo: S = undefined;
\\ foo.gamma = .<cursor>
\\}
, &.{
.{ .label = "alpha", .kind = .Field, .detail = "*const S" },
.{ .label = "beta", .kind = .Field, .detail = "u32" },
.{ .label = "gamma", .kind = .Field, .detail = "?S = null" },
});
try testCompletion(
\\const S = struct { alpha: u32 };
\\fn foo(s: S) void {}
\\test {
\\ foo(.<cursor>)
\\}
, &.{
.{ .label = "alpha", .kind = .Field, .detail = "u32" },
});
try testCompletion(
\\const S = struct { alpha: u32 };
\\fn foo(s: *S) void { s = .{.<cursor>} }
Expand Down Expand Up @@ -2786,6 +2830,62 @@ test "insert replace behaviour - function with partial argument placeholders" {
});
}

test "insert replace behaviour - struct literal" {
try testCompletionTextEdit(.{
.source =
\\const S = struct { alpha: u32 };
\\const foo: S = .<cursor>
,
.label = "alpha",
.expected_insert_line = "const foo: S = .{ .alpha = ",
.expected_replace_line = "const foo: S = .{ .alpha = ",
});
try testCompletionTextEdit(.{
.source =
\\const S = struct { alpha: u32 };
\\const foo: S = .<cursor>
,
.label = "alpha",
.expected_insert_line = "const foo: S = .{ .alpha = $1 }$0",
.expected_replace_line = "const foo: S = .{ .alpha = $1 }$0",
.enable_snippets = true,
});
}

test "insert replace behaviour - tagged union" {
try testCompletionTextEdit(.{
.source =
\\const Birdie = enum { canary };
\\const U = union(enum) { alpha: []const u8 };
\\const foo: U = .<cursor>
,
.label = "alpha",
.expected_insert_line = "const foo: U = .{ .alpha = $1 }$0",
.expected_replace_line = "const foo: U = .{ .alpha = $1 }$0",
.enable_snippets = true,
});
try testCompletionTextEdit(.{
.source =
\\const Birdie = enum { canary };
\\const U = union(enum) { alpha: []const u8 };
\\const foo: U = .<cursor>
,
.label = "alpha",
.expected_insert_line = "const foo: U = .{ .alpha = ",
.expected_replace_line = "const foo: U = .{ .alpha = ",
});
try testCompletionTextEdit(.{
.source =
\\const U = union(enum) { alpha: []const u8 };
\\const u: U = undefined;
\\const boolean = u == .<cursor>
,
.label = "alpha",
.expected_insert_line = "const boolean = u == .alpha",
.expected_replace_line = "const boolean = u == .alpha",
});
}

test "insert replace behaviour - doc test name" {
if (true) return error.SkipZigTest; // TODO
try testCompletionTextEdit(.{
Expand Down

0 comments on commit 06e87d2

Please sign in to comment.