Merge pull request #284 from zigtools/signature_help
Added signature help request handling.
This commit is contained in:
commit
42aefd2e67
4
.github/README.md
vendored
4
.github/README.md
vendored
@ -66,14 +66,14 @@ zig build config # Configure ZLS
|
||||
<!-- If this table grows too large, then delete this one and move it all over to the Wiki page about building from source. -->
|
||||
| Option | Type | Default Value | What it Does |
|
||||
| --- | --- | --- | --- |
|
||||
| `-Ddata_version` | `string` (master or 0.7.0) | master | The data file version. This selects the files in the `src/data` folder that correspond to the Zig version being served.|
|
||||
| `-Ddata_version` | `string` (master, 0.7.0 or 0.7.1) | master | The data file version. This selects the files in the `src/data` folder that correspond to the Zig version being served.|
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can configure zls by providing a zls.json file.
|
||||
zls will look for a zls.json configuration file in multiple locations with the following priority:
|
||||
- In the local configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders#folder-list))
|
||||
- In the same directory as the executable
|
||||
- In the global configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders#folder-list))
|
||||
|
||||
The following options are currently available.
|
||||
|
||||
|
@ -64,14 +64,14 @@ zls config # Configure ZLS
|
||||
<!-- If this table grows too large, then delete this one and move it all over to the Wiki page about building from source. -->
|
||||
| Option | Type | Default Value | What it Does |
|
||||
| --- | --- | --- | --- |
|
||||
| `-Ddata_version` | `string` (master or 0.7.0) | master | The data file version. This selects the files in the `src/data` folder that correspond to the Zig version being served.|
|
||||
| `-Ddata_version` | `string` (master, 0.7.0 or 0.7.1) | master | The data file version. This selects the files in the `src/data` folder that correspond to the Zig version being served.|
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can configure zls by running `zls config`.
|
||||
zls will look for a zls.json configuration file in multiple locations with the following priority:
|
||||
- In the local configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders#folder-list))
|
||||
- In the same directory as the executable
|
||||
- In the global configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders#folder-list))
|
||||
|
||||
The following options are currently available.
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
// const build_options = @import("build_options")
|
||||
|
||||
var builder: *std.build.Builder = undefined;
|
||||
|
||||
@ -10,11 +9,10 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
|
||||
const mode = b.standardReleaseOptions();
|
||||
const exe = b.addExecutable("zls", "src/main.zig");
|
||||
|
||||
exe.addBuildOption(
|
||||
[]const u8,
|
||||
"data_version",
|
||||
b.option([]const u8, "data_version", "The data version - either 0.7.0 or master.") orelse "master",
|
||||
b.option([]const u8, "data_version", "The data version - 0.7.0, 0.7.1 or master.") orelse "master",
|
||||
);
|
||||
|
||||
exe.addPackage(.{ .name = "known-folders", .path = "src/known-folders/known-folders.zig" });
|
||||
|
@ -171,6 +171,46 @@ pub fn getFunctionSnippet(
|
||||
return buffer.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Returns true if a function has a `self` parameter
|
||||
pub fn hasSelfParam(
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
document_store: *DocumentStore,
|
||||
handle: *DocumentStore.Handle,
|
||||
func: ast.full.FnProto,
|
||||
) !bool {
|
||||
// Non-decl prototypes cannot have a self parameter.
|
||||
if (func.name_token == null) return false;
|
||||
if (func.ast.params.len == 0) return false;
|
||||
|
||||
const tree = handle.tree;
|
||||
var it = func.iterate(tree);
|
||||
const param = it.next().?;
|
||||
if (param.type_expr == 0) return false;
|
||||
|
||||
const token_starts = tree.tokens.items(.start);
|
||||
const token_data = tree.nodes.items(.data);
|
||||
const in_container = innermostContainer(handle, token_starts[func.ast.fn_token]);
|
||||
|
||||
if (try resolveTypeOfNode(document_store, arena, .{
|
||||
.node = param.type_expr,
|
||||
.handle = handle,
|
||||
})) |resolved_type| {
|
||||
if (std.meta.eql(in_container, resolved_type))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isPtrType(tree, param.type_expr)) {
|
||||
if (try resolveTypeOfNode(document_store, arena, .{
|
||||
.node = token_data[param.type_expr].rhs,
|
||||
.handle = handle,
|
||||
})) |resolved_prefix_op| {
|
||||
if (std.meta.eql(in_container, resolved_prefix_op))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Gets a function signature (keywords, name, return value)
|
||||
pub fn getVariableSignature(tree: ast.Tree, var_decl: ast.full.VarDecl) []const u8 {
|
||||
const start = offsets.tokenLocation(tree, var_decl.ast.mut_token).start;
|
||||
@ -376,22 +416,6 @@ fn isBlock(tree: ast.Tree, node: ast.Node.Index) bool {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns `true` when the given `node` is one of the call tags
|
||||
fn isCall(tree: ast.Tree, node: ast.Node.Index) bool {
|
||||
return switch (tree.nodes.items(.tag)[node]) {
|
||||
.call,
|
||||
.call_comma,
|
||||
.call_one,
|
||||
.call_one_comma,
|
||||
.async_call,
|
||||
.async_call_comma,
|
||||
.async_call_one,
|
||||
.async_call_one_comma,
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn findReturnStatementInternal(
|
||||
tree: ast.Tree,
|
||||
fn_decl: ast.full.FnProto,
|
||||
@ -946,7 +970,7 @@ pub fn resolveTypeOfNodeInternal(
|
||||
};
|
||||
|
||||
return TypeWithHandle{
|
||||
.type = .{ .data = .{ .pointer = rhs_node }, .is_type_val = false },
|
||||
.type = .{ .data = .{ .pointer = rhs_node }, .is_type_val = rhs_type.type.is_type_val },
|
||||
.handle = rhs_type.handle,
|
||||
};
|
||||
},
|
||||
@ -2250,6 +2274,21 @@ pub fn iterateSymbolsGlobal(
|
||||
return try iterateSymbolsGlobalInternal(store, arena, handle, source_index, callback, context, &use_trail);
|
||||
}
|
||||
|
||||
pub fn innermostScope(handle: DocumentStore.Handle, source_index: usize) ast.Node.Index {
|
||||
var current = handle.document_scope.scopes[0].data.container;
|
||||
if (handle.document_scope.scopes.len == 1) return current;
|
||||
|
||||
for (handle.document_scope.scopes[1..]) |scope| {
|
||||
if (source_index >= scope.range.start and source_index <= scope.range.end) {
|
||||
switch (scope.data) {
|
||||
.container, .function, .block => |node| current = node,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
pub fn innermostContainer(handle: *DocumentStore.Handle, source_index: usize) TypeWithHandle {
|
||||
var current = handle.document_scope.scopes[0].data.container;
|
||||
if (handle.document_scope.scopes.len == 1) return TypeWithHandle.typeVal(.{ .node = current, .handle = handle });
|
||||
@ -3094,19 +3133,7 @@ fn makeScopeInternal(
|
||||
.async_call_one_comma,
|
||||
=> {
|
||||
var buf: [1]ast.Node.Index = undefined;
|
||||
const call: ast.full.Call = switch (node_tag) {
|
||||
.async_call,
|
||||
.async_call_comma,
|
||||
.call,
|
||||
.call_comma,
|
||||
=> tree.callFull(node_idx),
|
||||
.async_call_one,
|
||||
.async_call_one_comma,
|
||||
.call_one,
|
||||
.call_one_comma,
|
||||
=> tree.callOne(&buf, node_idx),
|
||||
else => unreachable,
|
||||
};
|
||||
const call = callFull(tree, node_idx, &buf).?;
|
||||
|
||||
try makeScopeInternal(allocator, scopes, error_completions, enum_completions, tree, call.ast.fn_expr);
|
||||
for (call.ast.params) |param|
|
||||
|
31
src/ast.zig
31
src/ast.zig
@ -857,6 +857,21 @@ pub fn isBuiltinCall(tree: ast.Tree, node: ast.Node.Index) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isCall(tree: ast.Tree, node: ast.Node.Index) bool {
|
||||
return switch (tree.nodes.items(.tag)[node]) {
|
||||
.call,
|
||||
.call_comma,
|
||||
.call_one,
|
||||
.call_one_comma,
|
||||
.async_call,
|
||||
.async_call_comma,
|
||||
.async_call_one,
|
||||
.async_call_one_comma,
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fnProto(tree: ast.Tree, node: ast.Node.Index, buf: *[1]ast.Node.Index) ?ast.full.FnProto {
|
||||
return switch (tree.nodes.items(.tag)[node]) {
|
||||
.fn_proto => tree.fnProto(node),
|
||||
@ -867,3 +882,19 @@ pub fn fnProto(tree: ast.Tree, node: ast.Node.Index, buf: *[1]ast.Node.Index) ?a
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callFull(tree: ast.Tree, node: ast.Node.Index, buf: *[1]ast.Node.Index) ?ast.full.Call {
|
||||
return switch (tree.nodes.items(.tag)[node]) {
|
||||
.async_call,
|
||||
.async_call_comma,
|
||||
.call,
|
||||
.call_comma,
|
||||
=> tree.callFull(node),
|
||||
.async_call_one,
|
||||
.async_call_one_comma,
|
||||
.call_one,
|
||||
.call_one_comma,
|
||||
=> tree.callOne(buf, node),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
1861
src/data/0.7.1.zig
Normal file
1861
src/data/0.7.1.zig
Normal file
File diff suppressed because it is too large
Load Diff
@ -41,6 +41,7 @@ console.log(
|
||||
signature: []const u8,
|
||||
snippet: []const u8,
|
||||
documentation: []const u8,
|
||||
arguments: []const []const u8,
|
||||
};
|
||||
|
||||
pub const builtins = [_]Builtin{` +
|
||||
@ -49,11 +50,13 @@ pub const builtins = [_]Builtin{` +
|
||||
const first_paren_idx = builtin.code.indexOf('(');
|
||||
var snippet = builtin.code.substr(0, first_paren_idx + 1);
|
||||
var rest = builtin.code.substr(first_paren_idx + 1);
|
||||
var args = [];
|
||||
|
||||
if (rest[0] == ')') {
|
||||
snippet += ')';
|
||||
} else {
|
||||
snippet += "${1:"
|
||||
args.push("");
|
||||
|
||||
var arg_idx = 2;
|
||||
var paren_depth = 1;
|
||||
@ -69,10 +72,12 @@ pub const builtins = [_]Builtin{` +
|
||||
}
|
||||
} else if (char == '"') {
|
||||
snippet += "\\\"";
|
||||
args[args.length - 1] += "\\\"";
|
||||
continue;
|
||||
} else if (char == ',' && paren_depth == 1) {
|
||||
snippet += "}, ${" + arg_idx + ':';
|
||||
arg_idx += 1;
|
||||
args.push("");
|
||||
skip_space = true;
|
||||
continue;
|
||||
} else if (char == ' ' && skip_space) {
|
||||
@ -80,6 +85,7 @@ pub const builtins = [_]Builtin{` +
|
||||
}
|
||||
|
||||
snippet += char;
|
||||
args[args.length - 1] += char;
|
||||
skip_space = false;
|
||||
}
|
||||
}
|
||||
@ -89,6 +95,9 @@ pub const builtins = [_]Builtin{` +
|
||||
.signature = "${builtin.code.replaceAll('"', "\\\"")}",
|
||||
.snippet = "${snippet}",
|
||||
.documentation =
|
||||
\\\\${builtin.documentation.split('\n').join("\n \\\\") + '\n'} },`;
|
||||
\\\\${builtin.documentation.split('\n').join("\n \\\\")}
|
||||
,
|
||||
.arguments = &.{${args.map(x => "\n \"" + x + "\"").join(",") + ((args.length > 0) ? ",\n " : "")}},
|
||||
},`;
|
||||
}).join('\n') + "\n};\n"
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
142
src/main.zig
142
src/main.zig
@ -124,6 +124,9 @@ const edit_not_applied_response =
|
||||
const no_completions_response =
|
||||
\\,"result":{"isIncomplete":false,"items":[]}}
|
||||
;
|
||||
const no_signatures_response =
|
||||
\\,"result":{"signatures":[]}}
|
||||
;
|
||||
const no_semantic_tokens_response =
|
||||
\\,"result":{"data":[]}}
|
||||
;
|
||||
@ -326,6 +329,7 @@ fn typeToCompletion(
|
||||
null,
|
||||
orig_handle,
|
||||
type_handle.type.is_type_val,
|
||||
null,
|
||||
config,
|
||||
);
|
||||
},
|
||||
@ -336,6 +340,7 @@ fn typeToCompletion(
|
||||
field_access.unwrapped,
|
||||
orig_handle,
|
||||
type_handle.type.is_type_val,
|
||||
null,
|
||||
config,
|
||||
),
|
||||
.primitive => {},
|
||||
@ -349,13 +354,13 @@ fn nodeToCompletion(
|
||||
unwrapped: ?analysis.TypeWithHandle,
|
||||
orig_handle: *DocumentStore.Handle,
|
||||
is_type_val: bool,
|
||||
parent_is_type_val: ?bool,
|
||||
config: Config,
|
||||
) error{OutOfMemory}!void {
|
||||
const node = node_handle.node;
|
||||
const handle = node_handle.handle;
|
||||
const tree = handle.tree;
|
||||
const node_tags = tree.nodes.items(.tag);
|
||||
const datas = tree.nodes.items(.data);
|
||||
const token_tags = tree.tokens.items(.tag);
|
||||
|
||||
const doc_kind: types.MarkupContent.Kind = if (client_capabilities.completion_doc_supports_md)
|
||||
@ -382,8 +387,17 @@ fn nodeToCompletion(
|
||||
.config = &config,
|
||||
.arena = arena,
|
||||
.orig_handle = orig_handle,
|
||||
.parent_is_type_val = is_type_val,
|
||||
};
|
||||
try analysis.iterateSymbolsContainer(&document_store, arena, node_handle, orig_handle, declToCompletion, context, !is_type_val);
|
||||
try analysis.iterateSymbolsContainer(
|
||||
&document_store,
|
||||
arena,
|
||||
node_handle,
|
||||
orig_handle,
|
||||
declToCompletion,
|
||||
context,
|
||||
!is_type_val,
|
||||
);
|
||||
}
|
||||
|
||||
if (is_type_val) return;
|
||||
@ -399,38 +413,9 @@ fn nodeToCompletion(
|
||||
const func = analysis.fnProto(tree, node, &buf).?;
|
||||
if (func.name_token) |name_token| {
|
||||
const use_snippets = config.enable_snippets and client_capabilities.supports_snippets;
|
||||
|
||||
const insert_text = if (use_snippets) blk: {
|
||||
// TODO Also check if we are dot accessing from a type val and dont skip in that case.
|
||||
const skip_self_param = if (func.ast.params.len > 0) param_check: {
|
||||
const in_container = analysis.innermostContainer(handle, tree.tokens.items(.start)[func.ast.fn_token]);
|
||||
|
||||
var it = func.iterate(tree);
|
||||
const param = it.next().?;
|
||||
|
||||
if (param.type_expr == 0) break :param_check false;
|
||||
|
||||
if (try analysis.resolveTypeOfNode(&document_store, arena, .{
|
||||
.node = param.type_expr,
|
||||
.handle = handle,
|
||||
})) |resolved_type| {
|
||||
if (std.meta.eql(in_container, resolved_type))
|
||||
break :param_check true;
|
||||
}
|
||||
|
||||
if (analysis.isPtrType(tree, param.type_expr)) {
|
||||
if (try analysis.resolveTypeOfNode(&document_store, arena, .{
|
||||
.node = datas[param.type_expr].rhs,
|
||||
.handle = handle,
|
||||
})) |resolved_prefix_op| {
|
||||
if (std.meta.eql(in_container, resolved_prefix_op))
|
||||
break :param_check true;
|
||||
}
|
||||
}
|
||||
|
||||
break :param_check false;
|
||||
} else false;
|
||||
|
||||
const skip_self_param = !(parent_is_type_val orelse true) and
|
||||
try analysis.hasSelfParam(arena, &document_store, handle, func);
|
||||
break :blk try analysis.getFunctionSnippet(&arena.allocator, tree, func, skip_self_param);
|
||||
} else tree.tokenSlice(func.name_token.?);
|
||||
|
||||
@ -567,7 +552,7 @@ fn nodeToCompletion(
|
||||
}
|
||||
}
|
||||
|
||||
fn identifierFromPosition(pos_index: usize, handle: DocumentStore.Handle) []const u8 {
|
||||
pub fn identifierFromPosition(pos_index: usize, handle: DocumentStore.Handle) []const u8 {
|
||||
const text = handle.document.text;
|
||||
|
||||
if (pos_index + 1 >= text.len) return &[0]u8{};
|
||||
@ -754,7 +739,11 @@ fn hoverDefinitionBuiltin(arena: *std.heap.ArenaAllocator, id: types.RequestId,
|
||||
.id = id,
|
||||
.result = .{
|
||||
.Hover = .{
|
||||
.contents = .{ .value = try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```\n{s}", .{ builtin.signature, builtin.documentation }) },
|
||||
.contents = .{ .value = try std.fmt.allocPrint(
|
||||
&arena.allocator,
|
||||
"```zig\n{s}\n```\n{s}",
|
||||
.{ builtin.signature, builtin.documentation },
|
||||
) },
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -945,13 +934,6 @@ fn referencesDefinitionLabel(arena: *std.heap.ArenaAllocator, id: types.RequestI
|
||||
});
|
||||
}
|
||||
|
||||
const DeclToCompletionContext = struct {
|
||||
completions: *std.ArrayList(types.CompletionItem),
|
||||
config: *const Config,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
orig_handle: *DocumentStore.Handle,
|
||||
};
|
||||
|
||||
fn hasComment(tree: ast.Tree, start_token: ast.TokenIndex, end_token: ast.TokenIndex) bool {
|
||||
const token_starts = tree.tokens.items(.start);
|
||||
|
||||
@ -961,6 +943,14 @@ fn hasComment(tree: ast.Tree, start_token: ast.TokenIndex, end_token: ast.TokenI
|
||||
return std.mem.indexOf(u8, tree.source[start..end], "//") != null;
|
||||
}
|
||||
|
||||
const DeclToCompletionContext = struct {
|
||||
completions: *std.ArrayList(types.CompletionItem),
|
||||
config: *const Config,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
orig_handle: *DocumentStore.Handle,
|
||||
parent_is_type_val: ?bool = null,
|
||||
};
|
||||
|
||||
fn declToCompletion(context: DeclToCompletionContext, decl_handle: analysis.DeclWithHandle) !void {
|
||||
const tree = decl_handle.handle.tree;
|
||||
switch (decl_handle.decl.*) {
|
||||
@ -971,6 +961,7 @@ fn declToCompletion(context: DeclToCompletionContext, decl_handle: analysis.Decl
|
||||
null,
|
||||
context.orig_handle,
|
||||
false,
|
||||
context.parent_is_type_val,
|
||||
context.config.*,
|
||||
),
|
||||
.param_decl => |param| {
|
||||
@ -1280,7 +1271,8 @@ fn initializeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req:
|
||||
},
|
||||
.capabilities = .{
|
||||
.signatureHelpProvider = .{
|
||||
.triggerCharacters = &[_][]const u8{ "(", "," },
|
||||
.triggerCharacters = &.{"("},
|
||||
.retriggerCharacters = &.{","},
|
||||
},
|
||||
.textDocumentSync = .Full,
|
||||
.renameProvider = true,
|
||||
@ -1398,13 +1390,20 @@ fn semanticTokensFullHandler(arena: *std.heap.ArenaAllocator, id: types.RequestI
|
||||
}
|
||||
}
|
||||
|
||||
fn completionHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.Completion, config: Config) !void {
|
||||
fn completionHandler(
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
id: types.RequestId,
|
||||
req: requests.Completion,
|
||||
config: Config,
|
||||
) !void {
|
||||
const handle = document_store.getHandle(req.params.textDocument.uri) orelse {
|
||||
logger.warn("Trying to complete in non existent document {s}", .{req.params.textDocument.uri});
|
||||
return try respondGeneric(id, no_completions_response);
|
||||
};
|
||||
|
||||
if (req.params.position.character >= 0) {
|
||||
if (req.params.position.character == 0)
|
||||
return try respondGeneric(id, no_completions_response);
|
||||
|
||||
const doc_position = try offsets.documentPosition(handle.document, req.params.position, offset_encoding);
|
||||
const pos_context = try analysis.documentPositionContext(arena, handle.document, doc_position);
|
||||
const use_snippets = config.enable_snippets and client_capabilities.supports_snippets;
|
||||
@ -1418,19 +1417,50 @@ fn completionHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req:
|
||||
.label => try completeLabel(arena, id, doc_position.absolute_index, handle, config),
|
||||
else => try respondGeneric(id, no_completions_response),
|
||||
}
|
||||
} else {
|
||||
try respondGeneric(id, no_completions_response);
|
||||
}
|
||||
|
||||
fn signatureHelpHandler(
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
id: types.RequestId,
|
||||
req: requests.SignatureHelp,
|
||||
config: Config,
|
||||
) !void {
|
||||
const getSignatureInfo = @import("signature_help.zig").getSignatureInfo;
|
||||
const handle = document_store.getHandle(req.params.textDocument.uri) orelse {
|
||||
logger.warn("Trying to get signature help in non existent document {s}", .{req.params.textDocument.uri});
|
||||
return try respondGeneric(id, no_signatures_response);
|
||||
};
|
||||
|
||||
if (req.params.position.character == 0)
|
||||
return try respondGeneric(id, no_signatures_response);
|
||||
|
||||
const doc_position = try offsets.documentPosition(handle.document, req.params.position, offset_encoding);
|
||||
if (try getSignatureInfo(
|
||||
&document_store,
|
||||
arena,
|
||||
handle,
|
||||
doc_position.absolute_index,
|
||||
data,
|
||||
)) |sig_info| {
|
||||
return try send(arena, types.Response{
|
||||
.id = id,
|
||||
.result = .{ .SignatureHelp = .{
|
||||
.signatures = &[1]types.SignatureInformation{sig_info},
|
||||
.activeSignature = 0,
|
||||
.activeParameter = sig_info.activeParameter,
|
||||
} },
|
||||
});
|
||||
}
|
||||
return try respondGeneric(id, no_signatures_response);
|
||||
}
|
||||
|
||||
fn signatureHelperHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, config: Config) !void {
|
||||
// TODO Implement this
|
||||
try respondGeneric(id,
|
||||
\\,"result":{"signatures":[]}}
|
||||
);
|
||||
}
|
||||
|
||||
fn gotoHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.GotoDefinition, config: Config, resolve_alias: bool) !void {
|
||||
fn gotoHandler(
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
id: types.RequestId,
|
||||
req: requests.GotoDefinition,
|
||||
config: Config,
|
||||
resolve_alias: bool,
|
||||
) !void {
|
||||
const handle = document_store.getHandle(req.params.textDocument.uri) orelse {
|
||||
logger.warn("Trying to go to definition in non existent document {s}", .{req.params.textDocument.uri});
|
||||
return try respondGeneric(id, null_result_response);
|
||||
@ -1612,7 +1642,7 @@ fn processJsonRpc(arena: *std.heap.ArenaAllocator, parser: *std.json.Parser, jso
|
||||
.{ "textDocument/didClose", requests.CloseDocument, closeDocumentHandler },
|
||||
.{ "textDocument/semanticTokens/full", requests.SemanticTokensFull, semanticTokensFullHandler },
|
||||
.{ "textDocument/completion", requests.Completion, completionHandler },
|
||||
.{ "textDocument/signatureHelp", void, signatureHelperHandler },
|
||||
.{ "textDocument/signatureHelp", requests.SignatureHelp, signatureHelpHandler },
|
||||
.{ "textDocument/definition", requests.GotoDefinition, gotoDefinitionHandler },
|
||||
.{ "textDocument/typeDefinition", requests.GotoDefinition, gotoDefinitionHandler },
|
||||
.{ "textDocument/implementation", requests.GotoDefinition, gotoDefinitionHandler },
|
||||
|
@ -18,12 +18,22 @@ fn Default(comptime T: type, comptime default_value: T) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ErrorUnwrappedReturnOf(comptime func: anytype) type {
|
||||
return switch (@typeInfo(@TypeOf(func))) {
|
||||
.Fn, .BoundFn => |fn_info| switch (@typeInfo(fn_info.return_type.?)) {
|
||||
.ErrorUnion => |err_union| err_union.payload,
|
||||
else => |T| return T,
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn Transform(comptime Original: type, comptime transform_fn: anytype) type {
|
||||
return struct {
|
||||
pub const original_type = Original;
|
||||
pub const transform = transform_fn;
|
||||
|
||||
value: @TypeOf(transform(@as(Original, undefined))),
|
||||
value: ErrorUnwrappedReturnOf(transform_fn),
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,16 +111,25 @@ fn fromDynamicTreeInternal(arena: *std.heap.ArenaAllocator, value: std.json.Valu
|
||||
}
|
||||
} else if (T == std.json.Value) {
|
||||
out.* = value;
|
||||
} else {
|
||||
switch (T) {
|
||||
} else if (comptime std.meta.trait.is(.Enum)(T)) {
|
||||
const info = @typeInfo(T).Enum;
|
||||
if (info.layout != .Auto)
|
||||
@compileError("Only auto layout enums are allowed");
|
||||
|
||||
const TagType = info.tag_type;
|
||||
if (value != .Integer) return error.MalformedJson;
|
||||
out.* = std.meta.intToEnum(
|
||||
T,
|
||||
std.math.cast(TagType, value.Integer) catch return error.MalformedJson,
|
||||
) catch return error.MalformedJson;
|
||||
} else if (comptime std.meta.trait.is(.Int)(T)) {
|
||||
if (value != .Integer) return error.MalformedJson;
|
||||
out.* = std.math.cast(T, value.Integer) catch return error.MalformedJson;
|
||||
} else switch (T) {
|
||||
bool => {
|
||||
if (value != .Bool) return error.MalformedJson;
|
||||
out.* = value.Bool;
|
||||
},
|
||||
i64 => {
|
||||
if (value != .Integer) return error.MalformedJson;
|
||||
out.* = value.Integer;
|
||||
},
|
||||
f64 => {
|
||||
if (value != .Float) return error.MalformedJson;
|
||||
out.* = value.Float;
|
||||
@ -121,7 +140,6 @@ fn fromDynamicTreeInternal(arena: *std.heap.ArenaAllocator, value: std.json.Valu
|
||||
},
|
||||
else => @compileError("Invalid type " ++ @typeName(T)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fromDynamicTree(arena: *std.heap.ArenaAllocator, comptime T: type, value: std.json.Value) error{ MalformedJson, OutOfMemory }!T {
|
||||
@ -204,6 +222,23 @@ const TextDocumentIdentifierPositionRequest = struct {
|
||||
},
|
||||
};
|
||||
|
||||
pub const SignatureHelp = struct {
|
||||
params: struct {
|
||||
textDocument: TextDocumentIdentifier,
|
||||
position: types.Position,
|
||||
context: ?struct {
|
||||
triggerKind: enum {
|
||||
invoked = 1,
|
||||
trigger_character = 2,
|
||||
content_change = 3,
|
||||
},
|
||||
triggerCharacter: ?[]const u8,
|
||||
isRetrigger: bool,
|
||||
activeSignatureHelp: ?types.SignatureHelp,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub const Completion = TextDocumentIdentifierPositionRequest;
|
||||
pub const GotoDefinition = TextDocumentIdentifierPositionRequest;
|
||||
pub const GotoDeclaration = TextDocumentIdentifierPositionRequest;
|
||||
|
374
src/signature_help.zig
Normal file
374
src/signature_help.zig
Normal file
@ -0,0 +1,374 @@
|
||||
const std = @import("std");
|
||||
const analysis = @import("analysis.zig");
|
||||
const offsets = @import("offsets.zig");
|
||||
const DocumentStore = @import("document_store.zig");
|
||||
const types = @import("types.zig");
|
||||
const ast = std.zig.ast;
|
||||
const Token = std.zig.Token;
|
||||
const identifierFromPosition = @import("main.zig").identifierFromPosition;
|
||||
usingnamespace @import("ast.zig");
|
||||
|
||||
fn fnProtoToSignatureInfo(
|
||||
document_store: *DocumentStore,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
commas: u32,
|
||||
skip_self_param: bool,
|
||||
handle: *DocumentStore.Handle,
|
||||
fn_node: ast.Node.Index,
|
||||
proto: ast.full.FnProto,
|
||||
) !types.SignatureInformation {
|
||||
const ParameterInformation = types.SignatureInformation.ParameterInformation;
|
||||
|
||||
const tree = handle.tree;
|
||||
const token_starts = tree.tokens.items(.start);
|
||||
const alloc = &arena.allocator;
|
||||
const label = analysis.getFunctionSignature(tree, proto);
|
||||
const proto_comments = types.MarkupContent{ .value = if (try analysis.getDocComments(
|
||||
alloc,
|
||||
tree,
|
||||
fn_node,
|
||||
.Markdown,
|
||||
)) |dc|
|
||||
dc
|
||||
else
|
||||
"" };
|
||||
|
||||
const arg_idx = if (skip_self_param) blk: {
|
||||
const has_self_param = try analysis.hasSelfParam(arena, document_store, handle, proto);
|
||||
break :blk commas + @boolToInt(has_self_param);
|
||||
} else commas;
|
||||
|
||||
var params = std.ArrayListUnmanaged(ParameterInformation){};
|
||||
var param_it = proto.iterate(tree);
|
||||
while (param_it.next()) |param| {
|
||||
const param_comments = if (param.first_doc_comment) |dc|
|
||||
types.MarkupContent{ .value = try analysis.collectDocComments(
|
||||
alloc,
|
||||
tree,
|
||||
dc,
|
||||
.Markdown,
|
||||
) }
|
||||
else
|
||||
null;
|
||||
|
||||
var param_label_start: usize = 0;
|
||||
var param_label_end: usize = 0;
|
||||
if (param.comptime_noalias) |cn| {
|
||||
param_label_start = token_starts[cn];
|
||||
param_label_end = param_label_start + tree.tokenSlice(cn).len;
|
||||
}
|
||||
if (param.name_token) |nt| {
|
||||
if (param_label_start == 0)
|
||||
param_label_start = token_starts[nt];
|
||||
param_label_end = token_starts[nt] + tree.tokenSlice(nt).len;
|
||||
}
|
||||
if (param.anytype_ellipsis3) |ae| {
|
||||
if (param_label_start == 0)
|
||||
param_label_start = token_starts[ae];
|
||||
param_label_end = token_starts[ae] + tree.tokenSlice(ae).len;
|
||||
}
|
||||
if (param.type_expr != 0) {
|
||||
if (param_label_start == 0)
|
||||
param_label_start = token_starts[tree.firstToken(param.type_expr)];
|
||||
|
||||
const last_param_tok = lastToken(tree, param.type_expr);
|
||||
param_label_end = token_starts[last_param_tok] + tree.tokenSlice(last_param_tok).len;
|
||||
}
|
||||
const param_label = tree.source[param_label_start..param_label_end];
|
||||
try params.append(alloc, .{
|
||||
.label = param_label,
|
||||
.documentation = param_comments,
|
||||
});
|
||||
}
|
||||
return types.SignatureInformation{
|
||||
.label = label,
|
||||
.documentation = proto_comments,
|
||||
.parameters = params.items,
|
||||
.activeParameter = arg_idx,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSignatureInfo(
|
||||
document_store: *DocumentStore,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
handle: *DocumentStore.Handle,
|
||||
absolute_index: usize,
|
||||
comptime data: type,
|
||||
) !?types.SignatureInformation {
|
||||
const innermost_block = analysis.innermostScope(handle.*, absolute_index);
|
||||
const tree = handle.tree;
|
||||
const token_tags = tree.tokens.items(.tag);
|
||||
const token_starts = tree.tokens.items(.start);
|
||||
|
||||
// Use the innermost scope to determine the earliest token we would need
|
||||
// to scan up to find a function or buitin call
|
||||
const first_token = tree.firstToken(innermost_block);
|
||||
// We start by finding the token that includes the current cursor position
|
||||
const last_token = blk: {
|
||||
if (token_starts[0] >= absolute_index)
|
||||
return null;
|
||||
|
||||
var i: u32 = 1;
|
||||
while (i < token_tags.len) : (i += 1) {
|
||||
if (token_starts[i] >= absolute_index) {
|
||||
break :blk i - 1;
|
||||
}
|
||||
}
|
||||
break :blk @truncate(u32, token_tags.len - 1);
|
||||
};
|
||||
|
||||
// We scan the tokens from last to first, adding and removing open and close
|
||||
// delimiter tokens to a stack, while keeping track of commas corresponding
|
||||
// to each of the blocks in a stack.
|
||||
// When we encounter a dangling left parenthesis token, we continue scanning
|
||||
// backwards for a compatible possible function call lhs expression or a
|
||||
// single builtin token.
|
||||
// When a function call expression is detected, it is resolved to a declaration
|
||||
// or a function type and the resulting function prototype is converted into
|
||||
// a signature information object.
|
||||
const StackSymbol = enum {
|
||||
l_paren,
|
||||
r_paren,
|
||||
l_brace,
|
||||
r_brace,
|
||||
l_bracket,
|
||||
r_bracket,
|
||||
|
||||
fn from(tag: Token.Tag) @This() {
|
||||
return switch (tag) {
|
||||
.l_paren => .l_paren,
|
||||
.r_paren => .r_paren,
|
||||
.l_brace => .l_brace,
|
||||
.r_brace => .r_brace,
|
||||
.l_bracket => .l_bracket,
|
||||
.r_bracket => .r_bracket,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
};
|
||||
const alloc = &arena.allocator;
|
||||
var symbol_stack = try std.ArrayListUnmanaged(StackSymbol).initCapacity(alloc, 8);
|
||||
var curr_commas: u32 = 0;
|
||||
var comma_stack = try std.ArrayListUnmanaged(u32).initCapacity(alloc, 4);
|
||||
var curr_token = last_token;
|
||||
while (curr_token >= first_token and curr_token != 0) : (curr_token -= 1) {
|
||||
switch (token_tags[curr_token]) {
|
||||
.comma => curr_commas += 1,
|
||||
.l_brace => {
|
||||
curr_commas = comma_stack.popOrNull() orelse 0;
|
||||
if (symbol_stack.items.len != 0) {
|
||||
const peek_sym = symbol_stack.items[symbol_stack.items.len - 1];
|
||||
switch (peek_sym) {
|
||||
.r_brace => {
|
||||
_ = symbol_stack.pop();
|
||||
continue;
|
||||
},
|
||||
.r_bracket, .r_paren => {
|
||||
return null;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
try symbol_stack.append(alloc, .l_brace);
|
||||
},
|
||||
.l_bracket => {
|
||||
curr_commas = comma_stack.popOrNull() orelse 0;
|
||||
if (symbol_stack.items.len != 0) {
|
||||
const peek_sym = symbol_stack.items[symbol_stack.items.len - 1];
|
||||
switch (peek_sym) {
|
||||
.r_bracket => {
|
||||
_ = symbol_stack.pop();
|
||||
continue;
|
||||
},
|
||||
.r_brace, .r_paren => {
|
||||
return null;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
try symbol_stack.append(alloc, .l_bracket);
|
||||
},
|
||||
.l_paren => {
|
||||
const paren_commas = curr_commas;
|
||||
curr_commas = comma_stack.popOrNull() orelse 0;
|
||||
if (symbol_stack.items.len != 0) {
|
||||
const peek_sym = symbol_stack.items[symbol_stack.items.len - 1];
|
||||
switch (peek_sym) {
|
||||
.r_paren => {
|
||||
_ = symbol_stack.pop();
|
||||
continue;
|
||||
},
|
||||
.r_brace, .r_bracket => {
|
||||
return null;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a function expression or a builtin identifier
|
||||
if (curr_token == first_token)
|
||||
return null;
|
||||
|
||||
const expr_last_token = curr_token - 1;
|
||||
if (token_tags[expr_last_token] == .builtin) {
|
||||
// Builtin token, find the builtin and construct signature information.
|
||||
for (data.builtins) |builtin| {
|
||||
if (std.mem.eql(u8, builtin.name, tree.tokenSlice(expr_last_token))) {
|
||||
const param_infos = try alloc.alloc(
|
||||
types.SignatureInformation.ParameterInformation,
|
||||
builtin.arguments.len,
|
||||
);
|
||||
for (param_infos) |*info, i| {
|
||||
info.* = .{
|
||||
.label = builtin.arguments[i],
|
||||
.documentation = null,
|
||||
};
|
||||
}
|
||||
return types.SignatureInformation{
|
||||
.label = builtin.signature,
|
||||
.documentation = .{
|
||||
.value = builtin.documentation,
|
||||
},
|
||||
.parameters = param_infos,
|
||||
.activeParameter = paren_commas,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Scan for a function call lhs expression.
|
||||
var state: union(enum) {
|
||||
any,
|
||||
in_bracket: u32,
|
||||
in_paren: u32,
|
||||
} = .any;
|
||||
var i = expr_last_token;
|
||||
const expr_first_token = while (i > first_token) : (i -= 1) {
|
||||
switch (state) {
|
||||
.in_bracket => |*count| if (token_tags[i] == .r_bracket) {
|
||||
count.* += 1;
|
||||
} else if (token_tags[i] == .l_bracket) {
|
||||
count.* -= 1;
|
||||
if (count.* == 0)
|
||||
state = .any;
|
||||
},
|
||||
.in_paren => |*count| if (token_tags[i] == .r_paren) {
|
||||
count.* += 1;
|
||||
} else if (token_tags[i] == .l_paren) {
|
||||
count.* -= 1;
|
||||
if (count.* == 0)
|
||||
state = .any;
|
||||
},
|
||||
.any => switch (token_tags[i]) {
|
||||
.r_bracket => state = .{ .in_bracket = 1 },
|
||||
.r_paren => state = .{ .in_paren = 1 },
|
||||
.identifier,
|
||||
.period,
|
||||
.period_asterisk,
|
||||
=> {},
|
||||
else => break i + 1,
|
||||
},
|
||||
}
|
||||
} else first_token + 1;
|
||||
if (state != .any or expr_first_token > expr_last_token) {
|
||||
try symbol_stack.append(alloc, .l_paren);
|
||||
continue;
|
||||
}
|
||||
const expr_start = token_starts[expr_first_token];
|
||||
const last_token_slice = tree.tokenSlice(expr_last_token);
|
||||
const expr_end = token_starts[expr_last_token] + last_token_slice.len;
|
||||
const expr_source = tree.source[expr_start..expr_end];
|
||||
// Resolve the expression.
|
||||
var tokenizer = std.zig.Tokenizer.init(expr_source);
|
||||
if (try analysis.getFieldAccessType(
|
||||
document_store,
|
||||
arena,
|
||||
handle,
|
||||
expr_start,
|
||||
&tokenizer,
|
||||
)) |result| {
|
||||
const type_handle = result.unwrapped orelse result.original;
|
||||
var node = switch (type_handle.type.data) {
|
||||
.other => |n| n,
|
||||
else => {
|
||||
try symbol_stack.append(alloc, .l_paren);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
var buf: [1]ast.Node.Index = undefined;
|
||||
if (fnProto(type_handle.handle.tree, node, &buf)) |proto| {
|
||||
return try fnProtoToSignatureInfo(
|
||||
document_store,
|
||||
arena,
|
||||
paren_commas,
|
||||
false,
|
||||
type_handle.handle,
|
||||
node,
|
||||
proto,
|
||||
);
|
||||
}
|
||||
|
||||
const name = identifierFromPosition(expr_end - 1, handle.*);
|
||||
if (name.len == 0) {
|
||||
try symbol_stack.append(alloc, .l_paren);
|
||||
continue;
|
||||
}
|
||||
|
||||
const skip_self_param = !type_handle.type.is_type_val;
|
||||
const decl_handle = (try analysis.lookupSymbolContainer(
|
||||
document_store,
|
||||
arena,
|
||||
.{ .node = node, .handle = type_handle.handle },
|
||||
name,
|
||||
true,
|
||||
)) orelse {
|
||||
try symbol_stack.append(alloc, .l_paren);
|
||||
continue;
|
||||
};
|
||||
var res_handle = decl_handle.handle;
|
||||
node = switch (decl_handle.decl.*) {
|
||||
.ast_node => |n| n,
|
||||
else => {
|
||||
try symbol_stack.append(alloc, .l_paren);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
if (try analysis.resolveVarDeclAlias(
|
||||
document_store,
|
||||
arena,
|
||||
.{ .node = node, .handle = decl_handle.handle },
|
||||
)) |resolved| {
|
||||
switch (resolved.decl.*) {
|
||||
.ast_node => |n| {
|
||||
res_handle = resolved.handle;
|
||||
node = n;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (fnProto(res_handle.tree, node, &buf)) |proto| {
|
||||
return try fnProtoToSignatureInfo(
|
||||
document_store,
|
||||
arena,
|
||||
paren_commas,
|
||||
skip_self_param,
|
||||
res_handle,
|
||||
node,
|
||||
proto,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
.r_brace, .r_paren, .r_bracket => |tag| {
|
||||
try comma_stack.append(alloc, curr_commas);
|
||||
curr_commas = 0;
|
||||
try symbol_stack.append(alloc, StackSymbol.from(tag));
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
@ -31,6 +31,7 @@ pub const Hover = struct {
|
||||
|
||||
/// Params of a response (result)
|
||||
pub const ResponseParams = union(enum) {
|
||||
SignatureHelp: SignatureHelp,
|
||||
CompletionList: CompletionList,
|
||||
Location: Location,
|
||||
Hover: Hover,
|
||||
@ -289,12 +290,32 @@ pub const WorkspaceFolder = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub const SignatureInformation = struct {
|
||||
pub const ParameterInformation = struct {
|
||||
// TODO Can also send a pair of encoded offsets
|
||||
label: []const u8,
|
||||
documentation: ?MarkupContent,
|
||||
};
|
||||
|
||||
label: []const u8,
|
||||
documentation: ?MarkupContent,
|
||||
parameters: ?[]const ParameterInformation,
|
||||
activeParameter: ?u32,
|
||||
};
|
||||
|
||||
pub const SignatureHelp = struct {
|
||||
signatures: ?[]const SignatureInformation,
|
||||
activeSignature: ?u32,
|
||||
activeParameter: ?u32,
|
||||
};
|
||||
|
||||
// Only includes options we set in our initialize result.
|
||||
const InitializeResult = struct {
|
||||
offsetEncoding: []const u8,
|
||||
capabilities: struct {
|
||||
signatureHelpProvider: struct {
|
||||
triggerCharacters: []const []const u8,
|
||||
retriggerCharacters: []const []const u8,
|
||||
},
|
||||
textDocumentSync: enum {
|
||||
None = 0,
|
||||
|
@ -153,6 +153,6 @@ test "Requesting utf-8 offset encoding" {
|
||||
|
||||
try sendRequest(shutdown_message, process);
|
||||
try consumeOutputAndWait(process, .{
|
||||
\\{"jsonrpc":"2.0","id":0,"result":{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"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"}}}
|
||||
\\{"jsonrpc":"2.0","id":0,"result":{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["("],"retriggerCharacters":[","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"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"}}}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user