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 28, 2024
1 parent 2720509 commit 34e8935
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
32 changes: 23 additions & 9 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,13 @@ fn kindToSortScore(kind: types.CompletionItemKind) ?[]const u8 {
};
}

fn completeDot(document_store: *DocumentStore, analyser: *Analyser, arena: std.mem.Allocator, handle: *DocumentStore.Handle, loc: offsets.Loc) error{OutOfMemory}![]types.CompletionItem {
fn completeDot(
server: *Server,
analyser: *Analyser,
arena: std.mem.Allocator,
handle: *DocumentStore.Handle,
loc: offsets.Loc,
) error{OutOfMemory}![]types.CompletionItem {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

Expand All @@ -720,6 +726,7 @@ fn completeDot(document_store: *DocumentStore, analyser: *Analyser, arena: std.m
if (dot_token_index < 2) return &.{};

var completions = std.ArrayListUnmanaged(types.CompletionItem){};
const use_snippets = server.config.enable_snippets and server.client_capabilities.supports_snippets;

blk: {
const dot_context = getEnumLiteralContext(tree, dot_token_index) orelse break :blk;
Expand All @@ -731,10 +738,7 @@ fn completeDot(document_store: *DocumentStore, analyser: *Analyser, arena: std.m
&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(arena, container, &completions);
try collectContainerFields(arena, use_snippets, dot_context.likely, container, &completions);
}
}

Expand All @@ -746,7 +750,7 @@ fn completeDot(document_store: *DocumentStore, analyser: *Analyser, arena: std.m
if (token_tags[dot_token_index - 1] == .number_literal or token_tags[dot_token_index - 1] != .equal) return &.{};

// `var enum_val = .` or the get*Context logic failed because of syntax errors (parser didn't create the necessary node(s))
const enum_completions = try globalSetCompletions(document_store, arena, handle, .enum_set);
const enum_completions = try globalSetCompletions(&server.document_store, arena, handle, .enum_set);
return enum_completions;
}

Expand Down Expand Up @@ -900,7 +904,7 @@ pub fn completionAtIndex(server: *Server, analyser: *Analyser, arena: std.mem.Al
.var_access, .empty => try completeGlobal(server, analyser, arena, handle, source_index),
.field_access => |loc| try completeFieldAccess(server, analyser, arena, handle, source_index, loc),
.global_error_set => try completeError(server, arena, handle),
.enum_literal => |loc| try completeDot(&server.document_store, analyser, arena, handle, loc),
.enum_literal => |loc| try completeDot(server, analyser, arena, handle, loc),
.label => try completeLabel(server, analyser, arena, handle, source_index),
.import_string_literal,
.cinclude_string_literal,
Expand Down Expand Up @@ -1266,6 +1270,8 @@ fn getSwitchOrStructInitContext(
/// Given a Type that is a container, adds it's `.container_field*`s to completions
pub fn collectContainerFields(
arena: std.mem.Allocator,
snippets_ok: bool,
likely: EnumLiteralContext.Likely,
container: Analyser.Type,
completions: *std.ArrayListUnmanaged(types.CompletionItem),
) error{OutOfMemory}!void {
Expand All @@ -1280,11 +1286,19 @@ 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 completions.append(arena, .{
if (likely != .struct_field and !field.ast.tuple_like) {
try completions.append(arena, .{
.label = name,
.kind = if (field.ast.tuple_like) .EnumMember else .Field,
.detail = Analyser.getContainerFieldSignature(handle.tree, field),
.insertText = if (snippets_ok) try std.fmt.allocPrint(arena, "{{ .{s} = $1 }}$0", .{name}) else try std.fmt.allocPrint(arena, "{{ .{s} = ", .{name}),
.insertTextFormat = if (snippets_ok) .Snippet else .PlainText,
});
} else try completions.append(arena, .{
.label = name,
.kind = if (field.ast.tuple_like) .EnumMember else .Field,
.detail = Analyser.getContainerFieldSignature(handle.tree, field),
.insertText = name,
.insertText = if (field.ast.tuple_like) name else try std.fmt.allocPrint(arena, "{s} = ", .{name}),
.insertTextFormat = .PlainText,
});
}
Expand Down
88 changes: 80 additions & 8 deletions tests/lsp_features/completion.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Completion = struct {
documentation: ?[]const u8 = null,
insert_text: ?[]const u8 = null,
deprecated: bool = false,
insert_text_format: ?types.InsertTextFormat = null,
};

const CompletionSet = std.StringArrayHashMapUnmanaged(Completion);
Expand Down Expand Up @@ -1270,6 +1271,45 @@ test "enum" {
});
}

test "union(enum)" {
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, .insert_text_format = .PlainText, .insert_text = "alpha" },
.{ .label = "beta", .kind = .Field, .insert_text_format = .Snippet, .insert_text = "{ .beta = $1 }$0" },
});
try testCompletionWithOptions(
\\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, .insert_text_format = .PlainText, .insert_text = "alpha" },
.{ .label = "beta", .kind = .Field, .insert_text_format = .PlainText, .insert_text = "{ .beta = " },
}, .{
.enable_snippets = false,
});
}

