From 0ecdeeecb1311c9c25e11d02acdb2e4883b89c92 Mon Sep 17 00:00:00 2001 From: Techatrix <19954306+Techatrix@users.noreply.github.com> Date: Sun, 24 Jul 2022 11:38:13 +0000 Subject: [PATCH] Implement textDocument/inlayHint (#559) * Implement textDocument/inlayHint * Add corresponding Config options --- README.md | 1 + src/Config.zig | 9 + src/Server.zig | 74 ++++- src/data/data.zig | 10 + src/inlay_hints.zig | 710 ++++++++++++++++++++++++++++++++++++++++++++ src/requests.zig | 11 + src/setup.zig | 2 + src/types.zig | 37 +++ tests/sessions.zig | 4 +- 9 files changed, 845 insertions(+), 13 deletions(-) create mode 100644 src/data/data.zig create mode 100644 src/inlay_hints.zig diff --git a/README.md b/README.md index ee159a7..14b73d1 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ The following options are currently available. | `build_runner_path` | `?[]const u8` | `null` | Path to the build_runner.zig file provided by zls. `null` is equivalent to `${executable_directory}/build_runner.zig` | | `build_runner_cache_path` | `?[]const u8` | `null` | Path to a directroy that will be used as zig's cache when running `zig run build_runner.zig ...`. `null` is equivalent to `${KnownFloders.Cache}/zls` | | `enable_semantic_tokens` | `bool` | `true` | Enables semantic token support when the client also supports it. | +| `enable_inlay_hints` | `bool` | `false` | Enables inlay hint support when the client also supports it. | | `operator_completions` | `bool` | `true` | Enables `*` and `?` operators in completion lists. | |`include_at_in_builtins`|`bool`|`false`| Whether the @ sign should be part of the completion of builtins. |`max_detail_length`|`usize`|`1024 * 1024`| The detail field of completions is truncated to be no longer than this (in bytes). diff --git a/src/Config.zig b/src/Config.zig index 71ec7a8..5633ce3 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -38,6 +38,15 @@ build_runner_cache_path: ?[]const u8 = null, /// Semantic token support enable_semantic_tokens: bool = true, +/// Inlay hint support +enable_inlay_hints: bool = false, + +/// enable inlay hints for builtin functions +inlay_hints_show_builtin: bool = true, + +/// don't show inlay hints for single argument calls +inlay_hints_exclude_single_argument: bool = true, + /// Whether to enable `*` and `?` operators in completion lists operator_completions: bool = true, diff --git a/src/Server.zig b/src/Server.zig index a386f78..bc76641 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -15,11 +15,13 @@ const rename = @import("rename.zig"); const offsets = @import("offsets.zig"); const setup = @import("setup.zig"); const semantic_tokens = @import("semantic_tokens.zig"); +const inlay_hints = @import("inlay_hints.zig"); const shared = @import("shared.zig"); const Ast = std.zig.Ast; const known_folders = @import("known-folders"); const tracy = @import("tracy.zig"); const uri_utils = @import("uri.zig"); +const data = @import("data/data.zig"); // Server fields @@ -29,15 +31,6 @@ document_store: DocumentStore = undefined, client_capabilities: ClientCapabilities = .{}, offset_encoding: offsets.Encoding = .utf16, -const data = switch (build_options.data_version) { - .master => @import("data/master.zig"), - .@"0.7.0" => @import("data/0.7.0.zig"), - .@"0.7.1" => @import("data/0.7.1.zig"), - .@"0.8.0" => @import("data/0.8.0.zig"), - .@"0.8.1" => @import("data/0.8.1.zig"), - .@"0.9.0" => @import("data/0.9.0.zig"), -}; - const logger = std.log.scoped(.main); // Always set this to debug to make std.log call into our handler, then control the runtime @@ -86,8 +79,8 @@ pub fn log(comptime message_level: std.log.Level, comptime scope: @Type(.EnumLit .message = message, }, }, - }) catch |err| { - std.debug.print("Failed to send show message notification (error: {}).\n", .{err}); + }) catch { + // TODO: Find a way to handle this error properly }; } @@ -96,6 +89,7 @@ pub fn log(comptime message_level: std.log.Level, comptime scope: @Type(.EnumLit const ClientCapabilities = struct { supports_snippets: bool = false, supports_semantic_tokens: bool = false, + supports_inlay_hints: bool = false, hover_supports_md: bool = false, completion_doc_supports_md: bool = false, label_details_support: bool = false, @@ -1753,6 +1747,7 @@ fn initializeHandler(server: *Server, arena: *std.heap.ArenaAllocator, id: types if (req.params.capabilities.textDocument) |textDocument| { server.client_capabilities.supports_semantic_tokens = textDocument.semanticTokens.exists; + server.client_capabilities.supports_inlay_hints = textDocument.inlayHint.exists; if (textDocument.hover) |hover| { for (hover.contentFormat.value) |format| { if (std.mem.eql(u8, "markdown", format)) { @@ -1838,6 +1833,7 @@ fn initializeHandler(server: *Server, arena: *std.heap.ArenaAllocator, id: types }, }, }, + .inlayHintProvider = true, }, }, }, @@ -2348,6 +2344,61 @@ fn documentHighlightHandler(server: *Server, arena: *std.heap.ArenaAllocator, id } } +fn isPositionBefore(lhs: types.Position, rhs: types.Position) bool { + if (lhs.line == rhs.line) { + return lhs.character < rhs.character; + } else { + return lhs.line < rhs.line; + } +} + +fn inlayHintHandler(server: *Server, arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.InlayHint) !void { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + if (server.config.enable_inlay_hints) blk: { + const handle = server.document_store.getHandle(req.params.textDocument.uri) orelse { + logger.warn("Trying to get inlay hint of non existent document {s}", .{req.params.textDocument.uri}); + break :blk; + }; + + const hover_kind: types.MarkupContent.Kind = if (server.client_capabilities.hover_supports_md) .Markdown else .PlainText; + + // TODO cache hints per document + // because the function could be stored in a different document + // we need the regenerate hints when the document itself or its imported documents change + // with caching it would also make sense to generate all hints instead of only the visible ones + const hints = try inlay_hints.writeRangeInlayHint(arena, &server.config, &server.document_store, handle, req.params.range, hover_kind); + defer { + for (hints) |hint| { + server.allocator.free(hint.tooltip.value); + } + server.allocator.free(hints); + } + + // and only convert and return all hints in range for every request + var visible_hints = hints; + + // small_hints should roughly be sorted by position + for (hints) |hint, i| { + if (isPositionBefore(hint.position, req.params.range.start)) continue; + visible_hints = hints[i..]; + break; + } + for (visible_hints) |hint, i| { + if (isPositionBefore(hint.position, req.params.range.end)) continue; + visible_hints = visible_hints[0..i]; + break; + } + + return try send(arena, types.Response{ + .id = id, + .result = .{ .InlayHint = visible_hints }, + }); + } + return try respondGeneric(id, null_result_response); +} + // Needed for the hack seen below. fn extractErr(val: anytype) anyerror { val catch |e| return e; @@ -2434,6 +2485,7 @@ fn processJsonRpc(server: *Server, arena: *std.heap.ArenaAllocator, parser: *std .{ "textDocument/didSave", requests.SaveDocument, saveDocumentHandler }, .{ "textDocument/didClose", requests.CloseDocument, closeDocumentHandler }, .{ "textDocument/semanticTokens/full", requests.SemanticTokensFull, semanticTokensFullHandler }, + .{ "textDocument/inlayHint", requests.InlayHint, inlayHintHandler }, .{ "textDocument/completion", requests.Completion, completionHandler }, .{ "textDocument/signatureHelp", requests.SignatureHelp, signatureHelpHandler }, .{ "textDocument/definition", requests.GotoDefinition, gotoDefinitionHandler }, diff --git a/src/data/data.zig b/src/data/data.zig new file mode 100644 index 0000000..f6e3b04 --- /dev/null +++ b/src/data/data.zig @@ -0,0 +1,10 @@ +const build_options = @import("build_options"); + +pub usingnamespace switch (build_options.data_version) { + .master => @import("master.zig"), + .@"0.7.0" => @import("0.7.0.zig"), + .@"0.7.1" => @import("0.7.1.zig"), + .@"0.8.0" => @import("0.8.0.zig"), + .@"0.8.1" => @import("0.8.1.zig"), + .@"0.9.0" => @import("0.9.0.zig"), +}; diff --git a/src/inlay_hints.zig b/src/inlay_hints.zig new file mode 100644 index 0000000..9ba7965 --- /dev/null +++ b/src/inlay_hints.zig @@ -0,0 +1,710 @@ +const std = @import("std"); +const DocumentStore = @import("DocumentStore.zig"); +const analysis = @import("analysis.zig"); +const types = @import("types.zig"); +const Ast = std.zig.Ast; +const log = std.log.scoped(.inlay_hint); +const ast = @import("ast.zig"); +const data = @import("data/data.zig"); +const Config = @import("Config.zig"); + +/// don't show inlay hints for the given builtin functions +/// builtins with one parameter are skipped automatically +/// this option is rare and is therefore build-only and +/// non-configurable at runtime +pub const inlay_hints_exclude_builtins: []const u8 = &.{}; + +/// max number of children in a declaration/array-init/struct-init or similar +/// that will not get a visibility check +pub const inlay_hints_max_inline_children = 12; + +/// checks whether node is inside the range +fn isNodeInRange(tree: Ast, node: Ast.Node.Index, range: types.Range) bool { + const endLocation = tree.tokenLocation(0, tree.lastToken(node)); + if (endLocation.line < range.start.line) return false; + + const beginLocation = tree.tokenLocation(0, tree.firstToken(node)); + if (beginLocation.line > range.end.line) return false; + + return true; +} + +const Builder = struct { + allocator: std.mem.Allocator, + config: *const Config, + handle: *DocumentStore.Handle, + hints: std.ArrayList(types.InlayHint), + hover_kind: types.MarkupContent.Kind, + + fn init(allocator: std.mem.Allocator, config: *const Config, handle: *DocumentStore.Handle, hover_kind: types.MarkupContent.Kind) Builder { + return Builder{ + .allocator = allocator, + .config = config, + .handle = handle, + .hints = std.ArrayList(types.InlayHint).init(allocator), + .hover_kind = hover_kind, + }; + } + + fn deinit(self: *Builder) void { + for (self.hints.items) |hint| { + self.allocator.free(hint.tooltip.value); + } + self.hints.deinit(); + } + + fn appendParameterHint(self: *Builder, position: Ast.Location, label: []const u8, tooltip: []const u8, tooltip_noalias: bool, tooltip_comptime: bool) !void { + // TODO allocation could be avoided by extending InlayHint.jsonStringify + // 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 ""; + + if (self.hover_kind == .Markdown) { + break :blk try std.fmt.allocPrint(self.allocator, "```zig\n{s}{s}\n```", .{ prefix, tooltip }); + } + + break :blk try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ prefix, tooltip }); + }; + + try self.hints.append(.{ + .position = .{ + .line = @intCast(i64, position.line), + .character = @intCast(i64, position.column), + }, + .label = label, + .kind = types.InlayHintKind.Parameter, + .tooltip = .{ + .kind = self.hover_kind, + .value = tooltip_text, + }, + .paddingLeft = false, + .paddingRight = true, + }); + } + + fn toOwnedSlice(self: *Builder) []types.InlayHint { + return self.hints.toOwnedSlice(); + } +}; + +/// `call` is the function call +/// `decl_handle` should be a function protototype +/// writes parameter hints into `builder.hints` +fn writeCallHint(builder: *Builder, arena: *std.heap.ArenaAllocator, store: *DocumentStore, call: Ast.full.Call, decl_handle: analysis.DeclWithHandle) !void { + const handle = builder.handle; + const tree = handle.tree; + + const decl = decl_handle.decl; + const decl_tree = decl_handle.handle.tree; + + switch (decl.*) { + .ast_node => |fn_node| { + var buffer: [1]Ast.Node.Index = undefined; + if (ast.fnProto(decl_tree, fn_node, &buffer)) |fn_proto| { + var i: usize = 0; + var it = fn_proto.iterate(&decl_tree); + + if (try analysis.hasSelfParam(arena, store, decl_handle.handle, fn_proto)) { + _ = it.next(); + } + + while (it.next()) |param| : (i += 1) { + if (param.name_token == null) continue; + if (i >= call.ast.params.len) break; + + const token_tags = decl_tree.tokens.items(.tag); + + const no_alias = if (param.comptime_noalias) |t| token_tags[t] == .keyword_noalias or token_tags[t - 1] == .keyword_noalias else false; + const comp_time = if (param.comptime_noalias) |t| token_tags[t] == .keyword_comptime or token_tags[t - 1] == .keyword_comptime else false; + + try builder.appendParameterHint( + tree.tokenLocation(0, tree.firstToken(call.ast.params[i])), + decl_tree.tokenSlice(param.name_token.?), + decl_tree.getNodeSource(param.type_expr), + no_alias, + comp_time, + ); + } + } + }, + else => {}, + } +} + +/// takes parameter nodes from the ast and function parameter names from `Builtin.arguments` and writes parameter hints into `builder.hints` +fn writeBuiltinHint(builder: *Builder, parameters: []Ast.Node.Index, arguments: []const []const u8) !void { + if (parameters.len == 0) return; + + const handle = builder.handle; + const tree = handle.tree; + + for (arguments) |arg, i| { + if (i >= parameters.len) break; + if (arg.len == 0) continue; + + const colonIndex = std.mem.indexOfScalar(u8, arg, ':'); + const type_expr: []const u8 = if (colonIndex) |index| arg[index + 1 ..] else &.{}; + + var label: ?[]const u8 = null; + var no_alias = false; + var comp_time = false; + + var it = std.mem.split(u8, arg[0 .. colonIndex orelse arg.len], " "); + while (it.next()) |item| { + if (item.len == 0) continue; + label = item; + + no_alias = no_alias or std.mem.eql(u8, item, "noalias"); + comp_time = comp_time or std.mem.eql(u8, item, "comptime"); + } + + try builder.appendParameterHint( + tree.tokenLocation(0, tree.firstToken(parameters[i])), + label orelse "", + std.mem.trim(u8, type_expr, " \t\n"), + no_alias, + comp_time, + ); + } +} + +/// takes a Ast.full.Call (a function call), analysis its function expression, finds its declaration and writes parameter hints into `builder.hints` +fn writeCallNodeHint(builder: *Builder, arena: *std.heap.ArenaAllocator, store: *DocumentStore, call: Ast.full.Call) !void { + if (call.ast.params.len == 0) return; + if (builder.config.inlay_hints_exclude_single_argument and call.ast.params.len == 1) return; + + const handle = builder.handle; + const tree = handle.tree; + const node_tags = tree.nodes.items(.tag); + const node_data = tree.nodes.items(.data); + const main_tokens = tree.nodes.items(.main_token); + const token_tags = tree.tokens.items(.tag); + + switch (node_tags[call.ast.fn_expr]) { + .identifier => { + const location = tree.tokenLocation(0, main_tokens[call.ast.fn_expr]); + + const absolute_index = location.line_start + location.column; + + const name = tree.tokenSlice(main_tokens[call.ast.fn_expr]); + + if (try analysis.lookupSymbolGlobal(store, arena, handle, name, absolute_index)) |decl_handle| { + try writeCallHint(builder, arena, store, call, decl_handle); + } + }, + .field_access => { + const lhsToken = tree.firstToken(call.ast.fn_expr); + const rhsToken = node_data[call.ast.fn_expr].rhs; + std.debug.assert(token_tags[rhsToken] == .identifier); + + const lhsLocation = tree.tokenLocation(0, lhsToken); + const rhsLocation = tree.tokenLocation(0, rhsToken); + + const absolute_index = rhsLocation.line_start + rhsLocation.column; + + const range = .{ + .start = lhsLocation.line_start + lhsLocation.column, + .end = rhsLocation.line_start + rhsLocation.column + tree.tokenSlice(rhsToken).len, + }; + + var held_range = handle.document.borrowNullTerminatedSlice(range.start, range.end); + var tokenizer = std.zig.Tokenizer.init(held_range.data()); + + // note: we have the ast node, traversing it would probably yield better results + // than trying to re-tokenize and re-parse it + errdefer held_range.release(); + if (try analysis.getFieldAccessType(store, arena, handle, absolute_index, &tokenizer)) |result| { + held_range.release(); + const container_handle = result.unwrapped orelse result.original; + switch (container_handle.type.data) { + .other => |container_handle_node| { + if (try analysis.lookupSymbolContainer( + store, + arena, + .{ .node = container_handle_node, .handle = container_handle.handle }, + tree.tokenSlice(rhsToken), + true, + )) |decl_handle| { + try writeCallHint(builder, arena, store, call, decl_handle); + } + }, + else => {}, + } + } + }, + else => { + log.debug("cannot deduce fn expression with tag '{}'", .{node_tags[call.ast.fn_expr]}); + }, + } +} + +/// iterates over the ast and writes parameter hints into `builder.hints` for every function call and builtin call +/// nodes outside the given range are excluded +fn writeNodeInlayHint(builder: *Builder, arena: *std.heap.ArenaAllocator, store: *DocumentStore, maybe_node: ?Ast.Node.Index, range: types.Range) error{OutOfMemory}!void { + const node = maybe_node orelse return; + + const handle = builder.handle; + const tree = handle.tree; + const node_tags = tree.nodes.items(.tag); + const node_data = tree.nodes.items(.data); + const main_tokens = tree.nodes.items(.main_token); + + if (node == 0 or node > node_data.len) return; + + const FrameSize = @sizeOf(@Frame(writeNodeInlayHint)); + var child_frame = try arena.child_allocator.alignedAlloc(u8, std.Target.stack_align, FrameSize); + defer arena.child_allocator.free(child_frame); + + const tag = node_tags[node]; + + // NOTE traversing the ast instead of iterating over all nodes allows using visibility + // checks based on the given range which reduce runtimes by orders of magnitude for large files + switch (tag) { + .root => unreachable, + .call_one, + .call_one_comma, + .async_call_one, + .async_call_one_comma, + .call, + .call_comma, + .async_call, + .async_call_comma, + => { + var params: [1]Ast.Node.Index = undefined; + const call = ast.callFull(tree, node, ¶ms).?; + try writeCallNodeHint(builder, arena, store, call); + + for (call.ast.params) |param| { + if (call.ast.params.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, param, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, param, range }); + } + }, + + .builtin_call_two, + .builtin_call_two_comma, + .builtin_call, + .builtin_call_comma, + => { + var buffer: [2]Ast.Node.Index = undefined; + const parameters: []Ast.Node.Index = switch (tag) { + .builtin_call_two, .builtin_call_two_comma => blk: { + buffer[0] = node_data[node].lhs; + buffer[1] = node_data[node].rhs; + + var size: usize = 0; + + if (node_data[node].rhs != 0) { + size = 2; + } else if (node_data[node].lhs != 0) { + size = 1; + } + break :blk buffer[0..size]; + }, + .builtin_call, .builtin_call_comma => tree.extra_data[node_data[node].lhs..node_data[node].rhs], + else => unreachable, + }; + + if (builder.config.inlay_hints_show_builtin and parameters.len > 1) { + const name = tree.tokenSlice(main_tokens[node]); + + outer: for (data.builtins) |builtin| { + if (!std.mem.eql(u8, builtin.name, name)) continue; + + for (inlay_hints_exclude_builtins) |builtin_name| { + if (std.mem.eql(u8, builtin_name, name)) break :outer; + } + + try writeBuiltinHint(builder, parameters, builtin.arguments); + } + } + + for (parameters) |param| { + if (parameters.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, param, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, param, range }); + } + }, + + .optional_type, + .array_type, + .@"continue", + .anyframe_type, + .anyframe_literal, + .char_literal, + .integer_literal, + .float_literal, + .unreachable_literal, + .identifier, + .enum_literal, + .string_literal, + .multiline_string_literal, + .error_set_decl, + => {}, + + .array_type_sentinel => { + const array_type = tree.arrayTypeSentinel(node); + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, array_type.ast.sentinel, range }); + }, + + .ptr_type_aligned, + .ptr_type_sentinel, + .ptr_type, + .ptr_type_bit_range, + => { + const ptr_type: Ast.full.PtrType = ast.ptrType(tree, node).?; + + if (ptr_type.ast.sentinel != 0) { + return try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, ptr_type.ast.sentinel, range }); + } + + if (ptr_type.ast.align_node != 0) { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, ptr_type.ast.align_node, range }); + + if (ptr_type.ast.bit_range_start != 0) { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, ptr_type.ast.bit_range_start, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, ptr_type.ast.bit_range_end, range }); + } + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, ptr_type.ast.child_type, range }); + }, + + .@"usingnamespace", + .field_access, + .unwrap_optional, + .bool_not, + .negation, + .bit_not, + .negation_wrap, + .address_of, + .@"try", + .@"await", + .deref, + .@"suspend", + .@"resume", + .@"return", + .grouped_expression, + .@"comptime", + .@"nosuspend", + => try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].lhs, range }), + + .test_decl, + .global_var_decl, + .local_var_decl, + .simple_var_decl, + .aligned_var_decl, + .@"errdefer", + .@"defer", + .@"break", + => try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].rhs, range }), + + .@"catch", + .equal_equal, + .bang_equal, + .less_than, + .greater_than, + .less_or_equal, + .greater_or_equal, + .assign_mul, + .assign_div, + .assign_mod, + .assign_add, + .assign_sub, + .assign_shl, + .assign_shl_sat, + .assign_shr, + .assign_bit_and, + .assign_bit_xor, + .assign_bit_or, + .assign_mul_wrap, + .assign_add_wrap, + .assign_sub_wrap, + .assign_mul_sat, + .assign_add_sat, + .assign_sub_sat, + .assign, + .merge_error_sets, + .mul, + .div, + .mod, + .array_mult, + .mul_wrap, + .mul_sat, + .add, + .sub, + .array_cat, + .add_wrap, + .sub_wrap, + .add_sat, + .sub_sat, + .shl, + .shl_sat, + .shr, + .bit_and, + .bit_xor, + .bit_or, + .@"orelse", + .bool_and, + .bool_or, + .array_access, + .switch_range, + .error_value, + .error_union, + => { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].lhs, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].rhs, range }); + }, + + .slice_open, + .slice, + .slice_sentinel, + => { + const slice: Ast.full.Slice = switch (tag) { + .slice => tree.slice(node), + .slice_open => tree.sliceOpen(node), + .slice_sentinel => tree.sliceSentinel(node), + else => unreachable, + }; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, slice.ast.sliced, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, slice.ast.start, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, slice.ast.end, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, slice.ast.sentinel, range }); + }, + + .array_init_one, + .array_init_one_comma, + .array_init_dot_two, + .array_init_dot_two_comma, + .array_init_dot, + .array_init_dot_comma, + .array_init, + .array_init_comma, + => { + var buffer: [2]Ast.Node.Index = undefined; + const array_init: Ast.full.ArrayInit = switch (tag) { + .array_init, .array_init_comma => tree.arrayInit(node), + .array_init_one, .array_init_one_comma => tree.arrayInitOne(buffer[0..1], node), + .array_init_dot, .array_init_dot_comma => tree.arrayInitDot(node), + .array_init_dot_two, .array_init_dot_two_comma => tree.arrayInitDotTwo(&buffer, node), + else => unreachable, + }; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, array_init.ast.type_expr, range }); + for (array_init.ast.elements) |elem| { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, elem, range }); + } + }, + + .struct_init_one, + .struct_init_one_comma, + .struct_init_dot_two, + .struct_init_dot_two_comma, + .struct_init_dot, + .struct_init_dot_comma, + .struct_init, + .struct_init_comma, + => { + var buffer: [2]Ast.Node.Index = undefined; + const struct_init: Ast.full.StructInit = switch (tag) { + .struct_init, .struct_init_comma => tree.structInit(node), + .struct_init_dot, .struct_init_dot_comma => tree.structInitDot(node), + .struct_init_one, .struct_init_one_comma => tree.structInitOne(buffer[0..1], node), + .struct_init_dot_two, .struct_init_dot_two_comma => tree.structInitDotTwo(&buffer, node), + else => unreachable, + }; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, struct_init.ast.type_expr, range }); + + for (struct_init.ast.fields) |field_init| { + if (struct_init.ast.fields.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, field_init, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, field_init, range }); + } + }, + + .@"switch", + .switch_comma, + => { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].lhs, range }); + + const extra = tree.extraData(node_data[node].rhs, Ast.Node.SubRange); + const cases = tree.extra_data[extra.start..extra.end]; + + for (cases) |case_node| { + if (cases.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, case_node, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, case_node, range }); + } + }, + + .switch_case_one, + .switch_case, + => { + const switch_case = if (tag == .switch_case) tree.switchCase(node) else tree.switchCaseOne(node); + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, switch_case.ast.target_expr, range }); + }, + + .while_simple, + .while_cont, + .@"while", + .for_simple, + .@"for", + => { + const while_node = ast.whileAst(tree, node).?; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, while_node.ast.cond_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, while_node.ast.cont_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, while_node.ast.then_expr, range }); + + if (while_node.ast.else_expr != 0) { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, while_node.ast.else_expr, range }); + } + }, + + .if_simple, + .@"if", + => { + const if_node = ast.ifFull(tree, node); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, if_node.ast.cond_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, if_node.ast.then_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, if_node.ast.else_expr, range }); + }, + + .fn_proto_simple, + .fn_proto_multi, + .fn_proto_one, + .fn_proto, + .fn_decl, + => { + var buffer: [1]Ast.Node.Index = undefined; + const fn_proto: Ast.full.FnProto = ast.fnProto(tree, node, &buffer).?; + + var it = fn_proto.iterate(&tree); + while (it.next()) |param_decl| { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, param_decl.type_expr, range }); + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, fn_proto.ast.align_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, fn_proto.ast.addrspace_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, fn_proto.ast.section_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, fn_proto.ast.callconv_expr, range }); + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, fn_proto.ast.return_type, range }); + + if (tag == .fn_decl) { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].rhs, range }); + } + }, + + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => { + var buffer: [2]Ast.Node.Index = undefined; + const decl: Ast.full.ContainerDecl = switch (tag) { + .container_decl, .container_decl_trailing => tree.containerDecl(node), + .container_decl_two, .container_decl_two_trailing => tree.containerDeclTwo(&buffer, node), + .container_decl_arg, .container_decl_arg_trailing => tree.containerDeclArg(node), + .tagged_union, .tagged_union_trailing => tree.taggedUnion(node), + .tagged_union_enum_tag, .tagged_union_enum_tag_trailing => tree.taggedUnionEnumTag(node), + .tagged_union_two, .tagged_union_two_trailing => tree.taggedUnionTwo(&buffer, node), + else => unreachable, + }; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, decl.ast.arg, range }); + + for (decl.ast.members) |child| { + if (decl.ast.members.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, child, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, child, range }); + } + }, + + .container_field_init, + .container_field_align, + .container_field, + => { + const container_field = ast.containerField(tree, node).?; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, container_field.ast.value_expr, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, container_field.ast.align_expr, range }); + }, + + .block_two, + .block_two_semicolon, + => { + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].lhs, range }); + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, node_data[node].rhs, range }); + }, + + .block, + .block_semicolon, + => { + const subrange = tree.extra_data[node_data[node].lhs..node_data[node].rhs]; + + for (subrange) |child| { + if (subrange.len > inlay_hints_max_inline_children) { + if (!isNodeInRange(tree, child, range)) continue; + } + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, child, range }); + } + }, + + .asm_simple, + .@"asm", + .asm_output, + .asm_input, + => { + const asm_node: Ast.full.Asm = switch (tag) { + .@"asm" => tree.asmFull(node), + .asm_simple => tree.asmSimple(node), + else => return, + }; + + try await @asyncCall(child_frame, {}, writeNodeInlayHint, .{ builder, arena, store, asm_node.ast.template, range }); + }, + } +} + +/// creates a list of `InlayHint`'s from the given document +/// only parameter hints are created +/// only hints in the given range are created +/// Caller owns returned memory. +/// `InlayHint.tooltip.value` has to deallocated separately +pub fn writeRangeInlayHint(arena: *std.heap.ArenaAllocator, config: *const Config, store: *DocumentStore, handle: *DocumentStore.Handle, range: types.Range, hover_kind: types.MarkupContent.Kind) error{OutOfMemory}![]types.InlayHint { + var builder = Builder.init(arena.child_allocator, config, handle, hover_kind); + errdefer builder.deinit(); + + var buf: [2]Ast.Node.Index = undefined; + for (ast.declMembers(handle.tree, 0, &buf)) |child| { + if (!isNodeInRange(handle.tree, child, range)) continue; + try writeNodeInlayHint(&builder, arena, store, child, range); + } + + return builder.toOwnedSlice(); +} diff --git a/src/requests.zig b/src/requests.zig index ca5d67a..621ed94 100644 --- a/src/requests.zig +++ b/src/requests.zig @@ -155,6 +155,7 @@ pub const Initialize = struct { }, textDocument: ?struct { semanticTokens: Exists, + inlayHint: Exists, hover: ?struct { contentFormat: MaybeStringArray, }, @@ -264,6 +265,13 @@ pub const References = struct { }, }; +pub const InlayHint = struct { + params: struct { + textDocument: TextDocumentIdentifier, + range: types.Range, + }, +}; + pub const Configuration = struct { params: struct { settings: struct { @@ -276,6 +284,9 @@ pub const Configuration = struct { build_runner_path: ?[]const u8, build_runner_cache_path: ?[]const u8, enable_semantic_tokens: ?bool, + enable_inlay_hints: ?bool, + inlay_hints_show_builtin: ?bool, + inlay_hints_exclude_single_argument: ?bool, operator_completions: ?bool, include_at_in_builtins: ?bool, max_detail_length: ?usize, diff --git a/src/setup.zig b/src/setup.zig index 8ff24a9..6e54f8e 100644 --- a/src/setup.zig +++ b/src/setup.zig @@ -174,6 +174,7 @@ pub fn wizard(allocator: std.mem.Allocator) !void { const ief_apc = try askBool("Do you want to enable @import/@embedFile argument path completion?"); const style = try askBool("Do you want to enable style warnings?"); const semantic_tokens = try askBool("Do you want to enable semantic highlighting?"); + const inlay_hints = try askBool("Do you want to enable inlay hints?"); const operator_completions = try askBool("Do you want to enable .* and .? completions?"); const include_at_in_builtins = switch (editor) { .Sublime => true, @@ -194,6 +195,7 @@ pub fn wizard(allocator: std.mem.Allocator) !void { .enable_import_embedfile_argument_completions = ief_apc, .warn_style = style, .enable_semantic_tokens = semantic_tokens, + .enable_inlay_hints = inlay_hints, .operator_completions = operator_completions, .include_at_in_builtins = include_at_in_builtins, .max_detail_length = max_detail_length, diff --git a/src/types.zig b/src/types.zig index 21a08de..2ea83b3 100644 --- a/src/types.zig +++ b/src/types.zig @@ -39,6 +39,7 @@ pub const ResponseParams = union(enum) { Hover: Hover, DocumentSymbols: []DocumentSymbol, SemanticTokensFull: struct { data: []const u32 }, + InlayHint: []InlayHint, TextEdits: []TextEdit, Locations: []Location, WorkspaceEdit: WorkspaceEdit, @@ -334,6 +335,41 @@ pub const SignatureHelp = struct { activeParameter: ?u32, }; +pub const InlayHint = struct { + position: Position, + label: string, + kind: InlayHintKind, + tooltip: MarkupContent, + paddingLeft: bool, + paddingRight: bool, + + // appends a colon to the label and reduces the output size + pub fn jsonStringify(value: InlayHint, options: std.json.StringifyOptions, writer: anytype) @TypeOf(writer).Error!void { + try writer.writeAll("{\"position\":"); + try std.json.stringify(value.position, options, writer); + try writer.writeAll(",\"label\":\""); + try writer.writeAll(value.label); + try writer.writeAll(":\",\"kind\":"); + try std.json.stringify(value.kind, options, writer); + if (value.tooltip.value.len != 0) { + try writer.writeAll(",\"tooltip\":"); + try std.json.stringify(value.tooltip, options, writer); + } + if (value.paddingLeft) try writer.writeAll(",\"paddingLeft\":true"); + if (value.paddingRight) try writer.writeAll(",\"paddingRight\":true"); + try writer.writeByte('}'); + } +}; + +pub const InlayHintKind = enum(i64) { + Type = 1, + Parameter = 2, + + pub fn jsonStringify(value: InlayHintKind, options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(@enumToInt(value), options, out_stream); + } +}; + // Only includes options we set in our initialize result. const InitializeResult = struct { offsetEncoding: string, @@ -388,6 +424,7 @@ const InitializeResult = struct { tokenModifiers: []const string, }, }, + inlayHintProvider: bool, }, serverInfo: struct { name: string, diff --git a/tests/sessions.zig b/tests/sessions.zig index ca361c1..d59e742 100644 --- a/tests/sessions.zig +++ b/tests/sessions.zig @@ -102,7 +102,7 @@ const Server = struct { fn shutdown(self: *Server) void { // FIXME this shutdown request fails with a broken pipe on stdin on the CI - self.request("shutdown", "{}", null) catch @panic("Could not send shutdown request"); + self.request("shutdown", "{}", null) catch {}; // waitNoError(self.process) catch @panic("Server error"); } }; @@ -252,7 +252,7 @@ test "Pointer and optional deref" { test "Request utf-8 offset encoding" { var server = try Server.start(initialize_msg_offs, - \\{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["("],"retriggerCharacters":[","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"],"completionItem":{"labelDetailsSupport":true}},"documentHighlightProvider":false,"hoverProvider":true,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":false,"referencesProvider":true,"documentSymbolProvider":true,"colorProvider":false,"documentFormattingProvider":true,"documentRangeFormattingProvider":false,"foldingRangeProvider":false,"selectionRangeProvider":false,"workspaceSymbolProvider":false,"rangeProvider":false,"documentProvider":true,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"semanticTokensProvider":{"full":true,"range":false,"legend":{"tokenTypes":["type","parameter","variable","enumMember","field","errorTag","function","keyword","comment","string","number","operator","builtin","label","keywordLiteral"],"tokenModifiers":["namespace","struct","enum","union","opaque","declaration","async","documentation","generic"]}}},"serverInfo":{"name":"zls","version":"0.1.0"}} + \\{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["("],"retriggerCharacters":[","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"],"completionItem":{"labelDetailsSupport":true}},"documentHighlightProvider":true,"hoverProvider":true,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":false,"referencesProvider":true,"documentSymbolProvider":true,"colorProvider":false,"documentFormattingProvider":true,"documentRangeFormattingProvider":false,"foldingRangeProvider":false,"selectionRangeProvider":false,"workspaceSymbolProvider":false,"rangeProvider":false,"documentProvider":true,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"semanticTokensProvider":{"full":true,"range":false,"legend":{"tokenTypes":["type","parameter","variable","enumMember","field","errorTag","function","keyword","comment","string","number","operator","builtin","label","keywordLiteral"],"tokenModifiers":["namespace","struct","enum","union","opaque","declaration","async","documentation","generic"]}},"inlayHintProvider":true},"serverInfo":{"name":"zls","version":"0.1.0"}} ); defer server.shutdown(); }