* move language feature files into separate folder * move code completions into features/completions.zig * move goto definition/declaration into features folder * move hover into features folder * restructure imports * move selection range into features folder * restructure request handling tracy call * add missing features to `zls.zig`
311 lines
11 KiB
Zig
311 lines
11 KiB
Zig
const std = @import("std");
|
|
const zig_builtin = @import("builtin");
|
|
const Ast = std.zig.Ast;
|
|
const log = std.log.scoped(.zls_inlay_hint);
|
|
|
|
const DocumentStore = @import("../DocumentStore.zig");
|
|
const Analyser = @import("../analysis.zig");
|
|
const types = @import("../lsp.zig");
|
|
const offsets = @import("../offsets.zig");
|
|
const tracy = @import("../tracy.zig");
|
|
const ast = @import("../ast.zig");
|
|
const Config = @import("../Config.zig");
|
|
|
|
const data = @import("../data/data.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 = &.{};
|
|
|
|
pub const InlayHint = struct {
|
|
token_index: Ast.TokenIndex,
|
|
label: []const u8,
|
|
kind: types.InlayHintKind,
|
|
tooltip: types.MarkupContent,
|
|
};
|
|
|
|
const Builder = struct {
|
|
arena: std.mem.Allocator,
|
|
analyser: *Analyser,
|
|
config: *const Config,
|
|
handle: *const DocumentStore.Handle,
|
|
hints: std.ArrayListUnmanaged(InlayHint),
|
|
hover_kind: types.MarkupKind,
|
|
|
|
fn appendParameterHint(self: *Builder, token_index: Ast.TokenIndex, label: []const u8, tooltip: []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 "";
|
|
|
|
if (self.hover_kind == .markdown) {
|
|
break :blk try std.fmt.allocPrint(self.arena, "```zig\n{s}{s}\n```", .{ prefix, tooltip });
|
|
}
|
|
|
|
break :blk try std.fmt.allocPrint(self.arena, "{s}{s}", .{ prefix, tooltip });
|
|
};
|
|
|
|
try self.hints.append(self.arena, .{
|
|
.token_index = token_index,
|
|
.label = try std.fmt.allocPrint(self.arena, "{s}:", .{label}),
|
|
.kind = .Parameter,
|
|
.tooltip = .{
|
|
.kind = self.hover_kind,
|
|
.value = tooltip_text,
|
|
},
|
|
});
|
|
}
|
|
|
|
fn toOwnedSlice(self: *Builder) error{OutOfMemory}![]InlayHint {
|
|
return self.hints.toOwnedSlice(self.arena);
|
|
}
|
|
};
|
|
|
|
/// `call` is the function call
|
|
/// `decl_handle` should be a function protototype
|
|
/// writes parameter hints into `builder.hints`
|
|
fn writeCallHint(builder: *Builder, call: Ast.full.Call, decl_handle: Analyser.DeclWithHandle) !void {
|
|
const tracy_zone = tracy.trace(@src());
|
|
defer tracy_zone.end();
|
|
|
|
const handle = builder.handle;
|
|
const tree = handle.tree;
|
|
|
|
const decl = decl_handle.decl;
|
|
const decl_tree = decl_handle.handle.tree;
|
|
|
|
const fn_node = switch (decl.*) {
|
|
.ast_node => |fn_node| fn_node,
|
|
else => return,
|
|
};
|
|
|
|
var buffer: [1]Ast.Node.Index = undefined;
|
|
const fn_proto = decl_tree.fullFnProto(&buffer, fn_node) orelse return;
|
|
|
|
var i: usize = 0;
|
|
var it = fn_proto.iterate(&decl_tree);
|
|
|
|
if (try builder.analyser.hasSelfParam(decl_handle.handle, fn_proto)) {
|
|
_ = ast.nextFnParam(&it);
|
|
}
|
|
|
|
while (ast.nextFnParam(&it)) |param| : (i += 1) {
|
|
if (i >= call.ast.params.len) break;
|
|
const name_token = param.name_token orelse continue;
|
|
const name = decl_tree.tokenSlice(name_token);
|
|
|
|
if (builder.config.inlay_hints_hide_redundant_param_names or builder.config.inlay_hints_hide_redundant_param_names_last_token) {
|
|
const last_param_token = tree.lastToken(call.ast.params[i]);
|
|
const param_name = tree.tokenSlice(last_param_token);
|
|
|
|
if (std.mem.eql(u8, param_name, name)) {
|
|
if (tree.firstToken(call.ast.params[i]) == last_param_token) {
|
|
if (builder.config.inlay_hints_hide_redundant_param_names)
|
|
continue;
|
|
} else {
|
|
if (builder.config.inlay_hints_hide_redundant_param_names_last_token)
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const tooltip = if (param.anytype_ellipsis3) |token|
|
|
if (token_tags[token] == .keyword_anytype) "anytype" else ""
|
|
else
|
|
offsets.nodeToSlice(decl_tree, param.type_expr);
|
|
|
|
try builder.appendParameterHint(
|
|
tree.firstToken(call.ast.params[i]),
|
|
name,
|
|
tooltip,
|
|
no_alias,
|
|
comp_time,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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: []const Ast.Node.Index, arguments: []const []const u8) !void {
|
|
const tracy_zone = tracy.trace(@src());
|
|
defer tracy_zone.end();
|
|
|
|
const handle = builder.handle;
|
|
const tree = handle.tree;
|
|
|
|
const len = @min(arguments.len, parameters.len);
|
|
for (arguments[0..len], parameters[0..len]) |arg, parameter| {
|
|
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.firstToken(parameter),
|
|
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, call: Ast.full.Call) !void {
|
|
const tracy_zone = tracy.trace(@src());
|
|
defer tracy_zone.end();
|
|
|
|
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 source_index = offsets.tokenToIndex(tree, main_tokens[call.ast.fn_expr]);
|
|
const name = offsets.tokenToSlice(tree, main_tokens[call.ast.fn_expr]);
|
|
|
|
if (try builder.analyser.lookupSymbolGlobal(handle, name, source_index)) |decl_handle| {
|
|
try writeCallHint(builder, 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 start = offsets.tokenToIndex(tree, lhsToken);
|
|
const rhs_loc = offsets.tokenToLoc(tree, rhsToken);
|
|
|
|
var held_range = try builder.arena.dupeZ(u8, handle.text[start..rhs_loc.end]);
|
|
var tokenizer = std.zig.Tokenizer.init(held_range);
|
|
|
|
// note: we have the ast node, traversing it would probably yield better results
|
|
// than trying to re-tokenize and re-parse it
|
|
if (try builder.analyser.getFieldAccessType(handle, rhs_loc.end, &tokenizer)) |result| {
|
|
const container_handle = result.unwrapped orelse result.original;
|
|
switch (container_handle.type.data) {
|
|
.other => |container_handle_node| {
|
|
if (try builder.analyser.lookupSymbolContainer(
|
|
.{ .node = container_handle_node, .handle = container_handle.handle },
|
|
tree.tokenSlice(rhsToken),
|
|
true,
|
|
)) |decl_handle| {
|
|
try writeCallHint(builder, call, decl_handle);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {
|
|
log.debug("cannot deduce fn expression with tag '{}'", .{node_tags[call.ast.fn_expr]});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn writeNodeInlayHint(
|
|
builder: *Builder,
|
|
tree: Ast,
|
|
node: Ast.Node.Index,
|
|
) error{OutOfMemory}!void {
|
|
const node_tags = tree.nodes.items(.tag);
|
|
const main_tokens = tree.nodes.items(.main_token);
|
|
|
|
const tag = node_tags[node];
|
|
|
|
switch (tag) {
|
|
.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 = tree.fullCall(¶ms, node).?;
|
|
try writeCallNodeHint(builder, call);
|
|
},
|
|
|
|
.builtin_call_two,
|
|
.builtin_call_two_comma,
|
|
.builtin_call,
|
|
.builtin_call_comma,
|
|
=> blk: {
|
|
var buffer: [2]Ast.Node.Index = undefined;
|
|
const params = ast.builtinCallParams(tree, node, &buffer).?;
|
|
|
|
if (!builder.config.inlay_hints_show_builtin or params.len <= 1) break :blk;
|
|
|
|
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, params, builtin.arguments);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// creates a list of `InlayHint`'s from the given document
|
|
/// only parameter hints are created
|
|
/// only hints in the given loc are created
|
|
pub fn writeRangeInlayHint(
|
|
arena: std.mem.Allocator,
|
|
config: Config,
|
|
analyser: *Analyser,
|
|
handle: *const DocumentStore.Handle,
|
|
loc: offsets.Loc,
|
|
hover_kind: types.MarkupKind,
|
|
) error{OutOfMemory}![]InlayHint {
|
|
var builder: Builder = .{
|
|
.arena = arena,
|
|
.analyser = analyser,
|
|
.config = &config,
|
|
.handle = handle,
|
|
.hints = .{},
|
|
.hover_kind = hover_kind,
|
|
};
|
|
|
|
const nodes = try ast.nodesAtLoc(arena, handle.tree, loc);
|
|
|
|
for (nodes) |child| {
|
|
try writeNodeInlayHint(&builder, handle.tree, child);
|
|
try ast.iterateChildrenRecursive(handle.tree, child, &builder, error{OutOfMemory}, writeNodeInlayHint);
|
|
}
|
|
|
|
return try builder.toOwnedSlice();
|
|
}
|