diff --git a/src/analysis.zig b/src/analysis.zig index d2d0b3a20..89d7c6a68 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -386,6 +386,32 @@ pub fn fmtEscapedSnippet(raw_text: []const u8) std.fmt.Alt([]const u8, SnippetEs return .{ .data = raw_text }; } +pub fn renderBuiltinFunctionSignature( + arena: std.mem.Allocator, + name: []const u8, + builtin_data: version_data.Builtin, + multi_line: bool, +) error{OutOfMemory}![]u8 { + var signature: std.ArrayList(u8) = .empty; + try signature.appendSlice(arena, name); + try signature.append(arena, '('); + if (multi_line) try signature.append(arena, '\n'); + for (builtin_data.parameters, 0..) |parameter, i| { + if (multi_line) { + try signature.appendSlice(arena, " "); + } else if (i != 0) { + try signature.appendSlice(arena, ", "); + } + try signature.appendSlice(arena, parameter.signature); + if (multi_line) { + try signature.appendSlice(arena, ",\n"); + } + } + try signature.appendSlice(arena, ") "); + try signature.appendSlice(arena, builtin_data.return_type); + return signature.items; +} + pub fn isInstanceCall( analyser: *Analyser, call_handle: *DocumentStore.Handle, @@ -6338,7 +6364,8 @@ pub fn resolveExpressionTypeFromAncestors( const index = std.mem.findScalar(Ast.Node.Index, params, node) orelse return null; if (index >= data.parameters.len) return null; const parameter = data.parameters[index]; - const type_str = parameter.type orelse return null; + const colon_index = std.mem.findScalar(u8, parameter.signature, ':') orelse return null; + const type_str = parameter.signature[colon_index + 2 ..]; return analyser.instanceStdBuiltinType(type_str); } }, diff --git a/src/features/completions.zig b/src/features/completions.zig index 493514d66..76393a471 100644 --- a/src/features/completions.zig +++ b/src/features/completions.zig @@ -540,7 +540,16 @@ fn completeBuiltin(builder: *Builder) error{OutOfMemory}!void { // The generated snippet is not being escaped here because we can // assume that the builtin name has no characters that need to be escaped. if (builtin.parameters.len == 0) break :snippet try std.fmt.allocPrint(builder.arena, "{s}()", .{name}); - if (use_placeholders) break :snippet builtin.snippet; + if (use_placeholders) { + var snippet: std.ArrayList(u8) = .empty; + try snippet.print(builder.arena, "{s}(", .{name}); + for (builtin.parameters, 1..) |param, i| { + if (i != 1) try snippet.appendSlice(builder.arena, ", "); + try snippet.print(builder.arena, "${{{d}:{f}}}", .{ i, Analyser.fmtEscapedSnippet(param.signature) }); + } + try snippet.append(builder.arena, ')'); + break :snippet snippet.items; + } break :snippet try std.fmt.allocPrint(builder.arena, "{s}(${{1:}})", .{name}); }, }; @@ -548,12 +557,18 @@ fn completeBuiltin(builder: *Builder) error{OutOfMemory}!void { .only_name => .PlainText, .snippet => .Snippet, }; + const detail = try Analyser.renderBuiltinFunctionSignature( + builder.arena, + name, + builtin, + builtin.parameters.len > 3, + ); builder.completions.appendAssumeCapacity(.{ .label = name, .kind = .Function, .filterText = name[1..], - .detail = builtin.signature, + .detail = detail, .insertTextFormat = insert_text_format, .textEdit = if (builder.server.client_capabilities.supports_completion_insert_replace_support) .{ .insert_replace_edit = .{ .newText = new_text[1..], .insert = insert_range, .replace = replace_range } } @@ -561,7 +576,7 @@ fn completeBuiltin(builder: *Builder) error{OutOfMemory}!void { .{ .text_edit = .{ .newText = new_text[1..], .range = insert_range } }, .documentation = .{ .markup_content = .{ - .kind = .markdown, + .kind = if (builder.server.client_capabilities.completion_doc_supports_md) .markdown else .plaintext, .value = builtin.documentation, }, }, @@ -1618,7 +1633,8 @@ fn resolveBuiltinFnArg( const builtin = version_data.builtins.get(name) orelse return null; if (arg_index >= builtin.parameters.len) return null; const param = builtin.parameters[arg_index]; - const builtin_name = param.type orelse return null; + const colon_index = std.mem.findScalar(u8, param.signature, ':') orelse return null; + const builtin_name = param.signature[colon_index + 2 ..]; return analyser.instanceStdBuiltinType(builtin_name); } diff --git a/src/features/hover.zig b/src/features/hover.zig index fefefd1bc..96e947f37 100644 --- a/src/features/hover.zig +++ b/src/features/hover.zig @@ -250,13 +250,19 @@ fn hoverDefinitionBuiltin( } const builtin = data.builtins.get(name) orelse return null; + const signature = try Analyser.renderBuiltinFunctionSignature( + arena, + name, + builtin, + builtin.parameters.len > 3, + ); switch (markup_kind) { .plaintext, .unknown_value => { try contents.print(arena, \\{s} \\{s} - , .{ builtin.signature, builtin.documentation }); + , .{ signature, builtin.documentation }); }, .markdown => { try contents.print(arena, @@ -264,7 +270,7 @@ fn hoverDefinitionBuiltin( \\{s} \\``` \\{s} - , .{ builtin.signature, builtin.documentation }); + , .{ signature, builtin.documentation }); }, } diff --git a/src/features/inlay_hints.zig b/src/features/inlay_hints.zig index 73a9aefcd..ae741398e 100644 --- a/src/features/inlay_hints.zig +++ b/src/features/inlay_hints.zig @@ -162,20 +162,24 @@ const Builder = struct { node_tag: Ast.Node.Tag, token_index: Ast.TokenIndex, label: []const u8, - tooltip: []const u8, + tooltip_text: []const u8, tooltip_noalias: bool, tooltip_comptime: bool, ) !void { // adding tooltip_noalias & tooltip_comptime to InlayHint should be enough - const tooltip_text = blk: { - if (tooltip.len == 0) break :blk ""; - const prefix = if (tooltip_noalias) if (tooltip_comptime) "noalias comptime " else "noalias " else if (tooltip_comptime) "comptime " else ""; + const tooltip: ?types.MarkupContent = tooltip: { + if (tooltip_text.len == 0) break :tooltip null; + const prefix = if (tooltip_noalias) "noalias " else if (tooltip_comptime) "comptime " else ""; - if (self.hover_kind == .markdown) { - break :blk try std.fmt.allocPrint(self.arena, "```zig\n{s}{s}\n```", .{ prefix, tooltip }); - } + const text = switch (self.hover_kind) { + .markdown => try std.fmt.allocPrint(self.arena, "```zig\n{s}{s}\n```", .{ prefix, tooltip_text }), + .plaintext, .unknown_value => try std.fmt.allocPrint(self.arena, "{s}{s}", .{ prefix, tooltip_text }), + }; - break :blk try std.fmt.allocPrint(self.arena, "{s}{s}", .{ prefix, tooltip }); + break :tooltip .{ + .kind = self.hover_kind, + .value = text, + }; }; try self.hints.append(self.arena, .{ @@ -185,10 +189,7 @@ const Builder = struct { self.handle.tree.tokenStart(token_index), .label = try std.fmt.allocPrint(self.arena, "{s}:", .{label}), .kind = .Parameter, - .tooltip = .{ - .kind = self.hover_kind, - .value = tooltip_text, - }, + .tooltip = tooltip, }); } @@ -292,36 +293,31 @@ fn writeBuiltinHint(builder: *Builder, parameters: []const Ast.Node.Index, param const len = @min(params.len, parameters.len); for (params[0..len], parameters[0..len]) |param, parameter| { - const signature = param.signature; - if (signature.len == 0) continue; - - const colonIndex = std.mem.findScalar(u8, signature, ':'); - const type_expr = param.type orelse ""; - - // TODO: parse noalias/comptime/label in config_gen.zig - var maybe_label: ?[]const u8 = null; - var no_alias = false; - var comp_time = false; - - var it = std.mem.splitScalar(u8, signature[0 .. colonIndex orelse signature.len], ' '); - while (it.next()) |item| { - if (item.len == 0) continue; - 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"); + var signature = param.signature; + if (std.mem.eql(u8, signature, "...")) return; + + var is_comptime = false; + var is_noalias = false; + if (std.mem.cutPrefix(u8, signature, "comptime")) |rest| { + signature = rest; + is_comptime = true; + } else if (std.mem.cutPrefix(u8, signature, "noalias")) |rest| { + signature = rest; + is_noalias = true; } + signature = std.mem.trimStart(u8, signature, &std.ascii.whitespace); - const label = maybe_label orelse return; - if (label.len == 0 or std.mem.eql(u8, label, "...")) return; + const colon_index = std.mem.findScalar(u8, signature, ':'); + const label = signature[0 .. colon_index orelse signature.len]; + const tooltip = if (colon_index) |i| signature[i + 1 ..] else ""; try builder.appendParameterHint( tree.nodeTag(parameter), tree.firstToken(parameter), label, - std.mem.trim(u8, type_expr, " \t\n"), - no_alias, - comp_time, + tooltip, + is_noalias, + is_comptime, ); } } diff --git a/src/features/signature_help.zig b/src/features/signature_help.zig index 6eab53b23..5507de828 100644 --- a/src/features/signature_help.zig +++ b/src/features/signature_help.zig @@ -193,21 +193,30 @@ pub fn getSignatureInfo( const expr_last_token = curr_token - 1; if (tree.tokenTag(expr_last_token) == .builtin) { - // Builtin token, find the builtin and construct signature information. - const builtin = data.builtins.get(tree.tokenSlice(expr_last_token)) orelse return null; - const param_infos = try arena.alloc( - types.SignatureHelp.Signature.Parameter, - builtin.parameters.len, - ); + const builtin_name = tree.tokenSlice(expr_last_token); + const builtin = data.builtins.get(builtin_name) orelse return null; + + const param_infos = try arena.alloc(types.SignatureHelp.Signature.Parameter, builtin.parameters.len); for (param_infos, builtin.parameters) |*info, parameter| { info.* = .{ .label = .{ .string = parameter.signature }, - .documentation = null, + .documentation = if (parameter.documentation) |doc| + .{ .markup_content = .{ .kind = markup_kind, .value = doc } } + else + null, }; } return types.SignatureHelp.Signature{ - .label = builtin.signature, - .documentation = .{ .string = builtin.documentation }, + .label = try Analyser.renderBuiltinFunctionSignature( + arena, + builtin_name, + builtin, + false, + ), + .documentation = .{ .markup_content = .{ + .kind = markup_kind, + .value = builtin.documentation, + } }, .parameters = param_infos, .activeParameter = paren_commas, }; diff --git a/src/tools/config_gen.zig b/src/tools/config_gen.zig index ed5fc89c9..6873710ce 100644 --- a/src/tools/config_gen.zig +++ b/src/tools/config_gen.zig @@ -442,7 +442,7 @@ const Tokenizer = struct { const Builtin = struct { name: []const u8, - signature: [:0]const u8, + signature: []const u8, documentation: std.ArrayList(u8), }; @@ -469,7 +469,6 @@ fn collectBuiltinData(allocator: std.mem.Allocator, version: []const u8, langref var builtins: std.ArrayList(Builtin) = .empty; errdefer { for (builtins.items) |*builtin| { - allocator.free(builtin.signature); builtin.documentation.deinit(allocator); } builtins.deinit(allocator); @@ -551,7 +550,7 @@ fn collectBuiltinData(allocator: std.mem.Allocator, version: []const u8, langref switch (state) { .builtin_begin => { - builtins.items[builtins.items.len - 1].signature = try allocator.dupeZ(u8, content_name); + builtins.items[builtins.items.len - 1].signature = content_name; state = .builtin_content; }, .builtin_content => { @@ -795,8 +794,13 @@ fn writeMarkdownFromHtmlInternal(html: []const u8, single_line: bool, depth: u32 } const Parameter = struct { + documentation: ?[]const u8, signature: []const u8, - type: ?[]const u8, + + fn deinit(param: *Parameter, allocator: std.mem.Allocator) void { + if (param.documentation) |doc| allocator.free(doc); + param.* = undefined; + } }; /// takes in a signature (without name or leading parenthesis) like this: @@ -805,11 +809,15 @@ const Parameter = struct { /// `comptime DestType: type`, `integer: anytype`, `DestType` fn extractParametersAndReturnTypeFromSignature(allocator: std.mem.Allocator, signature: [:0]const u8) error{OutOfMemory}!struct { []Parameter, []const u8 } { var parameters: std.ArrayList(Parameter) = .empty; - defer parameters.deinit(allocator); + errdefer { + for (parameters.items) |*param| param.deinit(allocator); + defer parameters.deinit(allocator); + } var tokenizer: std.zig.Tokenizer = .init(signature); + var documentation: std.ArrayList(u8) = .empty; + defer documentation.deinit(allocator); var argument_start: ?usize = null; - var colon_index: ?usize = null; while (true) { const token = tokenizer.next(); switch (token.tag) { @@ -825,27 +833,22 @@ fn extractParametersAndReturnTypeFromSignature(allocator: std.mem.Allocator, sig } continue; }, - .colon => { - std.debug.assert(argument_start != null); - std.debug.assert(colon_index == null); - colon_index = token.loc.start; - }, .comma, .r_paren => |tag| { if (argument_start) |start| { try parameters.append(allocator, .{ + .documentation = if (documentation.items.len != 0) try documentation.toOwnedSlice(allocator) else null, .signature = std.mem.trim(u8, signature[start..token.loc.start], &std.ascii.whitespace), - .type = if (colon_index) |i| std.mem.trim(u8, signature[1 + i .. token.loc.start], &std.ascii.whitespace) else null, }); } argument_start = null; - colon_index = null; if (tag == .r_paren) break; }, - .doc_comment, .container_doc_comment => {}, + .doc_comment, .container_doc_comment => { + try documentation.print(allocator, "{s}\n", .{signature[token.loc.start + "///".len .. token.loc.end]}); + }, else => { if (argument_start == null) { argument_start = token.loc.start; - std.debug.assert(colon_index == null); } }, } @@ -855,39 +858,6 @@ fn extractParametersAndReturnTypeFromSignature(allocator: std.mem.Allocator, sig return .{ try parameters.toOwnedSlice(allocator), return_type }; } -fn createSignatureSnippet( - allocator: std.mem.Allocator, - builtin_name: []const u8, - parameters: []const Parameter, -) error{OutOfMemory}![]const u8 { - var snippet: std.ArrayList(u8) = .empty; - defer snippet.deinit(allocator); - - try snippet.print(allocator, "{s}(", .{builtin_name}); - for (parameters, 1..) |param, i| { - if (i != 1) try snippet.print(allocator, ", ", .{}); - try snippet.print(allocator, "${{{d}:", .{i}); - for (param.signature) |c| { - switch (c) { - // escaped character - '$', '}', '\\' => try snippet.print(allocator, "\\{c}", .{c}), - else => try snippet.append(allocator, c), - } - } - try snippet.append(allocator, '}'); - } - try snippet.append(allocator, ')'); - - return try snippet.toOwnedSlice(allocator); -} - -fn withoutStdBuiltinPrefix(type_str: []const u8) []const u8 { - if (std.mem.startsWith(u8, type_str, "std.builtin.")) { - return type_str["std.builtin.".len..]; - } - return type_str; -} - /// Generates data files from the Zig language Reference (https://ziglang.org/documentation/master/) /// Output example: https://github.com/zigtools/zls/blob/0.11.0/src/data/master.zig fn generateVersionDataFile( @@ -904,7 +874,6 @@ fn generateVersionDataFile( const builtins = try collectBuiltinData(allocator, version, langref_source); defer { for (builtins) |*builtin| { - allocator.free(builtin.signature); builtin.documentation.deinit(allocator); } allocator.free(builtins); @@ -924,42 +893,41 @@ fn generateVersionDataFile( \\const std = @import("std"); \\ \\pub const Builtin = struct { - \\ signature: []const u8, \\ return_type: []const u8, - \\ snippet: []const u8, \\ documentation: []const u8, \\ parameters: []const Parameter, \\ \\ pub const Parameter = struct { \\ signature: []const u8, - \\ type: ?[]const u8, + \\ documentation: ?[]const u8, \\ }; \\}; \\ - \\pub const builtins: std.StaticStringMap(Builtin) = .initComptime(&.{ + \\pub const builtins: std.StaticStringMap(Builtin) = .initComptime(&[_]struct { []const u8, Builtin }{ \\ ); for (builtins) |builtin| { - const parameters, const return_type = try extractParametersAndReturnTypeFromSignature(allocator, builtin.signature[builtin.name.len + 1 ..]); - defer allocator.free(parameters); - - const snippet = try createSignatureSnippet(allocator, builtin.name, parameters); - defer allocator.free(snippet); + const signature = try std.mem.replaceOwned(u8, allocator, builtin.signature[builtin.name.len + 1 ..], "std.builtin.", ""); + defer allocator.free(signature); + const signature_with_sentinel = try allocator.dupeZ(u8, signature); + defer allocator.free(signature_with_sentinel); + + const parameters, const return_type = try extractParametersAndReturnTypeFromSignature(allocator, signature_with_sentinel); + defer { + for (parameters) |*param| param.deinit(allocator); + defer allocator.free(parameters); + } try writer.print( \\ .{{ \\ "{f}", - \\ Builtin{{ - \\ .signature = "{f}", + \\ .{{ \\ .return_type = "{f}", - \\ .snippet = "{f}", \\ , .{ std.zig.fmtString(builtin.name), - std.zig.fmtString(builtin.signature), - std.zig.fmtString(withoutStdBuiltinPrefix(return_type)), - std.zig.fmtString(snippet), + std.zig.fmtString(return_type), }); const html = builtin.documentation.items["".len..]; @@ -975,7 +943,7 @@ fn generateVersionDataFile( try writer.writeAll( \\ , - \\ .parameters = &[_]Builtin.Parameter{ + \\ .parameters = &.{ ); if (parameters.len != 0) { @@ -988,12 +956,12 @@ fn generateVersionDataFile( , .{ std.zig.fmtString(param.signature), }); - if (param.type) |t| { - try writer.print(" .type = \"{f}\",\n", .{ - std.zig.fmtString(withoutStdBuiltinPrefix(t)), + if (param.documentation) |doc| { + try writer.print(" .documentation = \"{f}\",\n", .{ + std.zig.fmtString(doc), }); } else { - try writer.writeAll(" .type = null,\n"); + try writer.writeAll(" .documentation = null,\n"); } try writer.writeAll(" },\n"); } diff --git a/tests/lsp_features/hover.zig b/tests/lsp_features/hover.zig index 202857b15..f21d928fd 100644 --- a/tests/lsp_features/hover.zig +++ b/tests/lsp_features/hover.zig @@ -221,6 +221,21 @@ test "builtin" { , .{ .markup_kind = .plaintext }); } +test "builtin with multi line parameters" { + try testHoverWithOptions( + \\@Union() + , + \\@Union( + \\ comptime layout: Type.ContainerLayout, + \\ comptime ArgType: ?type, + \\ comptime field_names: []const []const u8, + \\ comptime field_types: *const [field_names.len]type, + \\ comptime field_attrs: *const [field_names.len]Type.UnionField.Attributes, + \\) type + \\Returns a [union](https://ziglang.org/documentation/master/#union) type with the properties specified by the arguments. + , .{ .markup_kind = .plaintext }); +} + test "struct" { try testHover( \\const Struct = packed struct(u32) {};