diff --git a/build.zig b/build.zig index 68ca0c5..ded553b 100644 --- a/build.zig +++ b/build.zig @@ -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", diff --git a/src/analysis.zig b/src/analysis.zig index 04dafda..55f09d5 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -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| diff --git a/src/ast.zig b/src/ast.zig index 062d535..b92c17f 100644 --- a/src/ast.zig +++ b/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, + }; +} diff --git a/src/main.zig b/src/main.zig index ef56a43..7896b7d 100644 --- a/src/main.zig +++ b/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":[]}} ; @@ -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 }, diff --git a/src/requests.zig b/src/requests.zig index 6d3f4e0..520f932 100644 --- a/src/requests.zig +++ b/src/requests.zig @@ -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; diff --git a/src/types.zig b/src/types.zig index e6a087e..4aac7ea 100644 --- a/src/types.zig +++ b/src/types.zig @@ -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,