Skip to content

Commit 8310251

Browse files
FnControlOptionTechatrix
authored andcommitted
Deduplicate completions with branching types
1 parent 695a1d5 commit 8310251

3 files changed

Lines changed: 157 additions & 9 deletions

File tree

src/analyser/completions.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
const std = @import("std");
55
const InternPool = @import("InternPool.zig");
66
const types = @import("lsp").types;
7+
const Completions = @import("../features/completions.zig").Completions;
78

89
/// generates a list of dot completions for the given typed-value in `index`
910
/// the given `index` must belong to the given InternPool
1011
pub fn dotCompletions(
1112
arena: std.mem.Allocator,
12-
completions: *std.ArrayList(types.completion.Item),
13+
completions: *Completions,
1314
ip: *InternPool,
1415
index: InternPool.Index,
1516
) error{OutOfMemory}!void {
@@ -438,9 +439,9 @@ fn testCompletion(
438439
defer arena_allocator.deinit();
439440

440441
const arena = arena_allocator.allocator();
441-
var completions: std.ArrayList(types.completion.Item) = .empty;
442+
var completions: Completions = .empty;
442443

443444
try dotCompletions(arena, &completions, ip, index);
444445

445-
try std.testing.expectEqualDeep(expected, completions.items);
446+
try std.testing.expectEqualDeep(expected, completions.items());
446447
}

src/features/completions.zig

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,82 @@ const Builder = struct {
2424
arena: std.mem.Allocator,
2525
orig_handle: *DocumentStore.Handle,
2626
source_index: usize,
27-
completions: std.ArrayList(types.completion.Item),
27+
completions: Completions,
2828
cached_prepare_function_completion_result: ?PrepareFunctionCompletionResult = null,
2929
use_snippets: bool,
3030
};
3131

32+
pub const Completions = struct {
33+
map: std.StringArrayHashMapUnmanaged(types.completion.Item),
34+
35+
pub const empty: Completions = .{ .map = .empty };
36+
37+
pub fn items(completions: Completions) []types.completion.Item {
38+
return completions.map.values();
39+
}
40+
41+
pub fn ensureUnusedCapacity(
42+
completions: *Completions,
43+
allocator: std.mem.Allocator,
44+
additional_count: usize,
45+
) !void {
46+
try completions.map.ensureUnusedCapacity(allocator, additional_count);
47+
}
48+
49+
pub fn append(
50+
completions: *Completions,
51+
allocator: std.mem.Allocator,
52+
item: types.completion.Item,
53+
) !void {
54+
try completions.ensureUnusedCapacity(allocator, 1);
55+
completions.appendAssumeCapacity(item);
56+
}
57+
58+
pub fn appendSlice(
59+
completions: *Completions,
60+
allocator: std.mem.Allocator,
61+
slice: []const types.completion.Item,
62+
) !void {
63+
try completions.ensureUnusedCapacity(allocator, slice.len);
64+
for (slice) |item|
65+
completions.appendAssumeCapacity(item);
66+
}
67+
68+
pub fn appendAssumeCapacity(
69+
completions: *Completions,
70+
item: types.completion.Item,
71+
) void {
72+
const gop = completions.map.getOrPutAssumeCapacity(item.label);
73+
const ptr = gop.value_ptr;
74+
75+
if (!gop.found_existing) {
76+
ptr.* = item;
77+
return;
78+
}
79+
80+
const keep_detail = eqlSlices(u8, ptr.detail, item.detail);
81+
const keep_label_details =
82+
if (item.labelDetails) |item_details|
83+
if (ptr.labelDetails) |ptr_details|
84+
eqlSlices(u8, ptr_details.detail, item_details.detail) and
85+
eqlSlices(u8, ptr_details.description, item_details.description)
86+
else
87+
false
88+
else
89+
false;
90+
91+
ptr.deprecated = ptr.deprecated orelse item.deprecated;
92+
ptr.tags = ptr.tags orelse item.tags;
93+
if (!keep_detail) ptr.detail = null;
94+
if (!keep_label_details) ptr.labelDetails = null;
95+
}
96+
97+
fn eqlSlices(comptime T: type, a: ?[]const T, b: ?[]const T) bool {
98+
return (a == null and b == null) or
99+
(a != null and b != null and std.mem.eql(T, a.?, b.?));
100+
}
101+
};
102+
32103
fn typeToCompletion(builder: *Builder, ty: Analyser.Type) Analyser.Error!void {
33104
const tracy_zone = tracy.trace(@src());
34105
defer tracy_zone.end();
@@ -175,12 +246,12 @@ fn declToCompletion(builder: *Builder, decl_handle: Analyser.DeclWithHandle) Ana
175246
}
176247
}
177248

178-
const documentation: types.Documentation = .{
249+
const documentation: ?types.Documentation = if (doc_comments.items.len > 0) .{
179250
.markup_content = .{
180251
.kind = if (builder.server.client_capabilities.completion_doc_supports_md) .markdown else .plaintext,
181252
.value = try std.mem.join(builder.arena, "\n\n", doc_comments.items),
182253
},
183-
};
254+
} else null;
184255

185256
try builder.completions.ensureUnusedCapacity(builder.arena, 1);
186257

