From 2489add07dae4f05b85703bf212cc253caef6a05 Mon Sep 17 00:00:00 2001 From: Techatrix <19954306+Techatrix@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:36:07 +0100 Subject: [PATCH] improve completions on functions - bring back the label details (accidentally removed in #1746) - remove the function name from the detail - differentiate between functions and methods - add empty parentheses when there are no arguments even if snippets are disabled --- src/Server.zig | 2 + src/analysis.zig | 163 ++++++++++++++++++-------- src/features/completions.zig | 129 ++++++++++++++------- tests/lsp_features/completion.zig | 184 ++++++++++++++++++++++-------- 4 files changed, 341 insertions(+), 137 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index af9ae3f80..7068089e0 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -69,6 +69,7 @@ const ClientCapabilities = struct { hover_supports_md: bool = false, signature_help_supports_md: bool = false, completion_doc_supports_md: bool = false, + label_details_support: bool = false, supports_configuration: bool = false, supports_workspace_did_change_configuration_dynamic_registration: bool = false, supports_textDocument_definition_linkSupport: bool = false, @@ -431,6 +432,7 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi } if (textDocument.completion) |completion| { if (completion.completionItem) |completionItem| { + server.client_capabilities.label_details_support = completionItem.labelDetailsSupport orelse false; server.client_capabilities.supports_snippets = completionItem.snippetSupport orelse false; if (completionItem.documentationFormat) |documentation_format| { for (documentation_format) |format| { diff --git a/src/analysis.zig b/src/analysis.zig index 24c1f62c8..ceec70ae5 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -135,7 +135,7 @@ fn formatSnippetPlaceholder( options: std.fmt.FormatOptions, writer: anytype, ) !void { - _ = fmt; + if (fmt.len != 0) std.fmt.invalidFmtError(fmt, data); _ = options; var split_it = std.mem.splitScalar(u8, data, '}'); @@ -148,74 +148,139 @@ fn formatSnippetPlaceholder( } } -const SnippetPlaceholderFormatter = std.fmt.Formatter(formatSnippetPlaceholder); - -fn fmtSnippetPlaceholder(bytes: []const u8) SnippetPlaceholderFormatter { +fn fmtSnippetPlaceholder(bytes: []const u8) std.fmt.Formatter(formatSnippetPlaceholder) { return .{ .data = bytes }; } -/// Creates snippet insert text for a function. Caller owns returned memory. -pub fn getFunctionSnippet( - allocator: std.mem.Allocator, - name: []const u8, - iterator: *Ast.full.FnProto.Iterator, -) ![]const u8 { - const tree = iterator.tree.*; +pub const FormatFunctionOptions = struct { + fn_proto: Ast.full.FnProto, + tree: *const Ast, + + include_fn_keyword: bool, + /// only included if available + include_name: bool, + skip_first_param: bool = false, + include_parameter_modifiers: bool, + include_parameter_names: bool, + include_parameter_types: bool, + include_return_type: bool, + snippet_placeholders: bool, +}; + +pub fn formatFunction( + data: FormatFunctionOptions, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + if (fmt.len != 0) std.fmt.invalidFmtError(fmt, data); + _ = options; - var buffer = std.ArrayListUnmanaged(u8){}; - try buffer.ensureTotalCapacity(allocator, 128); + const tree = data.tree; + var it = data.fn_proto.iterate(data.tree); - var buf_stream = buffer.writer(allocator); + if (data.include_fn_keyword) { + try writer.writeAll("fn "); + } + + if (data.include_name) { + if (data.fn_proto.name_token) |name_token| { + try writer.writeAll(tree.tokenSlice(name_token)); + } + } - try buf_stream.writeAll(name); - try buf_stream.writeByte('('); + try writer.writeByte('('); const token_tags = tree.tokens.items(.tag); + if (data.skip_first_param) { + _ = ast.nextFnParam(&it); + } + var i: usize = 0; - while (ast.nextFnParam(iterator)) |param| : (i += 1) { - if (i != 0) - try buf_stream.writeAll(", ${") - else - try buf_stream.writeAll("${"); + while (ast.nextFnParam(&it)) |param| : (i += 1) { + if (i != 0) { + try writer.writeAll(", "); + } + + if (data.snippet_placeholders) { + try writer.print("${{{d}:", .{i + 1}); + } - try buf_stream.print("{d}:", .{i + 1}); + // Note that parameter doc comments are being skipped - if (param.comptime_noalias) |token_index| { - if (token_tags[token_index] == .keyword_comptime) - try buf_stream.writeAll("comptime ") - else - try buf_stream.writeAll("noalias "); + if (data.include_parameter_modifiers) { + if (param.comptime_noalias) |token_index| { + switch (token_tags[token_index]) { + .keyword_comptime => try writer.writeAll("comptime "), + .keyword_noalias => try writer.writeAll("noalias "), + else => unreachable, + } + } } - if (param.name_token) |name_token| { - try buf_stream.print("{}", .{fmtSnippetPlaceholder(tree.tokenSlice(name_token))}); - try buf_stream.writeAll(": "); + if (data.include_parameter_names) { + if (param.name_token) |name_token| { + const name = tree.tokenSlice(name_token); + if (data.snippet_placeholders) { + try writer.print("{}", .{fmtSnippetPlaceholder(name)}); + } else { + try writer.writeAll(name); + } + } } - if (param.anytype_ellipsis3) |token_index| { - if (token_tags[token_index] == .keyword_anytype) - try buf_stream.writeAll("anytype") - else - try buf_stream.writeAll("..."); - } else if (param.type_expr != 0) { - var curr_token = tree.firstToken(param.type_expr); - const end_token = ast.lastToken(tree, param.type_expr); - while (curr_token <= end_token) : (curr_token += 1) { - const tag = token_tags[curr_token]; - const is_comma = tag == .comma; - - if (curr_token == end_token and is_comma) continue; - try buf_stream.print("{}", .{fmtSnippetPlaceholder(tree.tokenSlice(curr_token))}); - if (is_comma or tag == .keyword_const) try buf_stream.writeByte(' '); + if (data.include_parameter_types) { + try writer.writeAll(": "); + + if (param.type_expr != 0) { + if (data.snippet_placeholders) { + var curr_token = tree.firstToken(param.type_expr); + const end_token = ast.lastToken(tree.*, param.type_expr); + while (curr_token <= end_token) : (curr_token += 1) { + const tag = token_tags[curr_token]; + const is_comma = tag == .comma; + + if (curr_token == end_token and is_comma) continue; + try writer.print("{}", .{fmtSnippetPlaceholder(tree.tokenSlice(curr_token))}); + if (is_comma or tag == .keyword_const) try writer.writeByte(' '); + } + } else { + try writer.writeAll(offsets.nodeToSlice(tree.*, param.type_expr)); + } + } else if (param.anytype_ellipsis3) |token_index| { + switch (token_tags[token_index]) { + .keyword_anytype => try writer.writeAll("anytype"), + .ellipsis3 => try writer.writeAll("..."), + else => unreachable, + } } - } // else Incomplete and that's ok :) + } - try buf_stream.writeByte('}'); + if (data.snippet_placeholders) { + try writer.writeByte('}'); + } } - try buf_stream.writeByte(')'); + try writer.writeByte(')'); + + // ignoring align_expr + // ignoring addrspace_expr + // ignoring section_expr + // ignoring callconv_expr + + if (data.include_return_type) { + if (data.fn_proto.ast.return_type != 0) { + try writer.writeByte(' '); + if (ast.hasInferredError(tree.*, data.fn_proto)) { + try writer.writeByte('!'); + } + try writer.writeAll(offsets.nodeToSlice(tree.*, data.fn_proto.ast.return_type)); + } + } +} - return buffer.toOwnedSlice(allocator); +pub fn fmtFunction(options: FormatFunctionOptions) std.fmt.Formatter(formatFunction) { + return .{ .data = options }; } pub fn isInstanceCall( diff --git a/src/features/completions.zig b/src/features/completions.zig index 03f48dcac..aebf6b3e8 100644 --- a/src/features/completions.zig +++ b/src/features/completions.zig @@ -247,50 +247,101 @@ fn nodeToCompletion( => { var buf: [1]Ast.Node.Index = undefined; const func = tree.fullFnProto(&buf, node).?; - if (func.name_token) |name_token| { - const func_name = orig_name orelse tree.tokenSlice(name_token); - const use_snippets = server.config.enable_snippets and server.client_capabilities.supports_snippets; - - const insert_text = blk: { - if (!use_snippets) break :blk func_name; - - const skip_self_param = !(parent_is_type_val orelse true) and try analyser.hasSelfParam(handle, func); - - const use_placeholders = server.config.enable_argument_placeholders; - if (use_placeholders) { - var it = func.iterate(&tree); - if (skip_self_param) _ = ast.nextFnParam(&it); - break :blk try Analyser.getFunctionSnippet(arena, func_name, &it); - } + const name_token = func.name_token orelse return; + + const func_name = orig_name orelse tree.tokenSlice(name_token); + const use_snippets = server.config.enable_snippets and server.client_capabilities.supports_snippets; + const use_placeholders = server.config.enable_argument_placeholders; + const use_label_details = server.client_capabilities.label_details_support; + + const skip_self_param = !(parent_is_type_val orelse true) and try analyser.hasSelfParam(handle, func); + + const insert_text = blk: { + if (use_snippets and use_placeholders) { + break :blk try std.fmt.allocPrint(arena, "{}", .{Analyser.fmtFunction(.{ + .fn_proto = func, + .tree = &tree, + + .include_fn_keyword = false, + .include_name = true, + .skip_first_param = skip_self_param, + .include_parameter_modifiers = false, + .include_parameter_names = true, + .include_parameter_types = true, + .include_return_type = false, + .snippet_placeholders = true, + })}); + } - switch (func.ast.params.len) { - // No arguments, leave cursor at the end - 0 => break :blk try std.fmt.allocPrint(arena, "{s}()", .{func_name}), - 1 => { - if (skip_self_param) { - // The one argument is a self parameter, leave cursor at the end - break :blk try std.fmt.allocPrint(arena, "{s}()", .{func_name}); - } + switch (func.ast.params.len) { + // No arguments, leave cursor at the end + 0 => break :blk try std.fmt.allocPrint(arena, "{s}()", .{func_name}), + 1 => { + if (skip_self_param) { + // The one argument is a self parameter, leave cursor at the end + break :blk try std.fmt.allocPrint(arena, "{s}()", .{func_name}); + } - // Non-self parameter, leave the cursor in the parentheses - break :blk try std.fmt.allocPrint(arena, "{s}(${{1:}})", .{func_name}); - }, + // Non-self parameter, leave the cursor in the parentheses + if (!use_snippets) break :blk func_name; + break :blk try std.fmt.allocPrint(arena, "{s}(${{1:}})", .{func_name}); + }, + else => { // Atleast one non-self parameter, leave the cursor in the parentheses - else => break :blk try std.fmt.allocPrint(arena, "{s}(${{1:}})", .{func_name}), - } - }; + if (!use_snippets) break :blk func_name; + break :blk try std.fmt.allocPrint(arena, "{s}(${{1:}})", .{func_name}); + }, + } + }; - const is_type_function = Analyser.isTypeFunction(tree, func); + const kind: types.CompletionItemKind = if (Analyser.isTypeFunction(tree, func)) + .Struct + else if (skip_self_param) + .Method + else + .Function; + + const label_details: ?[]const u8 = if (use_label_details) + try std.fmt.allocPrint(arena, "{}", .{Analyser.fmtFunction(.{ + .fn_proto = func, + .tree = &tree, + + .include_fn_keyword = false, + .include_name = false, + .include_parameter_modifiers = true, + .include_parameter_names = true, + .include_parameter_types = true, + .include_return_type = false, + .snippet_placeholders = false, + })}) + else + null; + + const details = try std.fmt.allocPrint(arena, "{}", .{Analyser.fmtFunction(.{ + .fn_proto = func, + .tree = &tree, + + .include_fn_keyword = true, + .include_name = false, + .include_parameter_modifiers = true, + .include_parameter_names = true, + .include_parameter_types = true, + .include_return_type = true, + .snippet_placeholders = false, + })}); - try list.append(arena, .{ - .label = func_name, - .kind = if (is_type_function) .Struct else .Function, - .documentation = doc, - .detail = Analyser.getFunctionSignature(tree, func), - .insertText = insert_text, - .insertTextFormat = if (use_snippets) .Snippet else .PlainText, - }); - } + try list.append(arena, .{ + .label = func_name, + .labelDetails = if (use_label_details) .{ + .detail = label_details, + .description = null, + } else null, + .kind = kind, + .documentation = doc, + .detail = details, + .insertText = insert_text, + .insertTextFormat = if (use_snippets) .Snippet else .PlainText, + }); }, .global_var_decl, .local_var_decl, diff --git a/tests/lsp_features/completion.zig b/tests/lsp_features/completion.zig index 535815bca..b4e1cbd63 100644 --- a/tests/lsp_features/completion.zig +++ b/tests/lsp_features/completion.zig @@ -13,6 +13,7 @@ const allocator: std.mem.Allocator = std.testing.allocator; const Completion = struct { label: []const u8, + labelDetails: ?types.CompletionItemLabelDetails = null, kind: types.CompletionItemKind, detail: ?[]const u8 = null, documentation: ?[]const u8 = null, @@ -180,11 +181,38 @@ test "completion - function" { \\ \\} , &.{ - // TODO detail should be 'fn(alpha: u32, beta: []const u8) void' or 'foo: fn(alpha: u32, beta: []const u8) void' - .{ .label = "foo", .kind = .Function, .detail = "fn foo(alpha: u32, beta: []const u8) void" }, + .{ + .label = "foo", + .labelDetails = .{ + .detail = "(alpha: u32, beta: []const u8)", + .description = null, + }, + .kind = .Function, + .detail = "fn (alpha: u32, beta: []const u8) void", + }, .{ .label = "alpha", .kind = .Constant, .detail = "u32" }, .{ .label = "beta", .kind = .Constant, .detail = "[]const u8" }, }); + try testCompletion( + \\fn foo( + \\ comptime T: type, + \\ value: anytype, + \\) void { + \\ + \\} + , &.{ + .{ + .label = "foo", + .labelDetails = .{ + .detail = "(comptime T: type, value: anytype)", + .description = null, + }, + .kind = .Function, + .detail = "fn (comptime T: type, value: anytype) void", + }, + .{ .label = "T", .kind = .Constant, .detail = "type" }, + .{ .label = "value", .kind = .Constant }, + }); try testCompletion( \\const S = struct { alpha: u32 }; @@ -236,7 +264,7 @@ test "completion - generic function" { \\const foo = s2.; , &.{ .{ .label = "alpha", .kind = .Field, .detail = "u32" }, - .{ .label = "foo", .kind = .Function, .detail = "fn foo(self: S, comptime T: type) T" }, + .{ .label = "foo", .kind = .Method, .detail = "fn (self: S, comptime T: type) T" }, }); try testCompletion( \\const S = struct { @@ -248,7 +276,7 @@ test "completion - generic function" { \\const foo = s2.; , &.{ .{ .label = "alpha", .kind = .Field, .detail = "u32" }, - .{ .label = "foo", .kind = .Function, .detail = "fn foo(self: S, any: anytype, comptime T: type) T" }, + .{ .label = "foo", .kind = .Method, .detail = "fn (self: S, any: anytype, comptime T: type) T" }, }); } @@ -851,7 +879,7 @@ test "completion - struct" { \\ . \\} , &.{ - .{ .label = "add", .kind = .Function, .detail = "fn add(foo: Foo) Foo" }, + .{ .label = "add", .kind = .Method, .detail = "fn (foo: Foo) Foo" }, }); try testCompletion( @@ -866,7 +894,7 @@ test "completion - struct" { , &.{ .{ .label = "alpha", .kind = .Field, .detail = "u32" }, .{ .label = "beta", .kind = .Field, .detail = "[]const u8" }, - .{ .label = "foo", .kind = .Function, .detail = "fn foo(self: S) void" }, + .{ .label = "foo", .kind = .Method, .detail = "fn (self: S) void" }, }); try testCompletion( @@ -891,8 +919,10 @@ test "completion - struct" { \\const foo = Foo{}; \\const baz = foo.; , &.{ - .{ .label = "foo", .kind = .Function, .detail = "fn fooImpl(_: Foo) void" }, - .{ .label = "bar", .kind = .Function, .detail = "fn barImpl(_: *const Foo) void" }, + // TODO kind should be .Method + .{ .label = "foo", .kind = .Function, .detail = "fn (_: Foo) void" }, + // TODO kind should be .Method + .{ .label = "bar", .kind = .Function, .detail = "fn (_: *const Foo) void" }, }); } @@ -953,7 +983,7 @@ test "completion - enum" { \\}; \\const foo = E. , &.{ - .{ .label = "inner", .kind = .Function, .detail = "fn inner(_: E) void" }, + .{ .label = "inner", .kind = .Function, .detail = "fn (_: E) void" }, }); try testCompletion( \\const E = enum { @@ -963,7 +993,7 @@ test "completion - enum" { \\const e: E = undefined; \\const foo = e. , &.{ - .{ .label = "inner", .kind = .Function, .detail = "fn inner(_: E) void" }, + .{ .label = "inner", .kind = .Method, .detail = "fn (_: E) void" }, }); // Because current logic is to list all enums if all else fails, // the following tests include an extra enum to ensure that we're not just 'getting lucky' @@ -1791,8 +1821,8 @@ test "completion - declarations" { \\const foo: S = undefined; \\const bar = foo. , &.{ - .{ .label = "public", .kind = .Function, .detail = "fn public() S" }, - .{ .label = "private", .kind = .Function, .detail = "fn private() !void" }, + .{ .label = "public", .kind = .Function, .detail = "fn () S" }, + .{ .label = "private", .kind = .Function, .detail = "fn () !void" }, }); try testCompletion( @@ -1802,8 +1832,8 @@ test "completion - declarations" { \\}; \\const foo = S. , &.{ - .{ .label = "public", .kind = .Function, .detail = "fn public() S" }, - .{ .label = "private", .kind = .Function, .detail = "fn private() !void" }, + .{ .label = "public", .kind = .Function, .detail = "fn () S" }, + .{ .label = "private", .kind = .Function, .detail = "fn () !void" }, }); } @@ -1819,8 +1849,8 @@ test "completion - usingnamespace" { \\}; \\const foo = S2. , &.{ - .{ .label = "public", .kind = .Function, .detail = "fn public() S1" }, - .{ .label = "private", .kind = .Function, .detail = "fn private() !void" }, + .{ .label = "public", .kind = .Function, .detail = "fn () S1" }, + .{ .label = "private", .kind = .Function, .detail = "fn () !void" }, }); try testCompletion( \\const S1 = struct { @@ -1830,7 +1860,7 @@ test "completion - usingnamespace" { \\}; \\const foo = S1. , &.{ - .{ .label = "inner", .kind = .Function, .detail = "fn inner() void" }, + .{ .label = "inner", .kind = .Function, .detail = "fn () void" }, }); try testCompletion( \\fn Bar(comptime Self: type) type { @@ -1845,8 +1875,9 @@ test "completion - usingnamespace" { \\const foo: Foo = undefined; \\const bar = foo. , &.{ - .{ .label = "inner", .kind = .Function, .detail = "fn inner(self: Self) void" }, - .{ .label = "deinit", .kind = .Function, .detail = "fn deinit(self: Foo) void" }, + // TODO kind should be .Method + .{ .label = "inner", .kind = .Function, .detail = "fn (self: Self) void" }, + .{ .label = "deinit", .kind = .Method, .detail = "fn (self: Foo) void" }, }); try testCompletion( \\const Alpha = struct { @@ -1861,8 +1892,8 @@ test "completion - usingnamespace" { \\const gamma: Gamma = undefined; \\const g = gamma. , &.{ - .{ .label = "alpha", .kind = .Function, .detail = "fn alpha() void" }, - .{ .label = "beta", .kind = .Function, .detail = "fn beta() void" }, + .{ .label = "alpha", .kind = .Function, .detail = "fn () void" }, + .{ .label = "beta", .kind = .Function, .detail = "fn () void" }, }); try testCompletion( \\pub const chip_mod = struct { @@ -1888,8 +1919,8 @@ test "completion - usingnamespace" { , &.{ .{ .label = "inner", .kind = .Constant, .detail = "struct" }, .{ .label = "peripherals", .kind = .Constant, .detail = "struct" }, - .{ .label = "chip1fn1", .kind = .Function, .detail = "fn chip1fn1() void" }, - .{ .label = "chip1fn2", .kind = .Function, .detail = "fn chip1fn2(_: u32) void" }, + .{ .label = "chip1fn1", .kind = .Function, .detail = "fn () void" }, + .{ .label = "chip1fn2", .kind = .Function, .detail = "fn (_: u32) void" }, }); } @@ -1911,10 +1942,10 @@ test "completion - anytype resolution based on callsite-references" { \\ writer. \\} , &.{ - .{ .label = "write1", .kind = .Function, .detail = "fn write1() void" }, - .{ .label = "write2", .kind = .Function, .detail = "fn write2() void" }, - .{ .label = "writeAll1", .kind = .Function, .detail = "fn writeAll1() void" }, - .{ .label = "writeAll2", .kind = .Function, .detail = "fn writeAll2() void" }, + .{ .label = "write1", .kind = .Function, .detail = "fn () void" }, + .{ .label = "write2", .kind = .Function, .detail = "fn () void" }, + .{ .label = "writeAll1", .kind = .Function, .detail = "fn () void" }, + .{ .label = "writeAll2", .kind = .Function, .detail = "fn () void" }, }); try testCompletion( \\const Writer1 = struct { @@ -1933,8 +1964,8 @@ test "completion - anytype resolution based on callsite-references" { \\ writer. \\} , &.{ - .{ .label = "write1", .kind = .Function, .detail = "fn write1() void" }, - .{ .label = "writeAll1", .kind = .Function, .detail = "fn writeAll1() void" }, + .{ .label = "write1", .kind = .Function, .detail = "fn () void" }, + .{ .label = "writeAll1", .kind = .Function, .detail = "fn () void" }, }); } @@ -2022,8 +2053,8 @@ test "completion - either" { \\const foo: if (undefined) Alpha else Beta = undefined; \\const bar = foo. , &.{ - .{ .label = "alpha", .kind = .Function, .detail = "fn alpha() void" }, - .{ .label = "beta", .kind = .Function, .detail = "fn beta() void" }, + .{ .label = "alpha", .kind = .Function, .detail = "fn () void" }, + .{ .label = "beta", .kind = .Function, .detail = "fn () void" }, }); try testCompletion( \\const Alpha = struct { @@ -2037,8 +2068,8 @@ test "completion - either" { \\const gamma = if (undefined) alpha else beta; \\const foo = gamma. , &.{ - .{ .label = "alpha", .kind = .Function, .detail = "fn alpha() void" }, - .{ .label = "beta", .kind = .Function, .detail = "fn beta() void" }, + .{ .label = "alpha", .kind = .Function, .detail = "fn () void" }, + .{ .label = "beta", .kind = .Function, .detail = "fn () void" }, }); } @@ -2125,7 +2156,18 @@ test "completion - snippet - function with `self` parameter" { \\const s = S{}; \\s. , &.{ - .{ .label = "f", .kind = .Function, .detail = "fn f(self: S) void", .insert_text = "f()" }, + .{ .label = "f", .kind = .Method, .detail = "fn (self: S) void", .insert_text = "f()" }, + }); + try testCompletionWithOptions( + \\const S = struct { + \\ fn f(self: S) void {} + \\}; + \\const s = S{}; + \\s. + , &.{ + .{ .label = "f", .kind = .Method, .detail = "fn (self: S) void", .insert_text = "f()" }, + }, .{ + .enable_argument_placeholders = false, }); try testCompletion( \\const S = struct { @@ -2133,7 +2175,7 @@ test "completion - snippet - function with `self` parameter" { \\}; \\S. , &.{ - .{ .label = "f", .kind = .Function, .detail = "fn f(self: S) void", .insert_text = "f(${1:self: S})" }, + .{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f(${1:self: S})" }, }); try testCompletionWithOptions( \\const S = struct { @@ -2141,20 +2183,41 @@ test "completion - snippet - function with `self` parameter" { \\}; \\S. , &.{ - .{ .label = "f", .kind = .Function, .detail = "fn f(self: S) void", .insert_text = "f(${1:})" }, + .{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f(${1:})" }, }, .{ .enable_argument_placeholders = false, }); } test "completion - snippets disabled" { + try testCompletionWithOptions( + \\const S = struct { + \\ fn f(self: S) void {} + \\}; + \\const s = S{}; + \\s. + , &.{ + .{ .label = "f", .kind = .Method, .detail = "fn (self: S) void", .insert_text = "f()" }, + }, .{ + .enable_snippets = false, + }); try testCompletionWithOptions( \\const S = struct { \\ fn f(self: S) void {} \\}; \\S. , &.{ - .{ .label = "f", .kind = .Function, .detail = "fn f(self: S) void", .insert_text = "f" }, + .{ .label = "f", .kind = .Function, .detail = "fn (self: S) void", .insert_text = "f" }, + }, .{ + .enable_snippets = false, + }); + try testCompletionWithOptions( + \\const S = struct { + \\ fn f() void {} + \\}; + \\S. + , &.{ + .{ .label = "f", .kind = .Function, .detail = "fn () void", .insert_text = "f()" }, }, .{ .enable_snippets = false, }); @@ -2177,6 +2240,7 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C ctx.server.client_capabilities.completion_doc_supports_md = true; ctx.server.client_capabilities.supports_snippets = options.enable_snippets; + ctx.server.client_capabilities.label_details_support = true; ctx.server.config.enable_argument_placeholders = options.enable_argument_placeholders; ctx.server.config.enable_snippets = options.enable_snippets; @@ -2234,7 +2298,7 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C }; if (actual_completion.kind == null or expected_completion.kind != actual_completion.kind.?) { - try error_builder.msgAtIndex("label '{s}' should be of kind '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ + try error_builder.msgAtIndex("completion item '{s}' should be of kind '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ label, @tagName(expected_completion.kind), if (actual_completion.kind) |kind| @tagName(kind) else null, @@ -2251,7 +2315,7 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C if (actual_doc != null and std.mem.eql(u8, expected_doc, actual_doc.?)) break :doc_blk; - try error_builder.msgAtIndex("label '{s}' should have doc '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ + try error_builder.msgAtIndex("completion item '{s}' should have doc '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ label, expected_doc, actual_doc, @@ -2266,7 +2330,7 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C ); 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("label '{s}' should have insert text '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ + try error_builder.msgAtIndex("completion item '{s}' should have insert text '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ label, expected_insert, actual_insert, @@ -2274,15 +2338,37 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C return error.InvalidCompletionInsertText; } - if (expected_completion.detail == null) continue; - if (actual_completion.detail != null and std.mem.eql(u8, expected_completion.detail.?, actual_completion.detail.?)) continue; + if (expected_completion.detail) |expected_detail| blk: { + if (actual_completion.detail != null and std.mem.eql(u8, expected_detail, actual_completion.detail.?)) break :blk; + + try error_builder.msgAtIndex("completion item '{s}' should have detail '{s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ + label, + expected_detail, + actual_completion.detail, + }); + return error.InvalidCompletionDetail; + } + + if (expected_completion.labelDetails) |expected_label_details| blk: { + const actual_label_details = actual_completion.labelDetails orelse { + try error_builder.msgAtIndex("expected label details on completion item '{s}'!", test_uri, cursor_idx, .err, .{label}); + return error.InvalidCompletionLabelDetails; + }; + const detail_ok = (expected_label_details.detail == null and actual_label_details.detail == null) or + (expected_label_details.detail != null and actual_label_details.detail != null and std.mem.eql(u8, expected_label_details.detail.?, actual_label_details.detail.?)); - try error_builder.msgAtIndex("label '{s}' should have detail '{?s}' but was '{?s}'!", test_uri, cursor_idx, .err, .{ - label, - expected_completion.detail, - actual_completion.detail, - }); - return error.InvalidCompletionDetail; + const description_ok = (expected_label_details.description == null and actual_label_details.description == null) or + (expected_label_details.description != null and actual_label_details.description != null and std.mem.eql(u8, expected_label_details.description.?, actual_label_details.description.?)); + + if (detail_ok and description_ok) break :blk; + + try error_builder.msgAtIndex("completion item '{s}' should have label detail '{}' but was '{}'!", test_uri, cursor_idx, .err, .{ + label, + expected_label_details, + actual_label_details, + }); + return error.InvalidCompletionLabelDetails; + } } if (missing.count() != 0 or unexpected.count() != 0) { @@ -2294,7 +2380,7 @@ fn testCompletionWithOptions(source: []const u8, expected_completions: []const C try printLabels(out, missing, "missing"); try printLabels(out, unexpected, "unexpected"); try error_builder.msgAtIndex("invalid completions\n{s}", test_uri, cursor_idx, .err, .{buffer.items}); - return error.InvalidCompletions; + return error.MissingOrUnexpectedCompletions; } }