From 64fd8c233179b04f4420d5388ed6114c6547d1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:11:37 -0700 Subject: [PATCH 1/2] Implement hover and goto for enum literals --- src/Server.zig | 12 ++ src/analysis.zig | 241 ++++++++++++++++++++++++++++++++++- src/ast.zig | 27 ++++ src/features/goto.zig | 14 ++ src/features/hover.zig | 22 ++++ src/features/inlay_hints.zig | 4 +- 6 files changed, 311 insertions(+), 9 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 58a319422..b8d125203 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -311,6 +311,18 @@ pub fn getSymbolGlobal( }); } +pub fn getSymbolEnumLiteral( + server: *Server, + source_index: usize, + handle: *const DocumentStore.Handle, +) error{OutOfMemory}!?Analyser.DeclWithHandle { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + const nodes = try ast.nodesOverlappingIndex(server.arena.allocator(), handle.tree, source_index); + return try server.analyser.lookupSymbolEnumLiteral(handle, nodes); +} + /// Multiple when using branched types pub fn getSymbolFieldAccesses( server: *Server, diff --git a/src/analysis.zig b/src/analysis.zig index 469037b9a..671a54668 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -211,6 +211,18 @@ pub fn getFunctionSnippet(allocator: std.mem.Allocator, tree: Ast, func: Ast.ful return buffer.toOwnedSlice(allocator); } +pub fn isInstanceCall( + analyser: *Analyser, + call_handle: *const DocumentStore.Handle, + call: Ast.full.Call, + func_handle: *const DocumentStore.Handle, + func: Ast.full.FnProto, +) !bool { + return call_handle.tree.tokens.items(.tag)[call.ast.lparen - 2] == .period and + call.ast.params.len + 1 == func.ast.params.len and + try analyser.hasSelfParam(func_handle, func); +} + pub fn hasSelfParam(analyser: *Analyser, handle: *const DocumentStore.Handle, func: Ast.full.FnProto) !bool { // Non-decl prototypes cannot have a self parameter. if (func.name_token == null) return false; @@ -782,11 +794,9 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e // If we call as method, the first parameter should be skipped // TODO: Back-parse to extract the self argument? var it = fn_decl.iterate(&decl.handle.tree); - if (token_tags[call.ast.lparen - 2] == .period) { - if (try analyser.hasSelfParam(decl.handle, fn_decl)) { - _ = ast.nextFnParam(&it); - expected_params -= 1; - } + if (try analyser.isInstanceCall(handle, call, decl.handle, fn_decl)) { + _ = ast.nextFnParam(&it); + expected_params -= 1; } // Bind type params to the arguments passed in the call. @@ -1915,7 +1925,9 @@ pub fn getPositionContext( .field_access => {}, .other => {}, .global_error_set => {}, - .label => {}, + .label => |filled| if (filled) { + curr_ctx.ctx = .enum_literal; + }, else => curr_ctx.ctx = .{ .field_access = tokenLocAppend(curr_ctx.ctx.loc().?, tok), }, @@ -2578,6 +2590,223 @@ pub fn lookupSymbolContainer( return null; } +pub fn lookupSymbolEnumLiteral( + analyser: *Analyser, + handle: *const DocumentStore.Handle, + nodes: []Ast.Node.Index, +) error{OutOfMemory}!?DeclWithHandle { + if (nodes.len == 0) return null; + + const tree = handle.tree; + const node_tags = tree.nodes.items(.tag); + if (node_tags[nodes[0]] != .enum_literal) return null; + + const container_type = (try analyser.resolveEnumLiteralType( + handle, + nodes[0], + nodes[1..], + )) orelse return null; + + const container_node = switch (container_type.type.data) { + .other => |n| n, + else => return null, + }; + + return analyser.lookupSymbolContainer( + .{ .node = container_node, .handle = container_type.handle }, + tree.tokenSlice(tree.nodes.items(.main_token)[nodes[0]]), + !container_type.isEnumType(), + ); +} + +pub fn resolveEnumLiteralType( + analyser: *Analyser, + handle: *const DocumentStore.Handle, + node: Ast.Node.Index, + ancestors: []Ast.Node.Index, +) error{OutOfMemory}!?TypeWithHandle { + if (ancestors.len == 0) return null; + + const tree = handle.tree; + const node_tags: []Ast.Node.Tag = tree.nodes.items(.tag); + const datas: []Ast.Node.Data = tree.nodes.items(.data); + const token_tags: []std.zig.Token.Tag = tree.tokens.items(.tag); + + var call_buf: [1]Ast.Node.Index = undefined; + + if (tree.fullVarDecl(ancestors[0])) |var_decl| { + if (node == var_decl.ast.init_node) { + return try analyser.resolveTypeOfNode(.{ + .node = ancestors[0], + .handle = handle, + }); + } + } else if (tree.fullIf(ancestors[0])) |if_node| { + if (node == if_node.ast.then_expr or node == if_node.ast.else_expr) { + return (try analyser.resolveTypeOfNode(.{ + .node = ancestors[0], + .handle = handle, + })) orelse (try analyser.resolveEnumLiteralType( + handle, + ancestors[0], + ancestors[1..], + )); + } + } else if (tree.fullFor(ancestors[0])) |for_node| { + if (node == for_node.ast.else_expr) { + return (try analyser.resolveTypeOfNode(.{ + .node = ancestors[0], + .handle = handle, + })) orelse (try analyser.resolveEnumLiteralType( + handle, + ancestors[0], + ancestors[1..], + )); + } + } else if (tree.fullWhile(ancestors[0])) |while_node| { + if (node == while_node.ast.else_expr) { + return (try analyser.resolveTypeOfNode(.{ + .node = ancestors[0], + .handle = handle, + })) orelse (try analyser.resolveEnumLiteralType( + handle, + ancestors[0], + ancestors[1..], + )); + } + } else if (tree.fullSwitchCase(ancestors[0])) |switch_case| { + if (ancestors.len == 1) return null; + + switch (node_tags[ancestors[1]]) { + .@"switch", .switch_comma => {}, + else => return null, + } + + if (node == switch_case.ast.target_expr) { + return (try analyser.resolveTypeOfNode(.{ + .node = ancestors[1], + .handle = handle, + })) orelse (try analyser.resolveEnumLiteralType( + handle, + ancestors[1], + ancestors[2..], + )); + } + + for (switch_case.ast.values) |value| { + if (node == value) { + return try analyser.resolveTypeOfNode(.{ + .node = datas[ancestors[1]].lhs, + .handle = handle, + }); + } + } + } else if (tree.fullCall(&call_buf, ancestors[0])) |call| { + const arg_index = std.mem.indexOfScalar(Ast.Node.Index, call.ast.params, node) orelse return null; + + const fn_type = (try analyser.resolveTypeOfNode(.{ + .node = call.ast.fn_expr, + .handle = handle, + })) orelse return null; + + if (fn_type.type.is_type_val) return null; + + const fn_handle = fn_type.handle; + const fn_tree = fn_handle.tree; + const fn_node = switch (fn_type.type.data) { + .other => |n| n, + else => return null, + }; + + var fn_buf: [1]Ast.Node.Index = undefined; + const fn_proto = fn_tree.fullFnProto(&fn_buf, fn_node) orelse return null; + + var param_iter = fn_proto.iterate(&fn_tree); + if (try analyser.isInstanceCall(handle, call, fn_handle, fn_proto)) { + _ = ast.nextFnParam(¶m_iter); + } + + var param_index: usize = 0; + while (ast.nextFnParam(¶m_iter)) |param| : (param_index += 1) { + if (param_index == arg_index) { + return try analyser.resolveTypeOfNode(.{ + .node = param.type_expr, + .handle = fn_handle, + }); + } + } + } else switch (node_tags[ancestors[0]]) { + .assign => { + if (node == datas[ancestors[0]].rhs) { + return try analyser.resolveTypeOfNode(.{ + .node = datas[ancestors[0]].lhs, + .handle = handle, + }); + } + }, + + .equal_equal, .bang_equal => { + return (try analyser.resolveTypeOfNode(.{ + .node = datas[ancestors[0]].lhs, + .handle = handle, + })) orelse (try analyser.resolveTypeOfNode(.{ + .node = datas[ancestors[0]].rhs, + .handle = handle, + })); + }, + + .@"break" => { + if (node != datas[ancestors[0]].rhs) return null; + + const break_label_maybe: ?[]const u8 = if (datas[ancestors[0]].lhs != 0) + tree.tokenSlice(datas[ancestors[0]].lhs) + else + null; + + const index = blk: for (1..ancestors.len) |index| { + if (tree.fullFor(ancestors[index])) |for_node| { + const break_label = break_label_maybe orelse break :blk index; + const for_label = tree.tokenSlice(for_node.label_token orelse continue); + if (std.mem.eql(u8, break_label, for_label)) break :blk index; + } else if (tree.fullWhile(ancestors[index])) |while_node| { + const break_label = break_label_maybe orelse break :blk index; + const while_label = tree.tokenSlice(while_node.label_token orelse continue); + if (std.mem.eql(u8, break_label, while_label)) break :blk index; + } else switch (node_tags[ancestors[index]]) { + .block, + .block_semicolon, + .block_two, + .block_two_semicolon, + => { + const break_label = break_label_maybe orelse continue; + + const first_token = tree.firstToken(ancestors[index]); + if (token_tags[first_token] != .identifier) continue; + const block_label = tree.tokenSlice(first_token); + + if (std.mem.eql(u8, break_label, block_label)) break :blk index; + }, + + else => {}, + } + } else return null; + + return (try analyser.resolveTypeOfNode(.{ + .node = ancestors[index], + .handle = handle, + })) orelse (try analyser.resolveEnumLiteralType( + handle, + ancestors[index], + ancestors[index + 1 ..], + )); + }, + + else => {}, // TODO: Implement more expressions that may contain enum literals; better safe than sorry + } + + return null; +} + const CompletionContext = struct { pub fn hash(self: @This(), item: types.CompletionItem) u32 { _ = self; diff --git a/src/ast.zig b/src/ast.zig index c9bad9285..625ab375a 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -1604,6 +1604,33 @@ pub fn nodeChildrenRecursiveAlloc(allocator: std.mem.Allocator, tree: Ast, node: return children.toOwnedSlice(allocator); } +/// returns a list of nodes that overlap with the given source code index. +/// sorted from smallest to largest. +/// caller owns the returned memory. +pub fn nodesOverlappingIndex(allocator: std.mem.Allocator, tree: Ast, index: usize) error{OutOfMemory}![]Ast.Node.Index { + std.debug.assert(index <= tree.source.len); + + const Context = struct { + index: usize, + allocator: std.mem.Allocator, + nodes: std.ArrayListUnmanaged(Ast.Node.Index) = .{}, + + pub fn append(self: *@This(), ast: Ast, node: Ast.Node.Index) error{OutOfMemory}!void { + if (node == 0) return; + const loc = offsets.nodeToLoc(ast, node); + if (loc.start <= self.index and self.index <= loc.end) { + try iterateChildren(ast, node, self, error{OutOfMemory}, append); + try self.nodes.append(self.allocator, node); + } + } + }; + + var context: Context = .{ .index = index, .allocator = allocator }; + try iterateChildren(tree, 0, &context, error{OutOfMemory}, Context.append); + try context.nodes.append(allocator, 0); + return try context.nodes.toOwnedSlice(allocator); +} + /// returns a list of nodes that together encloses the given source code range /// caller owns the returned memory pub fn nodesAtLoc(allocator: std.mem.Allocator, tree: Ast, loc: offsets.Loc) error{OutOfMemory}![]Ast.Node.Index { diff --git a/src/features/goto.zig b/src/features/goto.zig index c3c90d861..276cf6e89 100644 --- a/src/features/goto.zig +++ b/src/features/goto.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Ast = std.zig.Ast; const log = std.log.scoped(.zls_goto); +const ast = @import("../ast.zig"); const types = @import("../lsp.zig"); const offsets = @import("../offsets.zig"); const URI = @import("../uri.zig"); @@ -67,6 +68,18 @@ pub fn gotoDefinitionGlobal( return try gotoDefinitionSymbol(server, decl, resolve_alias); } +pub fn gotoDefinitionEnumLiteral( + server: *Server, + source_index: usize, + handle: *const DocumentStore.Handle, +) error{OutOfMemory}!?types.Location { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + const decl = (try server.getSymbolEnumLiteral(source_index, handle)) orelse return null; + return try gotoDefinitionSymbol(server, decl, false); +} + pub fn gotoDefinitionBuiltin( server: *Server, handle: *const DocumentStore.Handle, @@ -191,6 +204,7 @@ pub fn goto( .embedfile_string_literal, => .{ .Location = (try gotoDefinitionString(server, pos_context, handle)) orelse return null }, .label => .{ .Location = (try gotoDefinitionLabel(server, source_index, handle)) orelse return null }, + .enum_literal => .{ .Location = (try gotoDefinitionEnumLiteral(server, source_index, handle)) orelse return null }, else => null, }; } diff --git a/src/features/hover.zig b/src/features/hover.zig index b2a072a3d..c654a2163 100644 --- a/src/features/hover.zig +++ b/src/features/hover.zig @@ -207,6 +207,27 @@ pub fn hoverDefinitionGlobal(server: *Server, pos_index: usize, handle: *const D }; } +pub fn hoverDefinitionEnumLiteral( + server: *Server, + source_index: usize, + handle: *const DocumentStore.Handle, +) error{OutOfMemory}!?types.Hover { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + const markup_kind: types.MarkupKind = if (server.client_capabilities.hover_supports_md) .markdown else .plaintext; + const decl = (try server.getSymbolEnumLiteral(source_index, handle)) orelse return null; + + return .{ + .contents = .{ + .MarkupContent = .{ + .kind = markup_kind, + .value = (try hoverSymbol(server, decl, markup_kind)) orelse return null, + }, + }, + }; +} + pub fn hoverDefinitionFieldAccess( server: *Server, handle: *const DocumentStore.Handle, @@ -244,6 +265,7 @@ pub fn hover(server: *Server, source_index: usize, handle: *const DocumentStore. .var_access => try hoverDefinitionGlobal(server, source_index, handle), .field_access => |loc| try hoverDefinitionFieldAccess(server, handle, source_index, loc), .label => try hoverDefinitionLabel(server, source_index, handle), + .enum_literal => try hoverDefinitionEnumLiteral(server, source_index, handle), else => null, }; diff --git a/src/features/inlay_hints.zig b/src/features/inlay_hints.zig index be45b622d..931728494 100644 --- a/src/features/inlay_hints.zig +++ b/src/features/inlay_hints.zig @@ -99,9 +99,7 @@ fn writeCallHint(builder: *Builder, call: Ast.full.Call, decl_handle: Analyser.D try params.append(builder.arena, param); } - const has_self_param = tree.tokens.items(.tag)[call.ast.lparen - 2] == .period and - call.ast.params.len + 1 == params.items.len and - try builder.analyser.hasSelfParam(decl_handle.handle, fn_proto); + const has_self_param = try builder.analyser.isInstanceCall(handle, call, decl_handle.handle, fn_proto); const parameters = params.items[@intFromBool(has_self_param)..]; const arguments = call.ast.params; From 7baecf2028707e6acbe01c568fde709c055bac0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:50:17 -0700 Subject: [PATCH 2/2] Fix test failure --- src/analysis.zig | 1 - src/features/inlay_hints.zig | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analysis.zig b/src/analysis.zig index 671a54668..51a84043a 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -219,7 +219,6 @@ pub fn isInstanceCall( func: Ast.full.FnProto, ) !bool { return call_handle.tree.tokens.items(.tag)[call.ast.lparen - 2] == .period and - call.ast.params.len + 1 == func.ast.params.len and try analyser.hasSelfParam(func_handle, func); } diff --git a/src/features/inlay_hints.zig b/src/features/inlay_hints.zig index 931728494..d756f000d 100644 --- a/src/features/inlay_hints.zig +++ b/src/features/inlay_hints.zig @@ -99,7 +99,8 @@ fn writeCallHint(builder: *Builder, call: Ast.full.Call, decl_handle: Analyser.D try params.append(builder.arena, param); } - const has_self_param = try builder.analyser.isInstanceCall(handle, call, decl_handle.handle, fn_proto); + const has_self_param = call.ast.params.len + 1 == params.items.len and + try builder.analyser.isInstanceCall(handle, call, decl_handle.handle, fn_proto); const parameters = params.items[@intFromBool(has_self_param)..]; const arguments = call.ast.params;