test "global enum set" {
try testCompletion(
\\const SomeError = error{ e };
Expand Down Expand Up @@ -1692,6 +1732,40 @@ test "struct init" {
.{ .label = "beta", .kind = .Field, .detail = "u32" },
.{ .label = "alpha", .kind = .Field, .detail = "*const S" },
});
try testCompletionWithOptions(
\\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", .insert_text_format = .Snippet, .insert_text = "{ .alpha = $1 }$0" },
.{ .label = "beta", .kind = .Field, .detail = "u32" },
.{ .label = "gamma", .kind = .Field, .detail = "?S = null" },
}, .{
.enable_snippets = true,
});
try testCompletionWithOptions(
\\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", .insert_text_format = .PlainText, .insert_text = "{ .alpha = " },
.{ .label = "beta", .kind = .Field, .detail = "u32" },
.{ .label = "gamma", .kind = .Field, .detail = "?S = null" },
}, .{
.enable_snippets = false,
});
try testCompletion(
\\const S = struct { alpha: u32 };
\\fn foo(s: *S) void { s = .{.<cursor>} }
Expand Down Expand Up @@ -2331,7 +2405,7 @@ test "snippet - function with `self` parameter" {
\\const s = S{};
\\s.<cursor>
, &.{
.{ .label = "f", .kind = .Method, .detail = "fn (self: S) void", .insert_text = "f()" },
.{ .label = "f", .kind = .Method, .detail = "fn (self: S) void", .insert_text_format = .Snippet, .insert_text = "f()" },
}, .{
.enable_argument_placeholders = false,
});
Expand All @@ -2341,15 +2415,15 @@ test "snippet - function with `self` parameter" {
\\};
\\S.<cursor>
, &.{
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f(${1:self: S})" },
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text_format = .Snippet, .insert_text = "f(${1:self: S})" },
});
try testCompletionWithOptions(
\\const S = struct {
\\ fn f(self: S) void {}
\\};
\\S.<cursor>
, &.{
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f(${1:})" },
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text_format = .Snippet, .insert_text = "f(${1:})" },
}, .{
.enable_argument_placeholders = false,
});
Expand All @@ -2373,7 +2447,7 @@ test "snippets disabled" {
\\};
\\S.<cursor>
, &.{
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f" },
.{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text_format = .PlainText, .insert_text = "f" },
}, .{
.enable_snippets = false,
});
Expand Down Expand Up @@ -2537,10 +2611,8 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C
}

if (expected_completion.insert_text) |expected_insert| blk: {
try std.testing.expectEqual(
@as(?types.InsertTextFormat, if (options.enable_snippets) .Snippet else .PlainText),
actual_completion.insertTextFormat,
);
if (expected_completion.insert_text_format) |_|
try std.testing.expectEqual(actual_completion.insertTextFormat, expected_completion.insert_text_format);
const actual_insert = actual_completion.insertText;
if (actual_insert != null and std.mem.eql(u8, expected_insert, actual_insert.?)) break :blk;
try error_builder.msgAtIndex("completion item '{s}' should have insert text '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{
Expand Down

0 comments on commit 34e8935

Please sign in to comment.