Skip to content

Commit

Permalink
improve completions on functions
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Techatrix committed Feb 11, 2024
1 parent 169ce86 commit 2489add
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 137 deletions.
2 changes: 2 additions & 0 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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| {
Expand Down
163 changes: 114 additions & 49 deletions src/analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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, '}');
Expand All @@ -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(
Expand Down
129 changes: 90 additions & 39 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 2489add

Please sign in to comment.