Started signature help implementation

This commit is contained in:
Alexandros Naskos 2021-04-01 14:14:49 +03:00
parent f45a934f50
commit 9cc8085699
No known key found for this signature in database
GPG Key ID: 02BF2E72B0EA32D2
6 changed files with 178 additions and 71 deletions

View File

@ -10,7 +10,6 @@ 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",

View File

@ -376,22 +376,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,
@ -3094,19 +3078,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|

View File

@ -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,
};
}

View File

@ -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":[]}}
;
@ -1280,7 +1283,8 @@ fn initializeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req:
},
.capabilities = .{
.signatureHelpProvider = .{
.triggerCharacters = &[_][]const u8{ "(", "," },
.triggerCharacters = &.{"("},
.retriggerCharacters = &.{","},
},
.textDocumentSync = .Full,
.renameProvider = true,
@ -1398,39 +1402,85 @@ 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) {
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;
if (req.params.position.character == 0)
return try respondGeneric(id, no_completions_response);
switch (pos_context) {
.builtin => try completeBuiltin(arena, id, config),
.var_access, .empty => try completeGlobal(arena, id, doc_position.absolute_index, handle, config),
.field_access => |range| try completeFieldAccess(arena, id, handle, doc_position, range, config),
.global_error_set => try completeError(arena, id, handle, config),
.enum_literal => try completeDot(arena, id, handle, config),
.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);
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;
switch (pos_context) {
.builtin => try completeBuiltin(arena, id, config),
.var_access, .empty => try completeGlobal(arena, id, doc_position.absolute_index, handle, config),
.field_access => |range| try completeFieldAccess(arena, id, handle, doc_position, range, config),
.global_error_set => try completeError(arena, id, handle, config),
.enum_literal => try completeDot(arena, id, handle, config),
.label => try completeLabel(arena, id, doc_position.absolute_index, handle, config),
else => try respondGeneric(id, no_completions_response),
}
}
fn signatureHelperHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, config: Config) !void {
fn signatureHelperHandler(
arena: *std.heap.ArenaAllocator,
id: types.RequestId,
req: requests.SignatureHelp,
config: Config,
) !void {
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);
const last_idx = doc_position.absolute_index;
const source = handle.document.text[0..last_idx];
// We use a text based method since we cannot rely on a valid call node being here
// or even that it is the innermost call if it is.
const trigger = source[last_idx - 1];
// Check for comment, multiline string literal lines and skip
// Tokenize line forwards, pull in previous line if we need it etc.
switch (trigger) {
'(' => {
// go backwards while char in 'a'...'z','A'...'Z','0'...'9', '_', check for keywords
// resolve function, send result
// then add support for @"..." in the identifiers
},
',' => {
// Go backwards, keep a stack for (), [], {}, "" (with \" support)
// first ( to be hit when
},
else => {},
}
// 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 +1662,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, signatureHelperHandler },
.{ "textDocument/definition", requests.GotoDefinition, gotoDefinitionHandler },
.{ "textDocument/typeDefinition", requests.GotoDefinition, gotoDefinitionHandler },
.{ "textDocument/implementation", requests.GotoDefinition, gotoDefinitionHandler },

View File

@ -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,26 +111,34 @@ fn fromDynamicTreeInternal(arena: *std.heap.ArenaAllocator, value: std.json.Valu
}
} else if (T == std.json.Value) {
out.* = value;
} 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;
},
[]const u8 => {
if (value != .String) return error.MalformedJson;
out.* = value.String;
},
else => @compileError("Invalid type " ++ @typeName(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;
},
f64 => {
if (value != .Float) return error.MalformedJson;
out.* = value.Float;
},
[]const u8 => {
if (value != .String) return error.MalformedJson;
out.* = value.String;
},
else => @compileError("Invalid type " ++ @typeName(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;

View File

@ -289,12 +289,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,