@@ -906,7 +977,7 @@ fn completeFileSystemStringLiteral(builder: *Builder, pos_context: Analyser.Posi
906977
const string_content_range = offsets.locToRange(source, .{ .start = insert_loc.start, .end = string_content_loc.end }, builder.server.offset_encoding);
907978

908979
// completions on module replace the entire string literal
909-
for (builder.completions.items) |*item| {
980+
for (builder.completions.items()) |*item| {
910981
if (item.kind == .Module and item.textEdit == null) {
911982
item.textEdit = createTextEdit(builder, .{ .newText = item.label, .insert = insert_range, .replace = string_content_range });
912983
}
@@ -1011,7 +1082,7 @@ pub fn completionAtIndex(
10111082

10121083
if (line_until_index.len == 0 or std.zig.isValidId(line_until_index)) {
10131084
try populateSnippedCompletions(&builder, .top_level);
1014-
return .{ .isIncomplete = false, .items = builder.completions.items };
1085+
return .{ .isIncomplete = false, .items = builder.completions.items() };
10151086
}
10161087

10171088
const pos_context = try Analyser.getPositionContext(arena, &handle.tree, source_index, false);
@@ -1035,7 +1106,7 @@ pub fn completionAtIndex(
10351106
else => return null,
10361107
}
10371108

1038-
const completions = builder.completions.items;
1109+
const completions = builder.completions.items();
10391110
if (completions.len == 0) return null;
10401111

10411112
const identifier_loc = prepareCompletionLoc(&handle.tree, source_index);

tests/lsp_features/completion.zig

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3475,6 +3475,58 @@ test "either" {
34753475
});
34763476
}
34773477

3478+
test "either - fields and methods with same name" {
3479+
try testCompletionWithOptions(
3480+
\\const Foo = struct {
3481+
\\ alpha: u32,
3482+
\\ beta: []const u8,
3483+
\\ fn gamma(_: @This()) void {}
3484+
\\ fn delta(_: @This()) f32 {}
3485+
\\};
3486+
\\const Bar = struct {
3487+
\\ alpha: u32,
3488+
\\ beta: bool,
3489+
\\ fn gamma(_: @This()) void {}
3490+
\\ fn delta(_: @This()) f64 {}
3491+
\\};
3492+
\\const fizz: if (undefined) Foo else Bar = undefined;
3493+
\\const buzz = fizz.<cursor>
3494+
, &.{
3495+
.{
3496+
.label = "alpha",
3497+
.labelDetails = .{
3498+
.detail = null,
3499+
.description = "u32",
3500+
},
3501+
.kind = .Field,
3502+
.detail = "u32",
3503+
},
3504+
.{
3505+
.label = "beta",
3506+
.labelDetails = null,
3507+
.kind = .Field,
3508+
.detail = null,
3509+
},
3510+
.{
3511+
.label = "gamma",
3512+
.labelDetails = .{
3513+
.detail = "()",
3514+
.description = "void",
3515+
},
3516+
.kind = .Method,
3517+
.detail = null,
3518+
},
3519+
.{
3520+
.label = "delta",
3521+
.labelDetails = null,
3522+
.kind = .Method,
3523+
.detail = null,
3524+
},
3525+
}, .{
3526+
.check_null_fields = true,
3527+
});
3528+
}
3529+
34783530
test "container type inside switch case value" {
34793531
try testCompletion(
34803532
\\test {
@@ -4642,6 +4694,7 @@ fn testCompletionWithOptions(
46424694
enable_snippets: bool = true,
46434695
completion_label_details: bool = true,
46444696
check_order: bool = false,
4697+
check_null_fields: bool = false,
46454698
},
46464699
) !void {
46474700
const cursor_idx = std.mem.find(u8, source, "<cursor>").?;
@@ -4742,6 +4795,14 @@ fn testCompletionWithOptions(
47424795
if (actual_doc) |str| std.zig.fmtString(str) else null,
47434796
});
47444797
return error.InvalidCompletionDoc;
4798+
} else blk: {
4799+
if (!options.check_null_fields) break :blk;
4800+
const actual_doc = actual_completion.documentation orelse break :blk;
4801+
try error_builder.msgAtIndex("completion item '{s}' has unexpected doc '{f}'", test_uri.raw, cursor_idx, .err, .{
4802+
label,
4803+
std.zig.fmtString(actual_doc.markup_content.value),
4804+
});
4805+
return error.InvalidCompletionDoc;
47454806
}
47464807

47474808
try std.testing.expect(actual_completion.insertText == null); // 'insertText' is subject to interpretation on the client so 'textEdit' should be preferred
@@ -4759,6 +4820,14 @@ fn testCompletionWithOptions(
47594820
actual_completion.detail,
47604821
});
47614822
return error.InvalidCompletionDetail;
4823+
} else blk: {
4824+
if (!options.check_null_fields) break :blk;
4825+
const actual_detail = actual_completion.detail orelse break :blk;
4826+
try error_builder.msgAtIndex("completion item '{s}' has unexpected detail '{s}'", test_uri.raw, cursor_idx, .err, .{
4827+
label,
4828+
actual_detail,
4829+
});
4830+
return error.InvalidCompletionDetail;
47624831
}
47634832

47644833
if (expected_completion.labelDetails) |expected_label_details| {
@@ -4789,6 +4858,13 @@ fn testCompletionWithOptions(
47894858
});
47904859
return error.InvalidCompletionLabelDetails;
47914860
}
4861+
} else blk: {
4862+
if (!options.check_null_fields) break :blk;
4863+
if (actual_completion.labelDetails == null) break :blk;
4864+
try error_builder.msgAtIndex("completion item '{s}' has unexpected label details", test_uri.raw, cursor_idx, .err, .{
4865+
label,
4866+
});
4867+
return error.InvalidCompletionLabelDetails;
47924868
}
47934869

47944870
blk: {

0 commit comments

Comments
 (0)